diff --git a/BossMod/Autorotation/Standard/akechi/AkechiBLM.cs b/BossMod/Autorotation/Standard/akechi/AkechiBLM.cs index 40779c425b..e6badffa6c 100644 --- a/BossMod/Autorotation/Standard/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/Standard/akechi/AkechiBLM.cs @@ -1,7 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Gauge; -using AID = BossMod.BLM.AID; -using SID = BossMod.BLM.SID; -using TraitID = BossMod.BLM.TraitID; +using BossMod.BLM; namespace BossMod.Autorotation.akechi; //Contribution by Akechi @@ -466,7 +464,7 @@ float AOEPriorityFunc(Actor actor) #endregion - public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { #region Variables var gauge = World.Client.GetGauge(); //Retrieve BLM gauge @@ -612,8 +610,8 @@ or MovementStrategy.AllowNoScathe #region Out of combat if (primaryTarget == null && - (tpusStrat == TPUSStrategy.Allow && (!Player.InCombat || Player.InCombat && TargetsInRange() is 0)) || - (tpusStrat == TPUSStrategy.OOConly && !Player.InCombat)) + ((tpusStrat == TPUSStrategy.Allow && (!Player.InCombat || Player.InCombat && TargetsInRange() is 0)) || + (tpusStrat == TPUSStrategy.OOConly && !Player.InCombat))) { if (Unlocked(AID.Transpose)) { @@ -827,7 +825,7 @@ private void BestST(Actor? target) //Single-target rotation based on level Player.InCombat) //if Blizzard III is unlocked QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III if (Unlocked(AID.Fire3) && - ((CD(AID.Manafont) < 5 && CD(AID.LeyLines) <= 121 && MP >= 10000)) || (!Player.InCombat && World.Client.CountdownRemaining <= 4)) //F3 opener + ((CD(AID.Manafont) < 5 && CD(AID.LeyLines) <= 121 && MP >= 10000) || (!Player.InCombat && World.Client.CountdownRemaining <= 4))) //F3 opener QueueGCD(AID.Fire3, target, canOpen ? GCDPriority.Opener : GCDPriority.NeedB3); } if (Player.Level is >= 1 and <= 34) @@ -936,7 +934,7 @@ private void BestST(Actor? target) //Single-target rotation based on level if (ElementTimer <= (GetCastTime(AID.Fire1) * 3) && //if time remaining on current element is less than 3x GCDs MP >= 4000) //and MP is 4000 or more QueueGCD(AID.Fire1, target, ElementTimer <= (GetCastTime(AID.Fire1) * 3) && MP >= 4000 ? GCDPriority.Paradox : GCDPriority.SecondStep); //Queue Fire I, increase priority if less than 3s left on element - //Step 4B - F3P + //Step 4B - F3P if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 AstralStacks == 3) //and Umbral Hearts are 0 QueueGCD(AID.Fire3, target, GCDPriority.ForcedStep); //Queue Fire III (AF3 F3P) @@ -945,7 +943,7 @@ private void BestST(Actor? target) //Single-target rotation based on level ((MP is < 1600 and >= 800) || //if MP is less than 1600 and not 0 (MP is <= 4000 and >= 800 && ElementTimer <= (GetCastTime(AID.Despair) * 2)))) //or if we dont have enough time for last F4s QueueGCD(AID.Despair, target, ElementTimer <= (GetCastTime(AID.Despair) * 2) ? GCDPriority.ForcedGCD : GCDPriority.ThirdStep); //Queue Despair - //Step 9 - swap from AF to UI + //Step 9 - swap from AF to UI if (MP <= 400) //and MP is less than 400 QueueGCD(AID.Blizzard3, target, GCDPriority.FourthStep); //Queue Blizzard III } @@ -1309,36 +1307,36 @@ private void BestAOE(Actor? target) //AOE rotation based on level private bool ShouldSpendPolyglot(Actor? target, PolyglotStrategy strategy) => strategy switch { PolyglotStrategy.AutoSpendAll - => Player.InCombat && - target != null && - Polyglots > 0 && //Spend 3 - (((CD(AID.Triplecast) < 5 || CD(AID.Triplecast) == 0 || (CD(AID.Triplecast) >= 59 && CD(AID.Triplecast) <= 65)) && PlayerHasEffect(SID.LeyLines, 30)) || //Triplecast prep - (CD(AID.LeyLines) < 5 || CD(AID.LeyLines) == 0 || CD(AID.LeyLines) <= 125 && CD(AID.LeyLines) >= 119) || //Ley Lines prep - CD(AID.Amplifier) < 0.6f || //Amplifier prep - (CD(AID.Manafont) < 0.6f && MP < 1600)), //Manafont prep + => target != null && + UsePolyglots(), PolyglotStrategy.AutoHold1 - => Player.InCombat && - target != null && - Polyglots > 1 && //Spend 2 - (((CD(AID.Triplecast) < 5 || CD(AID.Triplecast) == 0 || (CD(AID.Triplecast) >= 59 && CD(AID.Triplecast) <= 65)) && PlayerHasEffect(SID.LeyLines, 30)) || //Triplecast prep - (CD(AID.LeyLines) < 5 || CD(AID.LeyLines) == 0 || CD(AID.LeyLines) <= 125 && CD(AID.LeyLines) >= 119) || //Ley Lines prep - CD(AID.Amplifier) < 0.6f || //Amplifier prep - (CD(AID.Manafont) < 0.6f && MP < 1600)), //Manafont prep + => target != null && + Polyglots > 1 && + UsePolyglots(), PolyglotStrategy.AutoHold2 - => Player.InCombat && - target != null && - Polyglots > 2 && //Spend 1 - (((CD(AID.Triplecast) < 5 || (CD(AID.Triplecast) <= 60 && CD(AID.Triplecast) >= 65)) && PlayerHasEffect(SID.LeyLines, 30)) || //Triplecast prep - (CD(AID.LeyLines) < 5 || CD(AID.LeyLines) <= 120 && CD(AID.LeyLines) >= 110 || //Ley Lines prep - CD(AID.Amplifier) < 0.6f || //Amplifier prep - (CD(AID.Manafont) < 0.6f && MP < 1600))), //Manafont prep + => target != null && + Polyglots > 2 && + UsePolyglots(), PolyglotStrategy.AutoHold3 => Player.InCombat && target != null && Polyglots == MaxPolyglots && //if max Polyglots - EnochianTimer <= 10000f, //Enochian is 5s away from adding a Polyglot + (EnochianTimer <= 10000f || CD(AID.Amplifier) < GCD), //Enochian is 5s away from adding a Polyglot _ => false }; + private bool UsePolyglots() + { + if (Polyglots > 0) + { + if (Player.InCombat && + (((CD(AID.Triplecast) < 5 || (CD(AID.Triplecast) <= 60 && CD(AID.Triplecast) >= 65)) && PlayerHasEffect(SID.LeyLines, 30)) || //Triplecast prep + (CD(AID.LeyLines) < 5 || CD(AID.LeyLines) == 0 || CD(AID.LeyLines) <= 125 && CD(AID.LeyLines) >= 119) || //Ley Lines prep + CD(AID.Amplifier) < 0.6f || //Amplifier prep + (CD(AID.Manafont) < 0.6f && MP < 1600))) //Manafont prep + return true; + } + return false; + } private bool ShouldUsePolyglot(Actor? target, PolyglotStrategy strategy) => strategy switch { PolyglotStrategy.AutoSpendAll => ShouldSpendPolyglot(target, PolyglotStrategy.AutoSpendAll), diff --git a/BossMod/Autorotation/Standard/akechi/AkechiDRG.cs b/BossMod/Autorotation/Standard/akechi/AkechiDRG.cs index 52989731d3..0265cce05a 100644 --- a/BossMod/Autorotation/Standard/akechi/AkechiDRG.cs +++ b/BossMod/Autorotation/Standard/akechi/AkechiDRG.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Gauge; -using AID = BossMod.DRG.AID; -using SID = BossMod.DRG.SID; +using BossMod.DRG; namespace BossMod.Autorotation.akechi; //Contribution by Akechi diff --git a/BossMod/Autorotation/Standard/akechi/AkechiSCH.cs b/BossMod/Autorotation/Standard/akechi/AkechiSCH.cs index 1491d72271..6e42d68ef5 100644 --- a/BossMod/Autorotation/Standard/akechi/AkechiSCH.cs +++ b/BossMod/Autorotation/Standard/akechi/AkechiSCH.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Gauge; -using AID = BossMod.SCH.AID; -using SID = BossMod.SCH.SID; +using BossMod.SCH; namespace BossMod.Autorotation.akechi; //Contribution by Akechi diff --git a/BossMod/Autorotation/Standard/akechi/AkechiTools.cs b/BossMod/Autorotation/Standard/akechi/AkechiTools.cs new file mode 100644 index 0000000000..0f8555f1b8 --- /dev/null +++ b/BossMod/Autorotation/Standard/akechi/AkechiTools.cs @@ -0,0 +1,1245 @@ +using static BossMod.AIHints; + +namespace BossMod.Autorotation.akechi; +/// +/// The SharedTrack enum used for AOE and Hold strategies, typically for modules featuring damage rotations. +///
This enum defines tracks that can be used for all PvE classes and jobs, such as strategies containing executing standard rotations or explicitly holding abilities.
+/// Example Given: +///
- public enum Track { NoMercy = SharedTrack.Count }
+/// Explanation: +///
- Track is the enum for tracking specific abilities on user's specific rotation module.
+///
- NoMercy is the example enum of a specific ability being tracked on user's specific rotation module.
+///
- SharedTrack.Count is the shared track being used for executing our shared strategies listed above, called using .
+///
+/// - All strategies listed under into user's rotation module +public enum SharedTrack +{ + /// + /// The main strategy for tracking single-target and AOE rotations. + /// + /// - All strategies listed under into user's rotation module + AOE, + + /// + /// The main strategy used for tracking when to hold any buffs, gauge, or cooldown abilties for optimal usage. + /// + /// - All strategies listed under into user's rotation module + Hold, + + /// + /// Represents the total count of strategies available inside this specific track. We generally never actually use this as a strategy since there isn't any logic really linked to this besides the counting. + /// + Count +} + +/// +/// The Default Strategy enum used for tracking single-target and AOE strategies. +/// Example Given:
+/// - strategy.Option(SharedTrack.AOE).As<>(); +/// Explanation:
+/// - "strategy" is the parameter for tracking a specific strategy for a specific ability in the user's rotation module.
+/// - "Option" are the relative options for user's specific strategy.
+/// - "(SharedTrack.AOE)" is the enum representing all options relating to this custom strategy being tracked in the user's rotation module.
+/// - ".As<>();" is the relative strategy for user's specific ability being tracked in the user's rotation module. +///
+/// - All strategies listed under into user's rotation module +public enum AOEStrategy +{ + /// + /// The default strategy used for automatically executing the rotation.
+ /// This can also be called using , or also as `strategy.Automatic()` in some cases. + /// Example:
+ /// - strategy.Option(SharedTrack.AOE).As<>() == + /// Explanation:
+ /// - "strategy.Option(SharedTrack.AOE).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - The most optimal rotation automatically executed. + Automatic, + + /// + /// The main strategy used for force-executing the single-target rotation.
+ /// This can also be called using , or also as `strategy.ForceST()` in some cases. + /// Example:
+ /// - strategy.Option(SharedTrack.AOE).As<>() == + /// Explanation:
+ /// - "strategy.Option(SharedTrack.AOE).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - The single-target rotation force-executed, regardless of any conditions. + ForceST, + + /// + /// The main strategy used for force-executing the AOE rotation.
+ /// This can also be called using , or also as `strategy.ForceAOE()` in some cases. + /// Example:
+ /// - strategy.Option(SharedTrack.AOE).As<>() == + /// Explanation:
+ /// - "strategy.Option(SharedTrack.AOE).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - The AOE rotation force-executed, regardless of any conditions. + ForceAOE +} + +/// +/// The Default Strategy enum used for tracking when to hold any buffs, gauge, or cooldown abilties for optimal usage. +/// Example Given:
+/// - strategy.Option(SharedTrack.Hold).As<>(); +/// Explanation:
+/// - "strategy" is the parameter for tracking a specific strategy for a specific ability in the user's rotation module.
+/// - "Option" are the relative options for user's specific strategy.
+/// - "(SharedTrack.Hold)" is the enum representing all options relating to this custom strategy being tracked in the user's rotation module.
+/// - ".As<>();" is the relative strategy for user's specific ability being tracked in the user's rotation module. +///
+/// - All strategies listed under into user's rotation module +public enum HoldStrategy +{ + /// + /// The default strategy used for not holding any buffs, gauge, or cooldown abilties.
+ /// This can also be called using , or also as `strategy.DontHold()` in some cases. + /// Example:
+ /// - strategy.Option(SharedTrack.Hold).As<>() == + /// Explanation:
+ /// - "strategy.Option(SharedTrack.Hold).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - The availability of using all buffs, gauge, or cooldown abilties. + DontHold, + + /// + /// The main strategy used for only holding any ability that is cooldown-related.
+ /// This can also be called using , or also as `strategy.HoldCDs()` in some cases. + /// Example:
+ /// - strategy.Option(SharedTrack.Hold).As<>() == + /// Explanation:
+ /// - "strategy.Option(SharedTrack.Hold).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - The availability of using all buffs and gauge abilties, but forbidden from using any cooldowns. + HoldCooldowns, + + /// + /// The main strategy used for only holding any ability that is gauge-related.
+ /// This can also be called using , or also as `strategy.HoldGauge()` in some cases. + /// Example:
+ /// - strategy.Option(SharedTrack.Hold).As<>() == + /// Explanation:
+ /// - "strategy.Option(SharedTrack.Hold).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - The availability of using all buffs and cooldowns, but forbidden from using any gauge abilties. + HoldGauge, + + /// + /// The main strategy used for only holding any ability that is buff-related.
+ /// This can also be called using , or also as `strategy.HoldBuffs()` in some cases. + /// Example:
+ /// - strategy.Option(SharedTrack.Hold).As<>() == + /// Explanation:
+ /// - "strategy.Option(SharedTrack.Hold).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - The availability of using all cooldowns and gauge abilties, but forbidden from using any buffs. + HoldBuffs, + + /// + /// The main strategy used for holding any ability that is buff, cooldown, or gauge-related.
+ /// This can also be called using , or also as `strategy.HoldAll()` in some cases. + /// Example:
+ /// - strategy.Option(SharedTrack.Hold).As<>() == + /// Explanation:
+ /// - "strategy.Option(SharedTrack.Hold).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - The forbiddance from using all buffs, cooldowns and gauge abilties. + HoldEverything +} + +/// +/// The Default Strategy enum used for allowing or forbidding use of module-specific GCD abilities. +/// Example Given:
+/// - strategy.Option(Track.SonicBreak).As<>() +/// Explanation:
+/// - "strategy" is the parameter for tracking a specific strategy for a specific ability in the user's rotation module.
+/// - "Option" are the relative options for user's specific strategy.
+/// - "(Track.SonicBreak)" is the user's module-specific GCD ability enum being tracked.
+/// - ".As<>()" is the relative Default strategy for user's module-specific GCD ability being tracked in the user's rotation module. +///
+/// - All strategies listed under . +public enum GCDStrategy +{ + /// + /// The default strategy used for automatically executing user's module-specific GCD ability. + /// Example:
+ /// - strategy.Option(Track.SonicBreak).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.SonicBreak).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Automatic execution of ability based on user's logic. + Automatic, + + /// + /// The main strategy used for force-executing user's module-specific GCD ability. + /// Example:
+ /// - strategy.Option(Track.SonicBreak).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.SonicBreak).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Forced execution of user's module-specific GCD ability. + Force, + + /// + /// The main strategy used for forbidding use of user's module-specific GCD ability. + /// Example:
+ /// - strategy.Option(Track.SonicBreak).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.SonicBreak).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Forbiddance of execution for user's module-specific GCD ability. + Delay +} + +/// +/// The Default Strategy enum used for allowing or forbidding use of module-specific GCD abilities. +/// Example Given:
+/// - strategy.Option(Track.NoMercy).As<>() +/// Explanation:
+/// - "strategy" is the parameter for tracking a specific strategy for a specific ability in the user's rotation module.
+/// - "Option" are the relative options for user's specific strategy.
+/// - "(Track.NoMercy)" is the user's module-specific OGCD ability enum being tracked.
+/// - ".As<>()" is the relative Default strategy for user's module-specific OGCD ability being tracked in the user's rotation module. +///
+/// - All strategies listed under . +public enum OGCDStrategy +{ + /// + /// The default strategy used for automatically executing user's module-specific OGCD ability. + /// Example:
+ /// - strategy.Option(Track.NoMercy).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.NoMercy).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Automatic execution of ability based on user's logic. + Automatic, + + /// + /// The main strategy used for force-executing user's module-specific OGCD ability. + /// Example:
+ /// - strategy.Option(Track.NoMercy).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.NoMercy).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Forced execution of user's module-specific OGCD ability. + Force, + + /// + /// The main strategy used for force-executing user's module-specific OGCD ability in the very next weave window. + /// Example:
+ /// - strategy.Option(Track.NoMercy).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.NoMercy).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Forced execution of user's module-specific OGCD ability inside next weave. + AnyWeave, + + /// + /// The main strategy used for force-executing user's module-specific OGCD ability in the first-half of very next weave window. + /// Example:
+ /// - strategy.Option(Track.NoMercy).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.NoMercy).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Forced execution of user's module-specific OGCD ability inside next early-weave. + EarlyWeave, + + /// + /// The main strategy used for force-executing user's module-specific OGCD ability in the second-half of very next weave window. + /// Example:
+ /// - strategy.Option(Track.NoMercy).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.NoMercy).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Forced execution of user's module-specific OGCD ability inside next late-weave. + LateWeave, + + /// + /// The main strategy used for forbidding use of user's module-specific OGCD ability. + /// Example:
+ /// - strategy.Option(Track.NoMercy).As<>() == + /// Explanation:
+ /// - "strategy.Option(Track.NoMercy).As<>()" is the full function (or local variable if user desires) for calling .
+ /// - "" is the chosen option for this specific strategy.
+ ///
+ /// - Forbiddance of execution for user's module-specific OGCD ability. + Delay +} + +/// The core foundation of how we execute everything, from queuing GCDs to implementing our rotation helpers, functions, and tools.
This base provides a robust framework equipped with a comprehensive suite of functions designed to streamline optimization and simplify the creation of advanced rotation modules.
+/// The user's specified Action ID being checked, called by using .[class/job acronym]. +/// The user's specified Trait ID being checked, called by using .[class/job acronym]. +/// The specified Rotation Module Manager being used. +/// The User that is executing this module. +public abstract class AkechiTools(RotationModuleManager manager, Actor player) : RotationModule(manager, player) + where AID : struct, Enum where TraitID : Enum +{ + #region Core Ability Execution + /// The Next GCD being queued. + protected AID NextGCD; + + /// The Next GCD being queued's Priority. + protected int NextGCDPrio; + + #region Queuing + #region GCD + /// + /// The primary helper we use for calling all our GCD abilities onto any actor. + ///
This also handles Ground Target abilities, such as BLM:LeyLines or NIN:Shukuchi
+ ///
+ /// The user's specified Action ID being checked. + /// The user's specified Target being checked. + /// The user's specified Priority. + /// The user's specified application delay. + /// The user's specified cast time for the ability. + protected void QueueGCD

(AID aid, Actor? target, P priority, float delay = 0, float castTime = 0) where P : Enum + => QueueGCD(aid, target, (int)(object)priority, delay, castTime); + + ///

+ /// The primary helper we use for calling all our GCD abilities onto any enemy. + ///
This also handles Ground Target abilities, such as BLM:LeyLines or NIN:Shukuchi
+ ///
+ /// The user's specified Action ID being checked. + /// The user's specified Target being checked. + /// The user's specified Priority. + /// The user's specified application delay. + /// The user's specified cast time for the ability. + protected void QueueGCD

(AID aid, Enemy? target, P priority, float delay = 0, float castTime = 0) where P : Enum => QueueGCD(aid, target?.Actor, (int)(object)priority, delay, castTime); + protected void QueueGCD(AID aid, Enemy? target, int priority = 2, float delay = 0, float castTime = 0) => QueueGCD(aid, target?.Actor, priority, delay, castTime); + protected void QueueGCD(AID aid, Actor? target, int priority = 2, float delay = 0, float castTime = 0) + { + if (priority == 0) + return; + + if (QueueAction(aid, target, ActionQueue.Priority.High + priority, delay, castTime) && priority > NextGCDPrio) + { + NextGCD = aid; + NextGCDPrio = priority; + } + } + #endregion + + #region OGCD + ///

+ /// The primary helper we use for calling all our OGCD abilities onto any actor.
+ /// This also handles Ground Target abilities, such as BLM:LeyLines or NIN:Shukuchi + /// NOTE: For compatibility between Actor? and Enemy? inside one function, use `primarytarget?.Actor` as `Enemy?` definition. + ///
+ /// The user's specified Action ID being checked. + /// The user's specified Target being checked. + /// The user's specified Priority. + /// The user's specified application delay. + /// The user's specified cast time for the ability. + protected void QueueOGCD

(AID aid, Actor? target, P priority, float delay = 0, float castTime = 0) where P : Enum => QueueOGCD(aid, target, (int)(object)priority, delay, castTime); + + ///

+ /// The primary helper we use for calling all our OGCD abilities onto any enemy.
+ /// This also handles Ground Target abilities, such as BLM:LeyLines or NIN:Shukuchi
+ /// NOTE: For compatibility between Actor? and Enemy? inside one function, use `primarytarget?.Actor` as `Enemy?` definition. + ///
+ /// The user's specified Action ID being checked. + /// The user's specified Target being checked. + /// The user's specified Priority. + /// The user's specified application delay. + /// The user's specified cast time for the ability. + protected void QueueOGCD

(AID aid, Enemy? target, P priority, float delay = 0, float castTime = 0) where P : Enum => QueueOGCD(aid, target?.Actor, (int)(object)priority, delay, castTime); + protected void QueueOGCD(AID aid, Enemy? target, int priority = 1, float delay = 0, float castTime = 0) => QueueOGCD(aid, target?.Actor, priority, delay, castTime); + protected void QueueOGCD(AID aid, Actor? target, int priority = 1, float delay = 0, float castTime = 0) + { + if (priority == 0) + return; + + QueueAction(aid, target, ActionQueue.Priority.Low + priority, delay, castTime); + } + #endregion + + protected bool QueueAction(AID aid, Actor? target, float priority, float delay, float castTime) + { + var def = ActionDefinitions.Instance.Spell(aid); + Vector3 targetPos = default; + + if ((uint)(object)aid == 0) + return false; + + if (def == null) + return false; + + if (def.Range != 0 && target == null) + { + return false; + } + + //Ground Targeting abilities + if (def.AllowedTargets.HasFlag(ActionTargets.Area)) + { + if (def.Range == 0) + targetPos = Player.PosRot.XYZ(); + else if (target != null) + targetPos = target.PosRot.XYZ(); + } + + //TODO: is this necessary? idk maybe for melees + if (castTime == 0) + castTime = 0; + + Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, priority, delay: delay, castTime: castTime, targetPos: targetPos); + return true; + } + #endregion + + #endregion + + #region HP/MP/Shield + ///

+ /// A quick and easy helper for retrieving the current HP value of the player. + /// Example:
+ /// - HP >= 6900 + /// Explanation:
+ /// - "HP" represents the current HP value of the player.
+ /// - ">= 6900" is the example conditional expression specified by the user.
+ ///
+ /// - A value representing the Player's current HP + protected uint HP { get; private set; } + + /// + /// A quick and easy helper for retrieving the current MP value of the player. + /// Example:
+ /// - MP != 4200 + /// Explanation:
+ /// - "MP" represents the current MP value of the player.
+ /// - "!= 4200" is the example conditional expression specified by the user.
+ ///
+ /// - A value representing the Player's current MP + protected uint MP { get; private set; } + + /// + /// A quick and easy helper for retrieving the current Shield value of the player. + /// Example:
+ /// - Shield > 0 + /// Explanation:
+ /// - "Shield" represents the current Shield value of the player.
+ /// - "> 0" is the example conditional expression specified by the user.
+ ///
+ /// - A value representing the Player's current Shield + protected uint Shield { get; private set; } + + /// + /// A quick and easy helper for retrieving the Current HP of any specified actor, whether it is the player or any other target user desires. + /// Example:
+ /// - TargetCurrentHP(primaryTarget) > 0 + /// Explanation:
+ /// - "TargetCurrentHP" represents the current HP value of the specified actor.
+ /// - "(primaryTarget)" represents the specified actor being checked.
+ /// - "> 0" is the example conditional expression specified by the user.
+ ///
+ /// Any specified player, ally, or target + /// - A value representing the current HP of user's specified actor + protected uint TargetCurrentHP(Actor actor) => actor.HPMP.CurHP; + + /// + /// A quick and easy helper for retrieving the Current Shield of any specified actor, whether it is the player or any other target user desires. + /// Example:
+ /// - TargetCurrentShield(primaryTarget) > 0 + /// Explanation:
+ /// - "TargetCurrentShield" represents the current Shield value of the specified actor.
+ /// - "(primaryTarget)" represents the specified actor being checked.
+ /// - "> 0" is the example conditional expression specified by the user.
+ ///
+ /// Any specified player, ally, or target + /// - A value representing the current Shield of user's specified actor. + protected uint TargetCurrentShield(Actor actor) => actor.HPMP.Shield; + + /// + /// A quick and easy helper for checking if specified actor has any current Shield present, whether it is the player or any other target user desires. + /// Example:
+ /// - TargetHasShield(primaryTarget) + /// Explanation:
+ /// - "TargetHasShield" checks if the specified actor has any current Shield value.
+ /// - "(primaryTarget)" represents the specified actor being checked.
+ ///
+ /// Any specified player, ally, or target + protected bool TargetHasShield(Actor actor) => actor.HPMP.Shield > 0.1f; + + /// + /// A quick and easy helper for retrieving the Current HP Percentage of any specified actor, whether it is the player or any other target user desires. + /// Example:
+ /// - TargetHPP(primaryTarget) > 50 + /// Explanation:
+ /// - "TargetHPP" represents the current HP Percentage value of the specified actor.
+ /// - "(primaryTarget)" represents the specified actor being checked.
+ /// - "> 50" is the example conditional expression specified by the user.
+ ///
+ /// Any specified player, ally, or target + /// - A value representing the current HP Percentage (%) of user's specified actor. + public static float TargetHPP(Actor? target = null) + { + if (target is null || target.IsDead) + return 0f; + + if (target is Actor actor) + { + var HPP = (float)actor.HPMP.CurHP / actor.HPMP.MaxHP * 100f; + return Math.Clamp(HPP, 0f, 100f); + } + + return 0f; + } + #endregion + + #region Actions + /// + /// Checks if specified action is Unlocked based on Level and Job Quest (if required). + /// Example:
+ /// - Unlocked(AID.GnashingFang) + /// Explanation:
+ /// - "Unlocked" is the function.
+ /// - "(AID.GnashingFang)" is the example ability being checked, called using .. + ///
+ /// The user's specified Action ID being checked. + /// - TRUE if the specified action is Unlocked.
+ /// - FALSE if the specified action is still locked or Job Quest is unfinished.
+ protected bool Unlocked(AID aid) => ActionUnlocked(ActionID.MakeSpell(aid)); //Check if the desired ability is unlocked + + /// + /// Checks if specified trait is Unlocked based on Level and Job Quest (if required). + /// Example:
+ /// - Unlocked(TraitID.EnhancedBloodfest) + /// Explanation:
+ /// - "Unlocked" is the function.
+ /// - "(TraitID.EnhancedBloodfest)" is the example trait being checked, called using .. + ///
+ /// The user's specified Trait ID being checked. + /// - TRUE if the specified action is Unlocked.
+ /// - FALSE if the specified action is still locked or Job Quest is unfinished.
+ protected bool Unlocked(TraitID tid) => TraitUnlocked((uint)(object)tid); + + /// + /// Checks if last combo action is what the user is specifying.
+ /// NOTE: This does NOT check all actions, only combo actions. + /// Example:
+ /// - ComboLastMove == AID.BrutalShell + /// Explanation:
+ /// - "ComboLastMove" is the function.
+ /// - "AID.BrutalShell" is the example specified combo action being checked, called using .. + ///
+ /// - TRUE if the last combo action is what the user is specifying.
+ /// - FALSE if otherwise or last action was not a combo action.
+ protected AID ComboLastMove => (AID)(object)World.Client.ComboState.Action; + + /// + /// Retrieves actual cast time of a specified action. + /// Example:
+ /// - ActualCastTime(AID.Fire3) > 0 + /// Explanation:
+ /// - "ActualCastTime" is the function.
+ /// - "AID.Fire3" is the example specified action being checked, called using ..
+ /// - "> 0" is the example conditional expression specified by the user.
+ ///
+ /// The user's specified Action ID being checked. + /// - A value representing the current real-time Cast Time of user's specified action + protected virtual float ActualCastTime(AID aid) => ActionDefinitions.Instance.Spell(aid)!.CastTime; + + /// + /// Retrieves effective cast time of a specified action. + /// Example:
+ /// - EffectiveCastTime(AID.Fire3) > 0 + /// Explanation:
+ /// - "EffectiveCastTime" is the function.
+ /// - "AID.Fire3" is the example specified action being checked, called using ..
+ /// - "> 0" is the example conditional expression specified by the user.
+ ///
+ /// The user's specified Action ID being checked. + /// - A value representing the current effective Cast Time of user's specified action + protected virtual float EffectiveCastTime(AID aid) => PlayerHasEffect(ClassShared.SID.Swiftcast, 10) ? 0 : ActualCastTime(aid) * SpSGCDLength / 2.5f; + + /// Retrieves player's GCD length based on Skill-Speed. + /// NOTE: This function is only recommended for jobs with Skill-Speed. Spell-Speed users are unaffected by this function. + /// - A value representing the player's current GCD Length + protected float SkSGCDLength => ActionSpeed.GCDRounded(World.Client.PlayerStats.SkillSpeed, World.Client.PlayerStats.Haste, Player.Level); + + /// Retrieves player's current Skill-Speed stat. + /// - A value representing the player's current Skill-Speed + protected float SkS => ActionSpeed.Round(World.Client.PlayerStats.SkillSpeed); + + /// Retrieves player's GCD length based on Spell-Speed. + /// NOTE: This function is only recommended for jobs with Spell-Speed. Skill-Speed users are unaffected by this function. + /// - A value representing the player's current GCD Length + protected float SpSGCDLength => ActionSpeed.GCDRounded(World.Client.PlayerStats.SpellSpeed, World.Client.PlayerStats.Haste, Player.Level); + + /// Retrieves player's current Spell-Speed stat. + /// - A value representing the player's current Spell-Speed + protected float SpS => ActionSpeed.Round(World.Client.PlayerStats.SpellSpeed); + + /// Checks if we can fit in a skill-speed based GCD. + /// + /// How many extra GCDs the user can fit in. + /// - TRUE if we can fit in a skill-speed based GCD - FALSE if otherwise + protected bool CanFitSkSGCD(float duration, int extraGCDs = 0) => GCD + SkSGCDLength * extraGCDs < duration; + + /// Checks if we can fit in a spell-speed based GCD. + /// + /// How many extra GCDs the user can fit in. + /// - TRUE if we can fit in a spell-speed based GCD - FALSE if otherwise + protected bool CanFitSpSGCD(float duration, int extraGCDs = 0) => GCD + SpSGCDLength * extraGCDs < duration; + + /// Checks if player is available to weave in any abilities. + /// NOTE: This function is only recommended for jobs with Skill-Speed. Spell-Speed users are unaffected by this. + /// The cooldown time of the action specified. + /// The animation lock time of the action specified. + /// How many extra GCDs the user can fit in. + /// How much extra delay the user can add in, in seconds. + /// - TRUE if we can weave in a skill-speed based GCD - FALSE if otherwise + protected bool CanWeave(float cooldown, float actionLock, int extraGCDs = 0, float extraFixedDelay = 0) + => MathF.Max(cooldown, World.Client.AnimationLock) + actionLock + AnimationLockDelay <= GCD + SkSGCDLength * extraGCDs + extraFixedDelay; + + /// Checks if player is available to weave in any spells. + /// NOTE: This function is only recommended for jobs with Spell-Speed. Skill-Speed users are unaffected by this. + /// The cooldown time of the action specified. + /// The animation lock time of the action specified. + /// How many extra GCDs the user can fit in. + /// How much extra delay the user can add in, in seconds. + /// - TRUE if we can weave in a spell-speed based GCD - FALSE if otherwise + protected bool CanSpellWeave(float cooldown, float actionLock, int extraGCDs = 0, float extraFixedDelay = 0) + => MathF.Max(cooldown, World.Client.AnimationLock) + actionLock + AnimationLockDelay <= GCD + SpSGCDLength * extraGCDs + extraFixedDelay; + + /// Checks if player is available to weave in any abilities. + /// NOTE: This function is only recommended for jobs with Skill-Speed. Spell-Speed users are unaffected by this. + /// The user's specified Action ID being checked. + /// How many extra GCDs the user can fit in. + /// How much extra delay the user can add in, in seconds. + /// - TRUE if we can weave in any abilities - FALSE if otherwise + protected bool CanWeave(AID aid, int extraGCDs = 0, float extraFixedDelay = 0) + { + if (!Unlocked(aid)) + return false; + + var res = ActionDefinitions.Instance[ActionID.MakeSpell(aid)]!; + return SkS > 100 + ? CanSpellWeave(ChargeCD(aid), res.InstantAnimLock, extraGCDs, extraFixedDelay) + : SpS > 100 && CanWeave(ChargeCD(aid), res.InstantAnimLock, extraGCDs, extraFixedDelay); + } + + /// Checks if user is in pre-pull stage; useful for First GCD openings. + /// - TRUE if user is in pre-pull stage or fully not in combat- FALSE if otherwise + protected bool IsFirstGCD() => !Player.InCombat || (World.CurrentTime - Manager.CombatStart).TotalSeconds < 0.1f; + + /// Checks if user can Weave in any abilities.NOTE: This function is pretty sub-optimal, but gets the job done. CanWeave() is much more intricate if user really wants it. + protected bool CanWeaveIn => GCD is <= 2.49f and >= 0.01f; + + /// Checks if user can Early Weave in any abilities.NOTE: This function is pretty sub-optimal, but gets the job done. CanWeave() is much more intricate if user really wants it. + protected bool CanEarlyWeaveIn => GCD is <= 2.49f and >= 1.26f; + + /// Checks if user can Late Weave in any abilities.NOTE: This function is pretty sub-optimal, but gets the job done. CanWeave() is much more intricate if user really wants it. + protected bool CanLateWeaveIn => GCD is <= 1.25f and >= 0.01f; + + /// Checks if user can Quarter Weave in any abilities.NOTE: This function is pretty sub-optimal, but gets the job done. CanWeave() is much more intricate if user really wants it. + protected bool CanQuarterWeaveIn => GCD is < 0.9f and >= 0.01f; + #endregion + + #region Cooldown + /// Retrieves the total cooldown time left on the specified action. + /// The user's specified Action ID being checked. + /// - A value representing the current cooldown on user's specified Action ID + protected float TotalCD(AID aid) => World.Client.Cooldowns[ActionDefinitions.Instance.Spell(aid)!.MainCooldownGroup].Remaining; + + /// Returns the charge cooldown time left on the specified action. + /// The user's specified Action ID being checked. + /// - A value representing the current charge cooldown on user's specified Action ID + protected float ChargeCD(AID aid) => Unlocked(aid) ? ActionDefinitions.Instance.Spell(aid)!.ReadyIn(World.Client.Cooldowns, World.Client.DutyActions) : float.MaxValue; + + /// Checks if action is ready to be used based on if it's Unlocked and its charge cooldown timer. + /// The user's specified Action ID being checked. + /// - TRUE if the specified Action is Unlocked and off cooldown. - FALSE if locked or still on cooldown + protected bool ActionReady(AID aid) => Unlocked(aid) && ChargeCD(aid) < 0.6f; + + /// Checks if action has any charges remaining. + /// The user's specified Action ID being checked. + /// - TRUE if the specified Action has any charges available - FALSE if locked or still on cooldown + protected bool HasCharges(AID aid) => ChargeCD(aid) < 0.6f; + + /// Checks if action is on cooldown based on its total cooldown timer. + /// The user's specified Action ID being checked. + /// - TRUE if the specified Action is still on cooldown - FALSE if off cooldown + protected bool IsOnCooldown(AID aid) => TotalCD(aid) > 0.6f; + + /// Checks if action is off cooldown based on its total cooldown timer. + /// The user's specified Action ID being checked. + /// - TRUE if the specified Action is off cooldown - FALSE if still on cooldown + protected bool IsOffCooldown(AID aid) => !IsOnCooldown(aid); + + /// Checks if action is off cooldown based on its charge cooldown timer. + /// The user's specified Action ID being checked. + /// - TRUE if the specified Action is off cooldown - FALSE if still on cooldown + protected bool OnCooldown(AID aid) => MaxChargesIn(aid) > 0; + + /// Checks if last action used is what the user is specifying and within however long. + /// The user's specified Action ID being checked. + /// - TRUE if the specified Action was just used - FALSE if was not just used + protected bool LastActionUsed(AID aid) => Manager.LastCast.Data?.IsSpell(aid) == true; + + /// Retrieves time remaining until specified action is at max charges. + /// The user's specified Action ID being checked. + protected float MaxChargesIn(AID aid) => Unlocked(aid) ? ActionDefinitions.Instance.Spell(aid)!.ChargeCapIn(World.Client.Cooldowns, World.Client.DutyActions, Player.Level) : float.MaxValue; + + #endregion + + #region Status + /// Retrieves the amount of specified status effect's stacks remaining on any target. + /// NOTE: The effect MUST be owned by the Player. + /// Example Given: "StacksRemaining(Player, SID.Requiescat, 30) > 0" + /// The user's specified Target being checked. + /// The user's specified Status ID being checked. + /// The Total Effect Duration of specified Status ID being checked. + /// - A value indicating how many stacks exist + protected int StacksRemaining(Actor? target, SID sid, float duration = 1000f) where SID : Enum => StatusDetails(target, sid, Player.InstanceID, duration).Stacks; + + /// Retrieves the amount of specified status effect's time left remaining on any target. + /// NOTE: The effect MUST be owned by the Player. + /// Example Given: "StatusRemaining(Player, SID.Requiescat, 30) > 0f" + /// The user's specified Target being checked. + /// The user's specified Status ID being checked. + /// The Total Effect Duration of specified Status ID being checked. + /// - A value indicating how much time left on existing effect + protected float StatusRemaining(Actor? target, SID sid, float duration) where SID : Enum => StatusDetails(target, sid, Player.InstanceID, duration).Left; + + /// Checks if a specific status effect on the player exists. + /// NOTE: The effect MUST be owned by the Player. + /// Example Given: "PlayerHasEffect(SID.NoMercy, 20)" + /// The user's specified Status ID being checked. + /// The Total Effect Duration of specified Status ID being checked. + /// - A value indicating if the effect exists + protected bool PlayerHasEffect(SID sid, float duration) where SID : Enum => StatusRemaining(Player, sid, duration) > 0.1f; + + /// Checks if a specific status effect on the player exists. + /// NOTE: The effect can be owned by anyone; Player, Party, Alliance, NPCs or even enemies + /// Example Given: "PlayerHasEffectAny(SID.Troubadour)" + /// The user's specified Status ID being checked. + /// The Total Effect Duration of specified Status ID being checked. + /// - A value indicating if the effect exists + protected bool PlayerHasAnyEffect(SID sid) where SID : Enum => Player.FindStatus(sid) != null; + + /// Checks if a specific status effect on any specified target exists. + /// NOTE: The effect MUST be owned by the Player. + /// Example Given: "TargetHasEffect(primaryTarget, SID.SonicBreak, 30)" + /// The user's specified Target being checked. + /// The user's specified Status ID being checked. + /// The Total Effect Duration of specified Status ID being checked. + /// - A value indicating if the effect exists + protected bool TargetHasEffect(Actor? target, SID sid, float duration = 1000f) where SID : Enum => StatusRemaining(target, sid, duration) > 0.1f; + + /// Checks if a specific status effect on any specified target exists. + /// NOTE: The effect can be owned by anyone; Player, Party, Alliance, NPCs or even enemies + /// Example Given: "TargetHasAnyEffect(primaryTarget, SID.MeditativeBrotherhood)" + /// The user's specified Target being checked. + /// The user's specified Status ID being checked. + /// - A value indicating if the effect exists + protected bool TargetHasAnyEffect(Actor? target, SID sid) where SID : Enum => target?.FindStatus(sid) != null; + + /// Checks if Player has any stacks of specific status effect. + /// NOTE: The effect MUST be owned by the Player. + /// Example Given: "PlayerHasStacks(SID.Requiescat)" + /// The user's specified Status ID being checked. + /// - A value indicating if the effect exists + protected bool PlayerHasStacks(SID sid) where SID : Enum => StacksRemaining(Player, sid) > 0; + + #endregion + + #region Targeting + + #region Position Checks + /// + /// Checks precise positioning between player target and any other targets. + /// + protected delegate bool PositionCheck(Actor playerTarget, Actor targetToTest); + /// + /// Calculates the priority of a target based on the total number of targets and the primary target itself. + /// It is generic, so it can return different types based on the implementation. + /// + protected delegate P PriorityFunc

(int totalTargets, Actor primaryTarget); + ///

+ /// Position checker for determining the best target for an ability that deals Splash damage. + /// + protected PositionCheck IsSplashTarget => (primary, other) => Hints.TargetInAOECircle(other, primary.Position, 5); + /// + /// Position checker for determining the best target for an ability that deals damage in a Cone . + /// + protected PositionCheck IsConeTarget => (primary, other) => Hints.TargetInAOECone(other, Player.Position, 8, Player.DirectionTo(primary), 45.Degrees()); + /// + /// Position checker for determining the best target for an ability that deals damage in a Line within Ten (10) yalms. + /// + protected PositionCheck Is10yRectTarget => (primary, other) => Hints.TargetInAOERect(other, Player.Position, Player.DirectionTo(primary), 10, 2); + /// + /// Position checker for determining the best target for an ability that deals damage in a Line within Fifteen (15) yalms. + /// + protected PositionCheck Is15yRectTarget => (primary, other) => Hints.TargetInAOERect(other, Player.Position, Player.DirectionTo(primary), 15, 2); + /// + /// Position checker for determining the best target for an ability that deals damage in a Line within Twenty-five (25) yalms + /// + protected PositionCheck Is25yRectTarget => (primary, other) => Hints.TargetInAOERect(other, Player.Position, Player.DirectionTo(primary), 25, 2); + #endregion + + /// + /// Checks if target is within Zero (0) yalms in distance, or if Player is inside hitbox. + /// + /// The user's specified Target being checked. + /// + protected bool In0y(Actor? target) => Player.DistanceToHitbox(target) <= 0.00f; + + /// + /// Checks if target is within Three (3) yalms in distance. + /// + /// The user's specified Target being checked. + /// + protected bool In3y(Actor? target) => Player.DistanceToHitbox(target) <= 2.99f; + + /// + /// Checks if target is within Five (5) yalms in distance. + /// + /// The user's specified Target being checked. + /// + protected bool In5y(Actor? target) => Player.DistanceToHitbox(target) <= 4.99f; + + /// + /// Checks if target is within Ten (10) yalms in distance. + /// + /// The user's specified Target being checked. + /// + protected bool In10y(Actor? target) => Player.DistanceToHitbox(target) <= 9.99f; + + /// + /// Checks if target is within Fifteen (15) yalms in distance. + /// + /// The user's specified Target being checked. + /// + protected bool In15y(Actor? target) => Player.DistanceToHitbox(target) <= 14.99f; + + /// + /// Checks if target is within Twenty (20) yalms in distance. + /// + /// The user's specified Target being checked. + /// + protected bool In20y(Actor? target) => Player.DistanceToHitbox(target) <= 19.99f; + + /// + /// Checks if target is within Twenty-five (25) yalms in distance. + /// + /// The user's specified Target being checked. + /// + protected bool In25y(Actor? target) => Player.DistanceToHitbox(target) <= 24.99f; + + /// + /// A simpler smart-targeting helper for picking a specific target over your current target. + /// Very useful for intricate planning of ability targeting in specific situations. + /// + /// The user's picked strategy's option Track, retrieved from module's enums and definitions. (e.g. strategy.Option(Track.NoMercy)) + /// + protected Actor? TargetChoice(StrategyValues.OptionRef track) => ResolveTargetOverride(track.Value); //Resolves the target choice based on the strategy + + /// Targeting function for indicating when or not AOE Circle abilities should be used based on targets nearby. + /// The range of the AOE Circle ability, or radius from center of Player; this should be adjusted accordingly to user's module specific to job's abilities. + /// - A tuple with the following booleans: + /// -- OnTwoOrMore: A boolean indicating if there are two (2) or more targets inside Player's AOE Circle. + /// -- OnThreeOrMore: A boolean indicating if there are three (3) or more targets inside Player's AOE Circle. + /// -- OnFourOrMore: A boolean indicating if there are four (4) or more targets inside Player's AOE Circle. + /// -- OnFiveOrMore: A boolean indicating if there are five (5) or more targets inside Player's AOE Circle. + protected (bool OnTwoOrMore, bool OnThreeOrMore, bool OnFourOrMore, bool OnFiveOrMore) ShouldUseAOECircle(float range) + { + var OnTwoOrMore = Hints.NumPriorityTargetsInAOECircle(Player.Position, range) > 1; + var OnThreeOrMore = Hints.NumPriorityTargetsInAOECircle(Player.Position, range) > 2; + var OnFourOrMore = Hints.NumPriorityTargetsInAOECircle(Player.Position, range) > 3; + var OnFiveOrMore = Hints.NumPriorityTargetsInAOECircle(Player.Position, range) > 4; + + return (OnTwoOrMore, OnThreeOrMore, OnFourOrMore, OnFiveOrMore); + } + + /// + /// This function attempts to pick a suitable primary target automatically, even if a target is not already picked. + /// + /// The user's picked Strategy + /// The user's current specified Target. + /// + protected void GetPrimaryTarget(StrategyValues strategy, ref Enemy? primaryTarget, float range) + { + var AOEStrat = strategy.Option(SharedTrack.AOE).As(); + if (AOEStrat is AOEStrategy.Automatic) + { + if (Player.DistanceToHitbox(primaryTarget?.Actor) > range) + { + var newTarget = Hints.PriorityTargets.FirstOrDefault(x => Player.DistanceToHitbox(x.Actor) <= range); + if (newTarget != null) + primaryTarget = newTarget; + } + } + } + + /// + /// This function attempts to pick the best target automatically. + /// + /// The user's picked Strategy + /// The user's current picked Target. + /// + /// + /// + protected (Enemy? Best, int Targets) GetBestTarget(Enemy? primaryTarget, float range, PositionCheck isInAOE) => GetTarget(primaryTarget, range, isInAOE, (numTargets, _) => numTargets, a => a); + + /// + /// This function picks the target based on HP, modified by how many targets are in the AOE. + /// + /// + /// The user's current picked Target. + /// + /// + /// + protected (Enemy? Best, int Targets) GetTargetByHP(Enemy? primaryTarget, float range, PositionCheck isInAOE) => GetTarget(primaryTarget, range, isInAOE, (numTargets, enemy) => (numTargets, numTargets > 2 ? enemy.HPMP.CurHP : 0), args => args.numTargets); + + /// + /// Main function for picking a target, generalized for any prioritization and simplification logic. + /// + /// + /// + /// The user's current picked Target. + /// + /// + /// + /// + /// + protected (Enemy? Best, int Priority) GetTarget

(Enemy? primaryTarget, float range, PositionCheck isInAOE, PriorityFunc

prioritize, Func simplify) where P : struct, IComparable + { + P targetPrio(Actor potentialTarget) + { + var numTargets = Hints.NumPriorityTargetsInAOE(enemy => isInAOE(potentialTarget, enemy.Actor)); + return prioritize(numTargets, potentialTarget); // Prioritize based on number of targets in AOE and the potential target + } + + var (newtarget, newprio) = FindBetterTargetBy(primaryTarget?.Actor, range, targetPrio); + var newnewprio = simplify(newprio); + return (newnewprio > 0 ? Hints.FindEnemy(newtarget) : null, newnewprio); + } + + ///

+ /// Identify an appropriate target for applying DoT effect. This has no impact if any auto-targeting is disabled. + /// + /// + /// + /// + /// + /// + /// + protected (Enemy? Target, P Timer) GetDOTTarget

(StrategyValues strategy, Enemy? initial, Func getTimer, int maxAllowedTargets) where P : struct, IComparable + { + var AOEStrat = strategy.Option(SharedTrack.AOE).As(); + switch (AOEStrat) + { + case AOEStrategy.ForceST: + case AOEStrategy.ForceAOE: + case AOEStrategy.Automatic: + return (initial, getTimer(initial?.Actor)); + } + + var newTarget = initial; + var initialTimer = getTimer(initial?.Actor); + var newTimer = initialTimer; + var numTargets = 0; + foreach (var dotTarget in Hints.PriorityTargets) + { + if (dotTarget.ForbidDOTs) + continue; + + if (++numTargets > maxAllowedTargets) + return (null, getTimer(null)); + + var thisTimer = getTimer(dotTarget.Actor); + if (thisTimer.CompareTo(newTimer) < 0) + { + newTarget = dotTarget; + newTimer = thisTimer; + } + } + + return (newTarget, newTimer); + } + #endregion + + #region Actors + ///

+ /// Player's "actual" target; guaranteed to be an enemy. + /// + protected Enemy? PlayerTarget { get; private set; } + + //TODO: implement this soon + protected Actor? AnyTarget { get; private set; } + #endregion + + #region Positionals + /// + /// Retrieves the current positional of the target based on target's position and rotation. + /// + /// The user's specified Target being checked. + /// + protected Positional GetCurrentPositional(Actor target) => (Player.Position - target.Position).Normalized().Dot(target.Rotation.ToDirection()) switch //Check current positional based on target + { + < -0.7071068f => Positional.Rear, //value for Rear positional + < 0.7071068f => Positional.Flank, //value for Flank positional + _ => Positional.Front //default to Front positional if not on Rear or Flank + }; + + /// + /// Checks if player is on specified target's Rear Positional. + /// + /// The user's specified Target being checked. + /// + protected bool IsOnRear(Actor target) => GetCurrentPositional(target) == Positional.Rear; + + /// + /// Checks if player is on specified target's Flank Positional. + /// + /// The user's specified Target being checked. + /// + protected bool IsOnFlank(Actor target) => GetCurrentPositional(target) == Positional.Flank; + #endregion + + #region AI + /// + /// Establishes a goal-zone for a single target within a specified range.
+ /// Primarily utilized by caster-DPS jobs that lack a dedicated maximize-AOE function. + ///
+ /// The range within which the goal zone is applied. + protected void GoalZoneSingle(float range) + { + if (PlayerTarget != null) + Hints.GoalZones.Add(Hints.GoalSingleTarget(PlayerTarget.Actor, range)); + } + + /// + /// Defines a goal-zone using a combined strategy, factoring in AOE considerations. + /// + /// The strategy values that influence the goal zone logic. + /// The base range for the goal zone. + /// A function determining the area of effect. + /// The first available AOE action. + /// The minimum number of targets required to trigger AOE. + /// The positional requirement for the goal zone (default: any). + /// An optional parameter specifying the maximum action range. + protected void GoalZoneCombined(StrategyValues strategy, float range, Func fAoe, AID firstUnlockedAoeAction, int minAoe, Positional positional = Positional.Any, float? maximumActionRange = null) + { + if (!strategy.ForceAOE() && !Unlocked(firstUnlockedAoeAction)) + minAoe = 50; + + if (PlayerTarget == null) + { + if (minAoe < 50) + Hints.GoalZones.Add(fAoe); + } + else + { + Hints.GoalZones.Add(Hints.GoalCombined(Hints.GoalSingleTarget(PlayerTarget.Actor, positional, range), fAoe, minAoe)); + if (maximumActionRange is float r) + Hints.GoalZones.Add(Hints.GoalSingleTarget(PlayerTarget.Actor, r, 0.5f)); + } + } + #endregion + + #region Misc + /// Estimates the delay caused by animation lock. + /// - A value representing the current Animation Lock Delay + protected float AnimationLockDelay { get; private set; } + + /// Estimates the time remaining until the next Down-time phase. + protected float DowntimeIn { get; private set; } + + /// Elapsed time in seconds since the start of combat. + protected float CombatTimer { get; private set; } + + /// Time remaining on pre-pull (or any) Countdown Timer. + protected float? CountdownRemaining { get; private set; } + + /// Checks if player is currently moving. + protected bool IsMoving { get; private set; } + #endregion + + #region Shared Abilities + /// + /// Checks if player is able to execute the melee-DPS shared ability: True North + /// + protected bool CanTrueNorth { get; private set; } + + /// + /// Checks if player is under the effect of the melee-DPS shared ability: True North + /// + protected bool HasTrueNorth { get; private set; } + + /// + /// Checks if player is able to execute the caster-DPS shared ability: Swiftcast + /// + protected bool CanSwiftcast { get; private set; } + + /// + /// Checks if player is under the effect of the caster-DPS shared ability: Swiftcast + /// + protected bool HasSwiftcast { get; private set; } + + /// + /// Checks if player is able to execute the ranged-DPS shared ability: Peloton + /// + protected bool CanPeloton { get; private set; } + + /// + /// Checks if player is under the effect of the ranged-DPS shared ability: Peloton + /// + protected bool HasPeloton { get; private set; } + #endregion + + public sealed override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + { + NextGCD = default; + NextGCDPrio = 0; + HP = Player.HPMP.CurHP; + MP = Player.HPMP.CurMP; + Shield = Player.HPMP.Shield; + PlayerTarget = Hints.FindEnemy(primaryTarget); + AnimationLockDelay = estimatedAnimLockDelay; + IsMoving = isMoving; + DowntimeIn = Manager.Planner?.EstimateTimeToNextDowntime().Item2 ?? float.MaxValue; + CombatTimer = (float)(World.CurrentTime - Manager.CombatStart).TotalSeconds; + CountdownRemaining = World.Client.CountdownRemaining; + CanTrueNorth = ActionUnlocked(ActionID.MakeSpell(ClassShared.AID.TrueNorth)) && World.Client.Cooldowns[ActionDefinitions.Instance.Spell(ClassShared.AID.TrueNorth)!.MainCooldownGroup].Remaining < 45.6f; + HasTrueNorth = StatusRemaining(Player, ClassShared.SID.TrueNorth, 15) > 0.1f; + CanSwiftcast = ActionUnlocked(ActionID.MakeSpell(ClassShared.AID.Swiftcast)) && World.Client.Cooldowns[ActionDefinitions.Instance.Spell(ClassShared.AID.Swiftcast)!.MainCooldownGroup].Remaining < 0.6f; + HasSwiftcast = StatusRemaining(Player, ClassShared.SID.Swiftcast, 10) > 0.1f; + CanPeloton = !Player.InCombat && ActionUnlocked(ActionID.MakeSpell(ClassShared.AID.Peloton)) && World.Client.Cooldowns[ActionDefinitions.Instance.Spell(ClassShared.AID.Peloton)!.MainCooldownGroup].Remaining < 0.6f; + HasPeloton = PlayerHasAnyEffect(BRD.SID.Peloton); + + if (Player.MountId is not (103 or 117 or 128)) + Execution(strategy, PlayerTarget); + } + + /// + /// The core function responsible for orchestrating the execution of all abilities and strategies.
+ ///
+ /// The user's specified Strategy. + /// The user's specified Enemy. + /// - Primary execution of user's rotation module. + public abstract void Execution(StrategyValues strategy, Enemy? primaryTarget); +} + +static class ModuleExtensions +{ + /// Defines our shared AOE (rotation) and Hold strategies. + /// The definitions of our base module's strategies. + /// - Options for shared custom strategies to be used via AutoRotation or Cooldown Planner + public static RotationModuleDefinition DefineShared(this RotationModuleDefinition res) + { + res.Define(SharedTrack.AOE).As("AOE", uiPriority: 300) + .AddOption(AOEStrategy.Automatic, "Auto", "Automatically execute optimal rotation based on targets", supportedTargets: ActionTargets.Hostile) + .AddOption(AOEStrategy.ForceST, "ForceST", "Force-execute Single Target", supportedTargets: ActionTargets.Hostile) + .AddOption(AOEStrategy.ForceAOE, "ForceAOE", "Force-execute AOE rotation", supportedTargets: ActionTargets.Hostile); + + res.Define(SharedTrack.Hold).As("Hold", uiPriority: 290) + .AddOption(HoldStrategy.DontHold, "DontHold", "Allow use of all cooldowns, buffs, or gauge abilities") + .AddOption(HoldStrategy.HoldCooldowns, "Hold", "Forbid use of all cooldowns only") + .AddOption(HoldStrategy.HoldGauge, "HoldGauge", "Forbid use of all gauge abilities only") + .AddOption(HoldStrategy.HoldBuffs, "HoldBuffs", "Forbid use of all raidbuffs or buff-related abilities only") + .AddOption(HoldStrategy.HoldEverything, "HoldEverything", "Forbid use of all cooldowns, buffs, and gauge abilities"); + return res; + } + + /// A quick and easy helper for shortcutting how we define our GCD abilities. + /// The Track for the ability that the user is specifying; ability tracked must be inside the module's Track enum for target selection. + /// The Internal Name for the ability that the user is specifying; we usually want to put the full name of the ability here, as this will show up as the main name representing this option. (e.g. "No Mercy") + /// The Display Name for the ability that the user is specifying; we usually want to put some sort of abbreviation here, as this will show up as the secondary name representing this option. (e.g. "NM" or "N.Mercy") + /// The priority for specified ability inside the UI. (e.g. Higher the number = More to the left (in CDPlanner) or top (in Autorotation) menus) + /// The Cooldown for the ability that the user is specifying; 0 if none.NOTE: For charge abilities, this will check for its Charge CD, not its Total CD. + /// The Effect Duration for the ability that the user is specifying; 0 if none. + /// The Targets Supported for the ability that the user is specifying. + /// The Minimum Level required for the ability that the user is specifying. + /// The Maximum Level required for the ability that the user is specifying. + /// - Basic GCD options for any specified ability to be used via AutoRotation or Cooldown Planner + public static RotationModuleDefinition.ConfigRef DefineGCD(this RotationModuleDefinition res, Index track, string internalName, string displayName = "", int uiPriority = 100, float cooldown = 0, float effectDuration = 0, ActionTargets supportedTargets = ActionTargets.None, int minLevel = 1, int maxLevel = 100) where Index : Enum + { + return res.Define(track).As(internalName, displayName: displayName, uiPriority: uiPriority) + .AddOption(GCDStrategy.Automatic, "Auto", "Automatically uses when optimal", cooldown, effectDuration, supportedTargets, minLevel: minLevel, maxLevel) + .AddOption(GCDStrategy.Force, "Force", "Force use ASAP", cooldown, effectDuration, supportedTargets, minLevel: minLevel, maxLevel) + .AddOption(GCDStrategy.Delay, "Delay", "Do not use", 0, 0, ActionTargets.None, minLevel: minLevel, maxLevel); + } + + /// A quick and easy helper for shortcutting how we define our OGCD abilities. + /// The Track for the ability that the user is specifying; ability tracked must be inside the module's Track enum for target selection. + /// The Internal Name for the ability that the user is specifying; we usually want to put the full name of the ability here, as this will show up as the main name representing this option. (e.g. "No Mercy") + /// The Display Name for the ability that the user is specifying; we usually want to put some sort of abbreviation here, as this will show up as the secondary name representing this option. (e.g. "NM" or "N.Mercy") + /// The priority for specified ability inside the UI. (e.g. Higher the number = More to the left (in CDPlanner) or top (in Autorotation) menus) + /// The Cooldown for the ability that the user is specifying; 0 if none.NOTE: For charge abilities, this will check for its Charge CD, not its Total CD. + /// The Effect Duration for the ability that the user is specifying; 0 if none. + /// The Targets Supported for the ability that the user is specifying. + /// The Minimum Level required for the ability that the user is specifying. + /// The Maximum Level required for the ability that the user is specifying. + /// - Basic OGCD options for any specified ability to be used via AutoRotation or Cooldown Planner + public static RotationModuleDefinition.ConfigRef DefineOGCD(this RotationModuleDefinition res, Index track, string internalName, string displayName = "", int uiPriority = 100, float cooldown = 0, float effectDuration = 0, ActionTargets supportedTargets = ActionTargets.None, int minLevel = 1, int maxLevel = 100) where Index : Enum + { + return res.Define(track).As(internalName, displayName: displayName, uiPriority: uiPriority) + .AddOption(OGCDStrategy.Automatic, "Auto", "Automatically uses when optimal", cooldown, effectDuration, supportedTargets, minLevel: minLevel, maxLevel) + .AddOption(OGCDStrategy.Force, "Force", "Force use ASAP", cooldown, effectDuration, supportedTargets, minLevel: minLevel, maxLevel) + .AddOption(OGCDStrategy.AnyWeave, "AnyWeave", "Force use in next possible weave slot", cooldown, effectDuration, supportedTargets, minLevel: minLevel, maxLevel) + .AddOption(OGCDStrategy.EarlyWeave, "EarlyWeave", "Force use in next possible early-weave slot", cooldown, effectDuration, supportedTargets, minLevel: minLevel, maxLevel) + .AddOption(OGCDStrategy.LateWeave, "LateWeave", "Force use in next possible late-weave slot", cooldown, effectDuration, supportedTargets, minLevel: minLevel, maxLevel) + .AddOption(OGCDStrategy.Delay, "Delay", "Do not use", 0, 0, ActionTargets.None, minLevel: minLevel, maxLevel); + } + + #region Global Helpers + /// A global helper for automatically executing the best optimal rotation. See for more details. + /// - TRUE if is set to + /// - FALSE if set to any other option + public static bool Automatic(this StrategyValues strategy) => strategy.Option(SharedTrack.AOE).As() is AOEStrategy.Automatic; + + /// A global helper for force-executing the single-target rotation. See for more details. + /// - TRUE if is set to + /// - FALSE if set to any other option + public static bool ForceST(this StrategyValues strategy) => strategy.Option(SharedTrack.AOE).As() is AOEStrategy.ForceST; + + /// A global helper for force-executing the AOE rotation. See for more details. + /// - TRUE if is set to + /// - FALSE if set to any other option + public static bool ForceAOE(this StrategyValues strategy) => strategy.Option(SharedTrack.AOE).As() == AOEStrategy.ForceAOE; + + /// A global helper for forbidding ALL available abilities that are buff, gauge, or cooldown related. See for more details. + /// - TRUE if is set to + /// - FALSE if set to any other option + public static bool HoldAll(this StrategyValues strategy) => strategy.Option(SharedTrack.Hold).As() == HoldStrategy.HoldEverything; + + /// A global helper for forbidding ALL available abilities that are related to raidbuffs. See for more details. + /// - TRUE if is set to + /// - FALSE if set to any other option + public static bool HoldBuffs(this StrategyValues strategy) => strategy.Option(SharedTrack.Hold).As() == HoldStrategy.HoldBuffs; + + /// A global helper for forbidding ALL available abilities that have any sort of cooldown attached to it. See for more details. + /// - TRUE if is set to + /// - FALSE if set to any other option + public static bool HoldCDs(this StrategyValues strategy) => strategy.Option(SharedTrack.Hold).As() == HoldStrategy.HoldCooldowns; + + /// A global helper for forbidding ALL available abilities that are related to the job's gauge. See for more details. + /// - TRUE if is set to + /// - FALSE if set to any other option + public static bool HoldGauge(this StrategyValues strategy) => strategy.Option(SharedTrack.Hold).As() == HoldStrategy.HoldGauge; + + /// A global helper for allowing ALL available abilities that are buff, gauge, or cooldown related. This is the default option for this strategy. See for more details. + /// - TRUE if is set to + /// - FALSE if set to any other option + public static bool DontHold(this StrategyValues strategy) => strategy.Option(SharedTrack.Hold).As() == HoldStrategy.DontHold; + #endregion +} diff --git a/BossMod/Autorotation/Standard/akechi/Tank/AkechiDRK.cs b/BossMod/Autorotation/Standard/akechi/Tank/AkechiDRK.cs new file mode 100644 index 0000000000..56321e165b --- /dev/null +++ b/BossMod/Autorotation/Standard/akechi/Tank/AkechiDRK.cs @@ -0,0 +1,642 @@ +using static BossMod.AIHints; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using BossMod.DRK; + +namespace BossMod.Autorotation.akechi; +//Contribution by Akechi +//Discord: @akechdz or 'Akechi' on Puni.sh for maintenance + +public sealed class AkechiDRK(RotationModuleManager manager, Actor player) : AkechiTools(manager, player) +{ + #region Enums: Abilities / Strategies + public enum Track { Blood = SharedTrack.Count, MP, Carve, DeliriumCombo, Potion, Unmend, Delirium, SaltedEarth, SaltAndDarkness, LivingShadow, Shadowbringer, Disesteem } + public enum BloodStrategy { Automatic, OnlyBloodspiller, OnlyQuietus, ForceBloodspiller, ForceQuietus, Conserve } + public enum MPStrategy { Optimal, Auto3k, Auto6k, Auto9k, AutoRefresh, Edge3k, Edge6k, Edge9k, EdgeRefresh, Flood3k, Flood6k, Flood9k, FloodRefresh, Delay } + public enum CarveStrategy { Automatic, OnlyCarve, OnlyDrain, ForceCarve, ForceDrain, Delay } + public enum DeliriumComboStrategy { Automatic, ScarletDelirum, Comeuppance, Torcleaver, Impalement, Delay } + public enum PotionStrategy { Manual, AlignWithRaidBuffs, Immediate } + public enum UnmendStrategy { OpenerFar, OpenerForce, Force, Allow, Forbid } + #endregion + + #region Module Definitions & Strategies + public static RotationModuleDefinition Definition() + { + var res = new RotationModuleDefinition("Akechi DRK", "Standard Rotation Module", "Standard rotation (Akechi)|Tank", "Akechi", RotationModuleQuality.Ok, BitMask.Build((int)Class.DRK), 100); + + res.DefineShared(); + res.Define(Track.Blood).As("Blood", "Blood", uiPriority: 200) + .AddOption(BloodStrategy.Automatic, "Automatic", "Automatically use Blood-related abilities optimally") + .AddOption(BloodStrategy.OnlyBloodspiller, "Only Bloodspiller", "Uses Bloodspiller optimally as Blood spender only, regardless of targets", 0, 0, ActionTargets.Hostile, 62) + .AddOption(BloodStrategy.OnlyQuietus, "Only Quietus", "Uses Quietus optimally as Blood spender only, regardless of targets", 0, 0, ActionTargets.Hostile, 64) + .AddOption(BloodStrategy.ForceBloodspiller, "Force Bloodspiller", "Force use Bloodspiller ASAP", 0, 0, ActionTargets.Hostile, 62) + .AddOption(BloodStrategy.ForceQuietus, "Force Quietus", "Force use Quietus ASAP", 0, 0, ActionTargets.Hostile, 64) + .AddOption(BloodStrategy.Conserve, "Conserve", "Conserves all Blood-related abilities as much as possible"); + res.Define(Track.MP).As("MP", "MP", uiPriority: 190) + .AddOption(MPStrategy.Optimal, "Optimal", "Use MP actions optimally; 2 for 1 minute, 4 (or 5 if Dark Arts is active) for 2 minutes") + .AddOption(MPStrategy.Auto3k, "Auto 3k", "Automatically decide best MP action to use; Uses when at 3000+ MP", 0, 0, ActionTargets.Self, 30) + .AddOption(MPStrategy.Auto6k, "Auto 6k", "Automatically decide best MP action to use; Uses when at 6000+ MP", 0, 0, ActionTargets.Self, 30) + .AddOption(MPStrategy.Auto9k, "Auto 9k", "Automatically decide best MP action to use; Uses when at 9000+ MP", 0, 0, ActionTargets.Self, 30) + .AddOption(MPStrategy.AutoRefresh, "Auto Refresh", "Automatically decide best MP action to use as Darkside refresher only", 0, 0, ActionTargets.Self, 30) + .AddOption(MPStrategy.Edge3k, "Edge 3k", "Use Edge of Shadow as Darkside refresher & MP spender; Uses when at 3000+ MP", 0, 0, ActionTargets.Self, 40) + .AddOption(MPStrategy.Edge6k, "Edge 6k", "Use Edge of Shadow as Darkside refresher & MP spender; Uses when at 6000+ MP", 0, 0, ActionTargets.Self, 40) + .AddOption(MPStrategy.Edge9k, "Edge 9k", "Use Edge of Shadow as Darkside refresher & MP spender; Uses when at 9000+ MP", 0, 0, ActionTargets.Self, 40) + .AddOption(MPStrategy.EdgeRefresh, "Edge Refresh", "Use Edge abilities as Darkside refresher only", 0, 0, ActionTargets.Self, 40) + .AddOption(MPStrategy.Flood3k, "Flood 3k", "Use Flood of Shadow as Darkside refresher & MP spender; Uses when at 3000+ MP", 0, 0, ActionTargets.Self, 30) + .AddOption(MPStrategy.Flood6k, "Flood 6k", "Use Flood of Shadow as Darkside refresher & MP spender; Uses when at 6000+ MP", 0, 0, ActionTargets.Self, 30) + .AddOption(MPStrategy.Flood9k, "Flood 9k", "Use Flood of Shadow as Darkside refresher & MP spender; Uses when at 9000+ MP", 0, 0, ActionTargets.Self, 30) + .AddOption(MPStrategy.FloodRefresh, "Flood Refresh", "Use Flood abilities as Darkside refresher only", 0, 0, ActionTargets.Self, 30) + .AddOption(MPStrategy.Delay, "Delay", "Delay the use of MP actions for strategic reasons", 0, 0, ActionTargets.None, 30) + .AddAssociatedActions(AID.EdgeOfDarkness, AID.EdgeOfShadow, AID.FloodOfDarkness, AID.FloodOfShadow); + res.Define(Track.Carve).As("Carve & Spit / Abyssal Drain", "Carve", uiPriority: 160) + .AddOption(CarveStrategy.Automatic, "Auto", "Automatically decide when to use either Carve and Spit or Abyssal Drain") + .AddOption(CarveStrategy.OnlyCarve, "Only Carve and Spit", "Automatically use Carve and Spit as optimal spender, regardless of targets", 0, 0, ActionTargets.Hostile, 60) + .AddOption(CarveStrategy.OnlyDrain, "Only Abysssal Drain", "Automatically use Abyssal Drain as optimal spender, regardless of targets", 0, 0, ActionTargets.Hostile, 56) + .AddOption(CarveStrategy.ForceCarve, "Force Carve and Spit", "Force use Carve and Spit ASAP", 60, 0, ActionTargets.Hostile, 60) + .AddOption(CarveStrategy.ForceDrain, "Force Abyssal Drain", "Force use Abyssal Drain ASAP", 60, 0, ActionTargets.Hostile, 56) + .AddOption(CarveStrategy.Delay, "Delay", "Delay the use of Carve and Spit for strategic reasons", 0, 0, ActionTargets.None, 56) + .AddAssociatedActions(AID.CarveAndSpit); + res.Define(Track.DeliriumCombo).As("Delirium Combo", "Scarlet", uiPriority: 180) + .AddOption(DeliriumComboStrategy.Automatic, "Auto", "Automatically decide when to use Delirium Combo", 0, 0, ActionTargets.Hostile, 96) + .AddOption(DeliriumComboStrategy.ScarletDelirum, "Scarlet Delirium", "Force use Scarlet Delirium ASAP", 0, 0, ActionTargets.Hostile, 96) + .AddOption(DeliriumComboStrategy.Comeuppance, "Comeuppance", "Force use Comeuppance ASAP", 0, 0, ActionTargets.Hostile, 96) + .AddOption(DeliriumComboStrategy.Torcleaver, "Torcleaver", "Force use Torcleaver ASAP", 0, 0, ActionTargets.Hostile, 96) + .AddOption(DeliriumComboStrategy.Impalement, "Impalement", "Force use Impalement ASAP", 0, 0, ActionTargets.Hostile, 96) + .AddOption(DeliriumComboStrategy.Delay, "Delay", "Delay use of Scarlet combo for strategic reasons", 0, 0, ActionTargets.Hostile, 96) + .AddAssociatedActions(AID.ScarletDelirium, AID.Comeuppance, AID.Torcleaver, AID.Impalement); + res.Define(Track.Potion).As("Potion", uiPriority: 20) + .AddOption(PotionStrategy.Manual, "Manual", "Do not use automatically") + .AddOption(PotionStrategy.AlignWithRaidBuffs, "AlignWithRaidBuffs", "Align with Living Shadow (to ensure use on 2-minute windows)", 270, 30, ActionTargets.Self) + .AddOption(PotionStrategy.Immediate, "Immediate", "Use ASAP, regardless of any buffs", 270, 30, ActionTargets.Self) + .AddAssociatedAction(ActionDefinitions.IDPotionStr); + res.Define(Track.Unmend).As("Ranged", "Ranged", uiPriority: 30) + .AddOption(UnmendStrategy.OpenerFar, "Far (Opener)", "Use Unmend in pre-pull & out of melee range", supportedTargets: ActionTargets.Hostile) + .AddOption(UnmendStrategy.OpenerForce, "Force (Opener)", "Force use Unmend in pre-pull in any range", supportedTargets: ActionTargets.Hostile) + .AddOption(UnmendStrategy.Force, "Force", "Force use Unmend in any range", supportedTargets: ActionTargets.Hostile) + .AddOption(UnmendStrategy.Allow, "Allow", "Allow use of Unmend when out of melee range", supportedTargets: ActionTargets.Hostile) + .AddOption(UnmendStrategy.Forbid, "Forbid", "Prohibit use of Unmend") + .AddAssociatedActions(AID.Unmend); + res.DefineOGCD(Track.Delirium, "Delirium", "Deli.", uiPriority: 170, 60, 15, ActionTargets.Self, 35).AddAssociatedActions(AID.Delirium); + res.DefineOGCD(Track.SaltedEarth, "Salted Earth", "S.Earth", uiPriority: 140, 90, 15, ActionTargets.Self, 52).AddAssociatedActions(AID.SaltedEarth); + res.DefineOGCD(Track.SaltAndDarkness, "Salt & Darkness", "Salt & D.", uiPriority: 135, 20, 0, ActionTargets.Self, 86).AddAssociatedActions(AID.SaltAndDarkness); + res.DefineOGCD(Track.LivingShadow, "Living Shadow", "L.Shadow", uiPriority: 175, 100, 20, ActionTargets.Self, 80).AddAssociatedActions(AID.LivingShadow); + res.DefineOGCD(Track.Shadowbringer, "Shadowbringer", "S.bringer", uiPriority: 165, 60, 0, ActionTargets.Hostile, 90).AddAssociatedActions(AID.Shadowbringer); + res.DefineGCD(Track.Disesteem, "Disesteem", "D.esteem", uiPriority: 150, supportedTargets: ActionTargets.Hostile, minLevel: 100).AddAssociatedActions(AID.Disesteem); + + return res; + } + #endregion + + #region Priorities + public enum GCDPriority + { + None = 0, + Standard = 100, + Blood = 300, + Disesteem = 500, + DeliriumCombo = 600, + NeedBlood = 700, + Opener = 800, + ForcedGCD = 900, + } + public enum OGCDPriority + { + None = 0, + Standard = 100, + MP = 300, + CarveOrDrain = 400, + Shadowbringer = 450, + SaltedEarth = 500, + Delirium = 550, + LivingShadow = 600, + NeedRefresh = 650, + ForcedOGCD = 1100, //Enough to put it past CDPlanner's "Automatic" priority, which is really only Medium priority + } + #endregion + + #region Upgrade Paths + private AID BestEdge => Unlocked(AID.EdgeOfShadow) ? AID.EdgeOfShadow : Unlocked(AID.EdgeOfDarkness) ? AID.EdgeOfDarkness : AID.FloodOfDarkness; + private AID BestFlood => !Unlocked(AID.FloodOfShadow) ? AID.FloodOfDarkness : AID.FloodOfShadow; + private AID BestQuietus => Unlocked(AID.Quietus) ? AID.Quietus : AID.Bloodspiller; + private AID BestBloodSpender => ShouldUseAOE ? BestQuietus : AID.Bloodspiller; + private AID BestDelirium => Unlocked(AID.Delirium) ? AID.Delirium : AID.BloodWeapon; + private AID CarveOrDrain => ShouldUseAOE ? AID.AbyssalDrain : BestCarve; + private AID BestCarve => Unlocked(AID.CarveAndSpit) ? AID.CarveAndSpit : AID.AbyssalDrain; + private SID BestBloodWeapon => Unlocked(AID.ScarletDelirium) ? SID.EnhancedDelirium : Unlocked(AID.Delirium) ? SID.Delirium : SID.BloodWeapon; + private AID DeliriumCombo => Delirium.Step is 2 ? AID.Torcleaver : Delirium.Step is 1 ? AID.Comeuppance : ShouldUseAOE ? AID.Impalement : AID.ScarletDelirium; + #endregion + + #region Module Variables + public byte Blood; + public (byte State, bool IsActive) DarkArts; + public (float Timer, bool IsActive, bool NeedsRefresh) Darkside; + public bool RiskingBlood; + public bool RiskingMP; + public (float Left, float CD, bool IsActive, bool IsReady) SaltedEarth; + public (float CD, bool IsReady) AbyssalDrain; + public (float CD, bool IsReady) CarveAndSpit; + public (ushort Step, float Left, int Stacks, float CD, bool IsActive, bool IsReady) Delirium; + public (float Timer, float CD, bool IsActive, bool IsReady) LivingShadow; + public (float TotalCD, float ChargeCD, bool HasCharges, bool IsReady) Shadowbringer; + public (float Left, bool IsActive, bool IsReady) Disesteem; + private bool ShouldUseAOE; + public int NumAOERectTargets; + public int NumMPRectTargets; + public Enemy? BestAOERectTargets; + public Enemy? BestMPRectTargets; + public Enemy? BestTargetAOERect; + public Enemy? BestTargetMPRectHigh; + public Enemy? BestTargetMPRectLow; + private bool inOdd; + #endregion + + public override void Execution(StrategyValues strategy, Enemy? primaryTarget) + { + #region Variables + + #region Gauge + var gauge = World.Client.GetGauge(); //Retrieve DRK gauge + Blood = gauge.Blood; + DarkArts.State = gauge.DarkArtsState; //Retrieve current Dark Arts state + DarkArts.IsActive = DarkArts.State > 0; //Checks if Dark Arts is active + Darkside.Timer = gauge.DarksideTimer / 1000f; //Retrieve current Darkside timer + Darkside.IsActive = Darkside.Timer > 0.1f; //Checks if Darkside is active + Darkside.NeedsRefresh = Darkside.Timer <= 3; //Checks if Darkside needs to be refreshed + RiskingBlood = ComboLastMove is AID.SyphonStrike or AID.Unleash && Blood >= 80 || Delirium.CD <= 3 && Blood >= 70; //Checks if we are risking Blood + RiskingMP = MP >= 10000 || Darkside.NeedsRefresh; + //var ShouldUseDA = DarkArts.IsActive && (RiskingMP || (Delirium.CD <= (Darkside.Timer + GCD) && Delirium.IsActive)); + #endregion + + #region Cooldowns + SaltedEarth.Left = StatusRemaining(Player, SID.SaltedEarth, 15); //Retrieve current Salted Earth time left + SaltedEarth.CD = TotalCD(AID.SaltedEarth); //Retrieve current Salted Earth cooldown + SaltedEarth.IsActive = SaltedEarth.Left > 0.1f; //Checks if Salted Earth is active + SaltedEarth.IsReady = Unlocked(AID.SaltedEarth) && SaltedEarth.CD < 0.6f; //Salted Earth ability + AbyssalDrain.CD = TotalCD(AID.AbyssalDrain); //Retrieve current Abyssal Drain cooldown + AbyssalDrain.IsReady = Unlocked(AID.AbyssalDrain) && AbyssalDrain.CD < 0.6f; //Abyssal Drain ability + CarveAndSpit.CD = TotalCD(AID.CarveAndSpit); //Retrieve current Carve and Spit cooldown + CarveAndSpit.IsReady = Unlocked(AID.CarveAndSpit) && CarveAndSpit.CD < 0.6f; //Carve and Spit ability + Disesteem.Left = StatusRemaining(Player, SID.Scorn, 30); //Retrieve current Disesteem time left + Disesteem.IsActive = Disesteem.Left > 0.1f; //Checks if Disesteem is active + Disesteem.IsReady = Unlocked(AID.Disesteem) && Disesteem.Left > 0.1f; //Disesteem ability + Delirium.Step = gauge.DeliriumStep; //Retrieve current Delirium combo step + Delirium.Left = StatusRemaining(Player, BestBloodWeapon, 15); //Retrieve current Delirium time left + Delirium.Stacks = StacksRemaining(Player, BestBloodWeapon, 15); //Retrieve current Delirium stacks + Delirium.CD = TotalCD(BestDelirium); //Retrieve current Delirium cooldown + Delirium.IsActive = Delirium.Left > 0.1f; //Checks if Delirium is active + Delirium.IsReady = Unlocked(BestDelirium) && Delirium.CD < 0.6f; //Delirium ability + LivingShadow.Timer = gauge.ShadowTimer / 1000f; //Retrieve current Living Shadow timer + LivingShadow.CD = TotalCD(AID.LivingShadow); //Retrieve current Living Shadow cooldown + LivingShadow.IsActive = LivingShadow.Timer > 0; //Checks if Living Shadow is active + LivingShadow.IsReady = Unlocked(AID.LivingShadow) && LivingShadow.CD < 0.6f; //Living Shadow ability + Shadowbringer.TotalCD = TotalCD(AID.Shadowbringer); //Retrieve current Shadowbringer cooldown + Shadowbringer.ChargeCD = ChargeCD(AID.Shadowbringer); //Retrieve current Shadowbringer charge cooldown + Shadowbringer.HasCharges = TotalCD(AID.Shadowbringer) <= 60; //Checks if Shadowbringer has charges + Shadowbringer.IsReady = Unlocked(AID.Shadowbringer) && Shadowbringer.HasCharges; //Shadowbringer ability + #endregion + + #region Strategy Definitions + var mp = strategy.Option(Track.MP); + var mpStrat = mp.As(); //Retrieve MP strategy + var blood = strategy.Option(Track.Blood); + var bloodStrat = blood.As(); //Retrieve Blood strategy + var se = strategy.Option(Track.SaltedEarth); + var seStrat = se.As(); //Retrieve Salted Earth strategy + var cd = strategy.Option(Track.Carve); + var cdStrat = cd.As(); //Retrieve Carve and Drain strategy + var deli = strategy.Option(Track.Delirium); + var deliStrat = deli.As(); //Retrieve Delirium strategy + var ls = strategy.Option(Track.LivingShadow); + var lsStrat = ls.As(); //Retrieve Living Shadow strategy + var sb = strategy.Option(Track.Shadowbringer); + var sbStrat = sb.As(); //Retrieve Shadowbringer strategy + var dcombo = strategy.Option(Track.DeliriumCombo); + var dcomboStrat = dcombo.As(); //Retrieve Delirium combo strategy + var de = strategy.Option(Track.Disesteem); + var deStrat = de.As(); //Retrieve Disesteem strategy + var unmend = strategy.Option(Track.Unmend); + var unmendStrat = unmend.As(); //Retrieve Unmend strategy + #endregion + + #region Misc + ShouldUseAOE = ShouldUseAOECircle(5).OnThreeOrMore; + (BestAOERectTargets, NumAOERectTargets) = GetBestTarget(PlayerTarget, 10, (primary, other) => Hints.TargetInAOERect(other, Player.Position, Player.DirectionTo(primary), 10, 3.5f)); + BestTargetAOERect = Unlocked(AID.Shadowbringer) && NumAOERectTargets >= 2 + ? BestAOERectTargets + : primaryTarget; + BestTargetMPRectHigh = Unlocked(AID.FloodOfShadow) && NumAOERectTargets >= 3 + ? BestAOERectTargets + : primaryTarget; + BestTargetMPRectLow = !Unlocked(AID.FloodOfShadow) && NumAOERectTargets >= 4 + ? BestAOERectTargets + : primaryTarget; + inOdd = LivingShadow.CD is < 90 and > 30; + #endregion + + #endregion + + #region Full Rotation Execution + + #region Standard Rotations + if (strategy.Automatic()) + { + QueueGCD(BestRotation(), //queue the next single-target combo action only if combo is finished + TargetChoice(strategy.Option(SharedTrack.AOE)) //Get target choice + ?? primaryTarget?.Actor, //if none, pick primary target + GCDPriority.Standard); //with priority for 123/10 combo actions + } + if (strategy.ForceST()) //if Force Single Target option is picked + { + QueueGCD(ST(), + TargetChoice(strategy.Option(SharedTrack.AOE)) //Get target choice + ?? primaryTarget?.Actor, //if none, pick primary target + GCDPriority.Standard); //with priority for 123/10 combo actions + } + if (strategy.ForceAOE()) //if Force AOE option is picked + { + QueueGCD(AOE(), + Player, + GCDPriority.Standard); //with priority for 123/10 combo actions + } + #endregion + + #region Cooldowns + if (!strategy.HoldAll()) //if not holding cooldowns + { + if (!strategy.HoldCDs()) //if holding cooldowns + { + if (!strategy.HoldBuffs()) + { + if (ShouldUseDelirium(deliStrat, primaryTarget)) + QueueOGCD(BestDelirium, + Player, + deliStrat is OGCDStrategy.Force + or OGCDStrategy.AnyWeave + or OGCDStrategy.EarlyWeave + or OGCDStrategy.LateWeave + ? OGCDPriority.ForcedOGCD + : OGCDPriority.Delirium); + if (ShouldUseLivingShadow(lsStrat, primaryTarget)) + QueueOGCD(AID.LivingShadow, + Player, + lsStrat is OGCDStrategy.Force + or OGCDStrategy.AnyWeave + or OGCDStrategy.EarlyWeave + or OGCDStrategy.LateWeave + ? OGCDPriority.ForcedOGCD + : OGCDPriority.LivingShadow); + } + if (ShouldUseSaltedEarth(seStrat, primaryTarget)) + QueueOGCD(AID.SaltedEarth, + Player, + seStrat is OGCDStrategy.Force + or OGCDStrategy.AnyWeave + or OGCDStrategy.EarlyWeave + or OGCDStrategy.LateWeave + ? OGCDPriority.ForcedOGCD + : OGCDPriority.SaltedEarth); + if (ShouldUseCarveOrDrain(cdStrat, primaryTarget)) + { + if (cdStrat is CarveStrategy.Automatic) + QueueOGCD(CarveOrDrain, + TargetChoice(cd) ?? primaryTarget?.Actor, + cdStrat is CarveStrategy.ForceCarve + or CarveStrategy.ForceDrain + ? OGCDPriority.ForcedOGCD + : OGCDPriority.CarveOrDrain); + if (cdStrat is CarveStrategy.OnlyCarve) + QueueOGCD(BestCarve, + TargetChoice(cd) ?? primaryTarget?.Actor, + cdStrat is CarveStrategy.ForceCarve + ? OGCDPriority.ForcedOGCD + : OGCDPriority.CarveOrDrain); + if (cdStrat is CarveStrategy.OnlyDrain) + QueueOGCD(AID.AbyssalDrain, + TargetChoice(cd) ?? primaryTarget?.Actor, + cdStrat is CarveStrategy.ForceDrain + ? OGCDPriority.ForcedOGCD + : OGCDPriority.CarveOrDrain); + } + if (ShouldUseShadowbringer(sbStrat, primaryTarget)) + QueueOGCD(AID.Shadowbringer, + TargetChoice(sb) ?? BestTargetAOERect?.Actor, + sbStrat is OGCDStrategy.Force + or OGCDStrategy.AnyWeave + or OGCDStrategy.EarlyWeave + or OGCDStrategy.LateWeave + ? OGCDPriority.ForcedOGCD + : OGCDPriority.Shadowbringer); + if (ShouldUseDisesteem(deStrat, primaryTarget)) + QueueGCD(AID.Disesteem, + TargetChoice(de) ?? BestTargetAOERect?.Actor, + deStrat is GCDStrategy.Force + ? GCDPriority.ForcedGCD + : CombatTimer < 30 + ? GCDPriority.Opener + : GCDPriority.Disesteem); + } + if (!strategy.HoldGauge()) + { + if (ShouldUseBlood(bloodStrat, primaryTarget)) + { + if (bloodStrat is BloodStrategy.Automatic) + QueueGCD(BestBloodSpender, + TargetChoice(blood) ?? primaryTarget?.Actor, + bloodStrat is BloodStrategy.ForceBloodspiller + or BloodStrategy.ForceQuietus + ? GCDPriority.ForcedGCD + : RiskingBlood + ? GCDPriority.NeedBlood + : GCDPriority.Blood); + if (bloodStrat is BloodStrategy.OnlyBloodspiller) + QueueGCD(AID.Bloodspiller, + TargetChoice(blood) ?? primaryTarget?.Actor, + bloodStrat is BloodStrategy.ForceBloodspiller + ? GCDPriority.ForcedGCD + : RiskingBlood + ? GCDPriority.NeedBlood + : GCDPriority.Blood); + if (bloodStrat is BloodStrategy.OnlyQuietus) + QueueGCD(AID.Quietus, + Unlocked(AID.Quietus) + ? Player + : TargetChoice(blood) ?? primaryTarget?.Actor, + bloodStrat is BloodStrategy.ForceQuietus + ? GCDPriority.ForcedGCD + : RiskingBlood + ? GCDPriority.NeedBlood + : GCDPriority.Blood); + } + } + if (ShouldUseMP(mpStrat)) + { + if (mpStrat is MPStrategy.Optimal + or MPStrategy.Auto9k + or MPStrategy.Auto6k + or MPStrategy.Auto3k + or MPStrategy.AutoRefresh) + { + if (NumAOERectTargets >= 3) + QueueOGCD(BestFlood, + TargetChoice(mp) ?? BestTargetMPRectHigh?.Actor, + RiskingMP + ? OGCDPriority.ForcedOGCD + : OGCDPriority.MP); + if (NumAOERectTargets <= 2) + QueueOGCD(BestEdge, + TargetChoice(mp) ?? primaryTarget?.Actor, + RiskingMP + ? OGCDPriority.ForcedOGCD + : OGCDPriority.MP); + } + if (mpStrat is MPStrategy.Edge9k + or MPStrategy.Edge6k + or MPStrategy.Edge3k + or MPStrategy.EdgeRefresh) + QueueOGCD(BestEdge, + TargetChoice(mp) ?? primaryTarget?.Actor, + RiskingMP + ? OGCDPriority.ForcedOGCD + : OGCDPriority.MP); + if (mpStrat is MPStrategy.Flood9k + or MPStrategy.Flood6k + or MPStrategy.Flood3k + or MPStrategy.FloodRefresh) + QueueOGCD(BestFlood, + TargetChoice(mp) ?? BestTargetMPRectHigh?.Actor, + RiskingMP ? + OGCDPriority.ForcedOGCD + : OGCDPriority.MP); + } + } + if (ShouldUseSaltAndDarkness(strategy.Option(Track.SaltAndDarkness).As(), primaryTarget)) + QueueOGCD(AID.SaltAndDarkness, + Player, + OGCDPriority.SaltedEarth); + if (ShouldUseDeliriumCombo(dcomboStrat, primaryTarget)) + { + if (dcomboStrat is DeliriumComboStrategy.Automatic) + QueueGCD(DeliriumCombo, + TargetChoice(dcombo) ?? primaryTarget?.Actor, + GCDPriority.DeliriumCombo); + if (dcomboStrat is DeliriumComboStrategy.ScarletDelirum) + QueueGCD(AID.ScarletDelirium, + TargetChoice(dcombo) ?? primaryTarget?.Actor, + GCDPriority.ForcedGCD); + if (dcomboStrat is DeliriumComboStrategy.Comeuppance) + QueueGCD(AID.Comeuppance, + TargetChoice(dcombo) ?? primaryTarget?.Actor, + GCDPriority.ForcedGCD); + if (dcomboStrat is DeliriumComboStrategy.Torcleaver) + QueueGCD(AID.Torcleaver, + TargetChoice(dcombo) ?? primaryTarget?.Actor, + GCDPriority.ForcedGCD); + if (dcomboStrat is DeliriumComboStrategy.Impalement) + QueueGCD(AID.Impalement, + Player, + GCDPriority.ForcedGCD); + } + if (ShouldUseUnmend(unmendStrat, primaryTarget)) + QueueGCD(AID.Unmend, + TargetChoice(unmend) ?? primaryTarget?.Actor, + GCDPriority.Standard); + if (ShouldUsePotion(strategy.Option(Track.Potion).As())) + Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, + Player, + ActionQueue.Priority.VeryHigh + (int)OGCDPriority.ForcedOGCD, + 0, + GCD - 0.9f); + #endregion + + #endregion + } + + #region Rotation Helpers + private AID BestRotation() => ComboLastMove switch + { + AID.Souleater => ShouldUseAOE ? AOE() : ST(), + AID.SyphonStrike => ST(), + AID.HardSlash => ST(), + AID.StalwartSoul => ShouldUseAOE ? AOE() : ST(), + AID.Unleash => AOE(), + _ => ShouldUseAOE ? AOE() : ST(), + }; + private AID ST() => ComboLastMove switch + { + AID.SyphonStrike => AID.Souleater, + AID.HardSlash => AID.SyphonStrike, + _ => AID.HardSlash, + }; + private AID AOE() => ComboLastMove switch + { + AID.Unleash => AID.StalwartSoul, + _ => AID.Unleash, + }; + #endregion + + #region Cooldown Helpers + private bool ShouldSpendMP(MPStrategy strategy) + { + if (strategy != MPStrategy.Optimal) + return false; + if (strategy == MPStrategy.Optimal) + { + if (RiskingMP) + return true; + + if (DarkArts.IsActive) + { + if (Delirium.CD >= 40) + return true; + if (Delirium.CD >= Darkside.Timer + GCD) + return true; + } + //2 uses + if (Delirium.CD >= 40 && inOdd) + return MP >= 6000; + //4 uses (5 with DA) + if (Delirium.CD >= 40 && !inOdd) + return MP >= 3000; + } + return false; + } + private bool ShouldUseMP(MPStrategy strategy) => strategy switch + { + MPStrategy.Optimal => ShouldSpendMP(MPStrategy.Optimal), + MPStrategy.Auto3k => CanWeaveIn && MP >= 3000, + MPStrategy.Auto6k => CanWeaveIn && MP >= 6000, + MPStrategy.Auto9k => CanWeaveIn && MP >= 9000, + MPStrategy.AutoRefresh => CanWeaveIn && RiskingMP, + MPStrategy.Edge3k => CanWeaveIn && MP >= 3000, + MPStrategy.Edge6k => CanWeaveIn && MP >= 6000, + MPStrategy.Edge9k => CanWeaveIn && MP >= 9000, + MPStrategy.EdgeRefresh => CanWeaveIn && RiskingMP, + MPStrategy.Flood3k => CanWeaveIn && MP >= 3000, + MPStrategy.Flood6k => CanWeaveIn && MP >= 6000, + MPStrategy.Flood9k => CanWeaveIn && MP >= 9000, + MPStrategy.FloodRefresh => CanWeaveIn && RiskingMP, + MPStrategy.Delay => false, + _ => false + }; + private bool ShouldUseBlood(BloodStrategy strategy, Enemy? target) => strategy switch + { + BloodStrategy.Automatic => ShouldSpendBlood(BloodStrategy.Automatic, target), + BloodStrategy.OnlyBloodspiller => ShouldSpendBlood(BloodStrategy.Automatic, target), + BloodStrategy.OnlyQuietus => ShouldSpendBlood(BloodStrategy.Automatic, target), + BloodStrategy.ForceBloodspiller => Unlocked(AID.Bloodspiller) && (Blood >= 50 || Delirium.IsActive), + BloodStrategy.ForceQuietus => Unlocked(AID.Quietus) && (Blood >= 50 || Delirium.IsActive), + BloodStrategy.Conserve => false, + _ => false + }; + private bool ShouldSpendBlood(BloodStrategy strategy, Enemy? target) => strategy switch + { + BloodStrategy.Automatic => Player.InCombat && target != null && In3y(target?.Actor) && + Blood >= 50 && Darkside.IsActive && Unlocked(AID.Bloodspiller) && (RiskingBlood || Delirium.CD >= 39.5f), + _ => false + }; + private bool ShouldUseSaltedEarth(OGCDStrategy strategy, Enemy? target) => strategy switch + { + OGCDStrategy.Automatic => Player.InCombat && target != null && CanWeaveIn && In3y(target?.Actor) && + Darkside.IsActive && SaltedEarth.IsReady && (CombatTimer < 30 && ComboLastMove is AID.Souleater || CombatTimer >= 30), + OGCDStrategy.Force => SaltedEarth.IsReady, + OGCDStrategy.AnyWeave => SaltedEarth.IsReady && CanWeaveIn, + OGCDStrategy.EarlyWeave => SaltedEarth.IsReady && CanEarlyWeaveIn, + OGCDStrategy.LateWeave => SaltedEarth.IsReady && CanLateWeaveIn, + OGCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUseSaltAndDarkness(OGCDStrategy strategy, Enemy? target) => strategy switch + { + OGCDStrategy.Automatic => Player.InCombat && target?.Actor != null && CanWeaveIn && + TotalCD(AID.SaltAndDarkness) < 0.6f && SaltedEarth.IsActive, + OGCDStrategy.Force => SaltedEarth.IsActive, + OGCDStrategy.AnyWeave => SaltedEarth.IsActive && CanWeaveIn, + OGCDStrategy.EarlyWeave => SaltedEarth.IsActive && CanEarlyWeaveIn, + OGCDStrategy.LateWeave => SaltedEarth.IsActive && CanLateWeaveIn, + OGCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUseCarveOrDrain(CarveStrategy strategy, Enemy? target) => strategy switch + { + CarveStrategy.Automatic => ShouldSpendCarveOrDrain(CarveStrategy.Automatic, target), + CarveStrategy.OnlyCarve => ShouldSpendCarveOrDrain(CarveStrategy.Automatic, target), + CarveStrategy.OnlyDrain => ShouldSpendCarveOrDrain(CarveStrategy.Automatic, target), + CarveStrategy.ForceCarve => CarveAndSpit.IsReady, + CarveStrategy.ForceDrain => AbyssalDrain.IsReady, + CarveStrategy.Delay => false, + _ => false + }; + private bool ShouldSpendCarveOrDrain(CarveStrategy strategy, Enemy? target) => strategy switch + { + CarveStrategy.Automatic => Player.InCombat && target != null && CanWeaveIn && In3y(target?.Actor) && + Darkside.IsActive && AbyssalDrain.IsReady && (CombatTimer < 30 && ComboLastMove is AID.Souleater || CombatTimer >= 30), + _ => false + }; + private bool ShouldUseDelirium(OGCDStrategy strategy, Enemy? target) => strategy switch + { + OGCDStrategy.Automatic => Player.InCombat && target != null && CanWeaveIn && + Darkside.IsActive && Delirium.IsReady && (CombatTimer < 30 && ComboLastMove is AID.Souleater || CombatTimer >= 30), + OGCDStrategy.Force => Delirium.IsReady, + OGCDStrategy.AnyWeave => Delirium.IsReady && CanWeaveIn, + OGCDStrategy.EarlyWeave => Delirium.IsReady && CanEarlyWeaveIn, + OGCDStrategy.LateWeave => Delirium.IsReady && CanLateWeaveIn, + OGCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUseLivingShadow(OGCDStrategy strategy, Enemy? target) => strategy switch + { + OGCDStrategy.Automatic => Player.InCombat && target != null && CanWeaveIn && + Darkside.IsActive && LivingShadow.IsReady, + OGCDStrategy.Force => LivingShadow.IsReady, + OGCDStrategy.AnyWeave => LivingShadow.IsReady && CanWeaveIn, + OGCDStrategy.EarlyWeave => LivingShadow.IsReady && CanEarlyWeaveIn, + OGCDStrategy.LateWeave => LivingShadow.IsReady && CanLateWeaveIn, + OGCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUseShadowbringer(OGCDStrategy strategy, Enemy? target) => strategy switch + { + OGCDStrategy.Automatic => Player.InCombat && target != null && CanWeaveIn && + Darkside.IsActive && Shadowbringer.IsReady && LivingShadow.IsActive && Delirium.IsActive, + OGCDStrategy.Force => Shadowbringer.IsReady, + OGCDStrategy.AnyWeave => Shadowbringer.IsReady && CanWeaveIn, + OGCDStrategy.EarlyWeave => Shadowbringer.IsReady && CanEarlyWeaveIn, + OGCDStrategy.LateWeave => Shadowbringer.IsReady && CanLateWeaveIn, + OGCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUseDeliriumCombo(DeliriumComboStrategy strategy, Enemy? target) => strategy switch + { + DeliriumComboStrategy.Automatic => Player.InCombat && target != null && In3y(target?.Actor) && + Unlocked(AID.ScarletDelirium) && Delirium.Step is 0 or 1 or 2 && Delirium.IsActive, + DeliriumComboStrategy.ScarletDelirum => Unlocked(AID.ScarletDelirium) && Delirium.Step is 0 && Delirium.IsActive, + DeliriumComboStrategy.Comeuppance => Unlocked(AID.Comeuppance) && Delirium.Step is 1 && Delirium.IsActive, + DeliriumComboStrategy.Torcleaver => Unlocked(AID.Torcleaver) && Delirium.Step is 2 && Delirium.IsActive, + DeliriumComboStrategy.Impalement => Unlocked(AID.Impalement) && Delirium.Step is 0 && Delirium.IsActive, + DeliriumComboStrategy.Delay => false, + _ => false + }; + private bool ShouldUseDisesteem(GCDStrategy strategy, Enemy? target) => strategy switch + { + GCDStrategy.Automatic => Player.InCombat && target != null && In10y(target?.Actor) && + Darkside.IsActive && Disesteem.IsReady && (CombatTimer < 30 && Delirium.IsActive || CombatTimer >= 30 && !Delirium.IsActive && Delirium.CD > 15), + GCDStrategy.Force => Disesteem.IsReady, + GCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUseUnmend(UnmendStrategy strategy, Enemy? target) => strategy switch + { + UnmendStrategy.OpenerFar => (Player.InCombat || World.Client.CountdownRemaining < 0.8f) && IsFirstGCD() && !In3y(target?.Actor), + UnmendStrategy.OpenerForce => (Player.InCombat || World.Client.CountdownRemaining < 0.8f) && IsFirstGCD(), + UnmendStrategy.Force => true, + UnmendStrategy.Allow => !In3y(target?.Actor), + UnmendStrategy.Forbid => false, + _ => false + }; + private bool ShouldUsePotion(PotionStrategy strategy) => strategy switch + { + PotionStrategy.AlignWithRaidBuffs => LivingShadow.CD < 5, + PotionStrategy.Immediate => true, + _ => false + }; + #endregion +} diff --git a/BossMod/Autorotation/Standard/akechi/AkechiGNB.cs b/BossMod/Autorotation/Standard/akechi/Tank/AkechiGNB.cs similarity index 99% rename from BossMod/Autorotation/Standard/akechi/AkechiGNB.cs rename to BossMod/Autorotation/Standard/akechi/Tank/AkechiGNB.cs index 2dd8fd94bc..432c2f155a 100644 --- a/BossMod/Autorotation/Standard/akechi/AkechiGNB.cs +++ b/BossMod/Autorotation/Standard/akechi/Tank/AkechiGNB.cs @@ -1,7 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Gauge; -using AID = BossMod.GNB.AID; -using SID = BossMod.GNB.SID; -using TraitID = BossMod.GNB.TraitID; +using BossMod.GNB; namespace BossMod.Autorotation.akechi; //Contribution by Akechi @@ -159,7 +157,7 @@ public static RotationModuleDefinition Definition() { var res = new RotationModuleDefinition("Akechi GNB", //Title "Standard Rotation Module", //Description - "Standard rotation (Akechi)", //Category + "Standard rotation (Akechi)|Tank", //Category "Akechi", //Contributor RotationModuleQuality.Good, //Quality BitMask.Build((int)Class.GNB), //Job diff --git a/BossMod/Autorotation/Standard/akechi/AkechiPLD.cs b/BossMod/Autorotation/Standard/akechi/Tank/AkechiPLD.cs similarity index 99% rename from BossMod/Autorotation/Standard/akechi/AkechiPLD.cs rename to BossMod/Autorotation/Standard/akechi/Tank/AkechiPLD.cs index a781482859..937bccad60 100644 --- a/BossMod/Autorotation/Standard/akechi/AkechiPLD.cs +++ b/BossMod/Autorotation/Standard/akechi/Tank/AkechiPLD.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Gauge; -using AID = BossMod.PLD.AID; -using SID = BossMod.PLD.SID; +using BossMod.PLD; namespace BossMod.Autorotation.akechi; //Contribution by Akechi @@ -112,7 +111,7 @@ public static RotationModuleDefinition Definition() { var res = new RotationModuleDefinition("Akechi PLD", "Standard Rotation Module", - "Standard rotation (Akechi)", + "Standard rotation (Akechi)|Tank", "Akechi", RotationModuleQuality.Good, BitMask.Build((int)Class.GLA, (int)Class.PLD), diff --git a/BossMod/Autorotation/Standard/akechi/Tank/AkechiWAR.cs b/BossMod/Autorotation/Standard/akechi/Tank/AkechiWAR.cs new file mode 100644 index 0000000000..3f4a81eb4a --- /dev/null +++ b/BossMod/Autorotation/Standard/akechi/Tank/AkechiWAR.cs @@ -0,0 +1,697 @@ +using static BossMod.AIHints; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using BossMod.WAR; + +namespace BossMod.Autorotation.akechi; +//Contribution by Akechi +//Discord: @akechdz or 'Akechi' on Puni.sh for maintenance + +public sealed class AkechiWAR(RotationModuleManager manager, Actor player) : AkechiTools(manager, player) +{ + #region Enums: Abilities / Strategies + public enum Track { Gauge = SharedTrack.Count, SurgingTempest, Infuriate, PrimalRend, Upheaval, Onslaught, Tomahawk, Potion, InnerRelease, PrimalWrath, PrimalRuination } + public enum GaugeStrategy { Automatic, OnlyST, OnlyAOE, ForceST, ForceAOE, Conserve } + public enum SurgingTempestStrategy { Automatic, At30s, ForceEye, ForcePath, Delay } + public enum InfuriateStrategy { Automatic, Force, ForceOvercap, Delay } + public enum PrimalRendStrategy { Automatic, ASAP, ASAPNotMoving, AfterBF, LastSecond, GapClose, Force, Delay } + public enum UpheavalStrategy { Automatic, OnlyUpheaval, OnlyOrogeny, ForceUpheaval, ForceOrogeny, Delay } + public enum OnslaughtStrategy { Automatic, Force, Hold0, Hold1, Hold2, GapClose, Delay } + public enum TomahawkStrategy { OpenerFar, OpenerForce, Force, Allow, Forbid } + public enum PotionStrategy { Manual, AlignWithRaidBuffs, Immediate } + #endregion + + #region Module Definitions & Strategies + public static RotationModuleDefinition Definition() + { + var res = new RotationModuleDefinition("Akechi WAR", "Standard Rotation Module", "Standard rotation (Akechi)|Tank", "Akechi", RotationModuleQuality.Ok, BitMask.Build(Class.MRD, Class.WAR), 100); + + res.DefineShared(); + res.Define(Track.Gauge).As("Gauge", "Gauge", uiPriority: 200) + .AddOption(GaugeStrategy.Automatic, "Automatic", "Automatically use Gauge-related abilities optimally", minLevel: 35) + .AddOption(GaugeStrategy.OnlyST, "Only ST", "Uses Inner Beast / Fell Cleave / Inner Chaos optimally as Beast Gauge spender only, regardless of targets", 0, 0, ActionTargets.Hostile, 35) + .AddOption(GaugeStrategy.OnlyAOE, "Only AOE", "Uses Steel Cyclone / Decimate / Chaotic Cyclone optimally as Beast Gauge spender only, regardless of targets", 0, 0, ActionTargets.Hostile, 45) + .AddOption(GaugeStrategy.ForceST, "Force ST", "Force use Inner Beast / Fell Cleave / Inner Chaos", 0, 0, ActionTargets.Hostile, 35) + .AddOption(GaugeStrategy.ForceAOE, "Force AOE", "Force use Steel Cyclone / Decimate / Chaotic Cyclone", 0, 0, ActionTargets.Hostile, 45) + .AddOption(GaugeStrategy.Conserve, "Conserve", "Conserves all Gauge-related abilities as much as possible", 0, 0, ActionTargets.None, 35) + .AddAssociatedActions(AID.InnerBeast, AID.FellCleave, AID.InnerChaos, AID.Decimate, AID.ChaoticCyclone); + res.Define(Track.SurgingTempest).As("Surging Tempest", "S.Tempest", uiPriority: 200) + .AddOption(SurgingTempestStrategy.Automatic, "Auto", "Automatically refreshes Surging Tempest when 10s or less on its duration", minLevel: 50) + .AddOption(SurgingTempestStrategy.At30s, "At 30s", "Refresh Surging Tempest at less than or equal to 30s", 0, 10, ActionTargets.Hostile, 50) + .AddOption(SurgingTempestStrategy.ForceEye, "Force Eye", "Force use Storm's Eye as combo ender", 0, 10, ActionTargets.Hostile, 50) + .AddOption(SurgingTempestStrategy.ForcePath, "Force Path", "Force use Storm's Path as combo ender, essentially delaying Surging Tempest ", 0, 10, ActionTargets.Hostile, 26) + .AddAssociatedActions(AID.StormEye, AID.StormPath); + res.Define(Track.Infuriate).As("Infuriate", "Infuriate", uiPriority: 190) + .AddOption(InfuriateStrategy.Automatic, "Auto", "Automatically decide when to use Infuriate", minLevel: 50) + .AddOption(InfuriateStrategy.Force, "Force", "Force use Infuriate ASAP if not under Nascent Chaos effect", 0, 60, ActionTargets.Self, 50) + .AddOption(InfuriateStrategy.ForceOvercap, "Force Overcap", "Force use Infuriate to prevent overcap on charges if not under Nascent Chaos effect", 0, 60, ActionTargets.Self, 50) + .AddOption(InfuriateStrategy.Delay, "Delay", "Delay use of Infuriate to prevent overcap on charges", 0, 60, ActionTargets.None, 50) + .AddAssociatedActions(AID.Infuriate); + res.Define(Track.PrimalRend).As("Primal Rend", "P.Rend", uiPriority: 180) + .AddOption(PrimalRendStrategy.Automatic, "Auto", "Automatically decide when to use Primal Rend", minLevel: 90) + .AddOption(PrimalRendStrategy.ASAP, "ASAP", "Use Primal Rend ASAP after Inner Release", 0, 20, ActionTargets.Hostile, 90) + .AddOption(PrimalRendStrategy.ASAPNotMoving, "Force Not Moving", "Use Primal Rend ASAP after Inner Release when not moving", 0, 20, ActionTargets.Hostile, 90) + .AddOption(PrimalRendStrategy.AfterBF, "After BF", "Use Primal Rend after consuming all stacks of Burgeoning Fury; if failed to consume all stacks, will use last second", 0, 20, ActionTargets.Hostile, 90) + .AddOption(PrimalRendStrategy.LastSecond, "Last Second", "Force use Primal Rend on last second", 0, 20, ActionTargets.Hostile, 90) + .AddOption(PrimalRendStrategy.GapClose, "Gap Close", "Use as gapcloser when outside melee range", 0, 20, ActionTargets.Hostile, 90) + .AddOption(PrimalRendStrategy.Force, "Force", "Force use Primal Rend", 0, 20, ActionTargets.Hostile, 90) + .AddOption(PrimalRendStrategy.Delay, "Delay", "Delay use of Primal Rend", 0, 0, ActionTargets.Hostile, 90) + .AddAssociatedActions(AID.PrimalRend); + res.Define(Track.Upheaval).As("Upheaval", "Upheaval", uiPriority: 170) + .AddOption(UpheavalStrategy.Automatic, "Auto", "Automatically decide when to use Upheaval or Orogeny", minLevel: 64) + .AddOption(UpheavalStrategy.OnlyUpheaval, "Only Upheaval", "Uses Upheaval optimally as optimal spender only, regardless of targets", 0, 0, ActionTargets.Hostile, 64) + .AddOption(UpheavalStrategy.OnlyOrogeny, "Only Orogeny", "Uses Orogeny optimally as optimal spender only, regardless of targets", 0, 0, ActionTargets.Hostile, 86) + .AddOption(UpheavalStrategy.ForceUpheaval, "Force Upheaval", "Force use Upheaval", 0, 0, ActionTargets.Hostile, 64) + .AddOption(UpheavalStrategy.ForceOrogeny, "Force Orogeny", "Force use Orogeny", 0, 0, ActionTargets.Hostile, 86) + .AddOption(UpheavalStrategy.Delay, "Delay", "Delay use of Upheaval", 0, 0, ActionTargets.None, 64) + .AddAssociatedActions(AID.Upheaval, AID.Orogeny); + res.Define(Track.Onslaught).As("Onslaught", "Onslaught", uiPriority: 170) + .AddOption(OnslaughtStrategy.Automatic, "Auto", "Automatically decide when to use Onslaught; 1 for 1 minute, 3 for 2 minute", minLevel: 62) + .AddOption(OnslaughtStrategy.Force, "Force", "Force use Onslaught", 0, 0, ActionTargets.Hostile, 62) + .AddOption(OnslaughtStrategy.Hold0, "Force All", "Force use Onslaught; holds no charges", 0, 0, ActionTargets.Hostile, 62) + .AddOption(OnslaughtStrategy.Hold1, "Hold 1", "Force use Onslaught; holds 1 charge", 0, 0, ActionTargets.Hostile, 62) + .AddOption(OnslaughtStrategy.Hold2, "Hold 2", "Force use Onslaught; holds 2 charges", 0, 0, ActionTargets.Hostile, 88) + .AddOption(OnslaughtStrategy.GapClose, "Gap Close", "Use as gapcloser when outside melee range", 0, 0, ActionTargets.Hostile, 62) + .AddOption(OnslaughtStrategy.Delay, "Delay", "Delay use of Onslaught", 0, 0, ActionTargets.None, 62) + .AddAssociatedActions(AID.Onslaught); + res.Define(Track.Tomahawk).As("Ranged", "Ranged", uiPriority: 30) + .AddOption(TomahawkStrategy.OpenerFar, "Far (Opener)", "Use Tomahawk in pre-pull & out of melee range", supportedTargets: ActionTargets.Hostile) + .AddOption(TomahawkStrategy.OpenerForce, "Force (Opener)", "Force use Tomahawk in pre-pull in any range", supportedTargets: ActionTargets.Hostile) + .AddOption(TomahawkStrategy.Force, "Force", "Force use Tomahawk in any range", supportedTargets: ActionTargets.Hostile) + .AddOption(TomahawkStrategy.Allow, "Allow", "Allow use of Tomahawk when out of melee range", supportedTargets: ActionTargets.Hostile) + .AddOption(TomahawkStrategy.Forbid, "Forbid", "Prohibit use of Tomahawk") + .AddAssociatedActions(AID.Tomahawk); + res.Define(Track.Potion).As("Potion", uiPriority: 20) + .AddOption(PotionStrategy.Manual, "Manual", "Do not use automatically") + .AddOption(PotionStrategy.AlignWithRaidBuffs, "AlignWithRaidBuffs", "Align with Inner Release & Infuriate charges to ensure use on 2-minute windows", 270, 30, ActionTargets.Self) + .AddOption(PotionStrategy.Immediate, "Immediate", "Use ASAP, regardless of any buffs", 270, 30, ActionTargets.Self) + .AddAssociatedAction(ActionDefinitions.IDPotionStr); + res.DefineOGCD(Track.InnerRelease, "Inner Release", "InnerR.", uiPriority: 170, 60, 15, ActionTargets.Self, 6).AddAssociatedActions(AID.InnerRelease); + res.DefineOGCD(Track.PrimalWrath, "Primal Wrath", "P.Wrath", uiPriority: 135, 20, 0, ActionTargets.Hostile, 96).AddAssociatedActions(AID.PrimalWrath); + res.DefineGCD(Track.PrimalRuination, "PrimalRuination", "P.Ruin.", uiPriority: 150, supportedTargets: ActionTargets.Hostile, minLevel: 100).AddAssociatedActions(AID.PrimalRuination); + + return res; + } + #endregion + + #region Priorities + private GCDPriority FellCleave() + { + var ncActive = CanFitSkSGCD(NascentChaos.Left); + if (ncActive) + { + var prExpiringSoon = CanFitSkSGCD(PrimalRend.Left) && !CanFitSkSGCD(PrimalRend.Left, 2); + if (!CanFitSkSGCD(NascentChaos.Left, prExpiringSoon ? 2 : 1)) + return GCDPriority.LastChanceIC; + } + + var irActive = CanFitSkSGCD(InnerRelease.Left); + var effectiveIRStacks = InnerRelease.Stacks + (ncActive ? 1 : 0); + if (irActive && !CanFitSkSGCD(InnerRelease.Left, effectiveIRStacks)) + return GCDPriority.LastChanceFC; + + var needFCBeforeInf = ncActive || BeastGauge > 50; + if (needFCBeforeInf && !CanFitSkSGCD(Infuriate.TotalCD - (Unlocked(TraitID.EnhancedInfuriate) ? 5 : 0) - SkSGCDLength, 1)) + return GCDPriority.AvoidOvercapInfuriateNext; + + var numFCBeforeInf = InnerRelease.Stacks + ((ncActive || BeastGauge > 50) ? 1 : 0); + if (irActive && !CanFitSkSGCD(InnerRelease.Left, numFCBeforeInf + 1) && !CanFitSkSGCD(Infuriate.TotalCD - (Unlocked(TraitID.EnhancedInfuriate) ? 5 : 0) * numFCBeforeInf - SkSGCDLength, numFCBeforeInf)) + return GCDPriority.AvoidOvercapInfuriateIR; + + var imminentIRStacks = ncActive ? 4 : 3; + if (needFCBeforeInf && !CanFitSkSGCD(InnerRelease.CD, 1) && !CanFitSkSGCD(Infuriate.TotalCD - (Unlocked(TraitID.EnhancedInfuriate) ? 5 : 0) * imminentIRStacks - SkSGCDLength, imminentIRStacks)) + return GCDPriority.AvoidOvercapInfuriateIR; + + if (CanFitSkSGCD(BurstWindowLeft)) + return irActive ? GCDPriority.BuffedIR : GCDPriority.BuffedFC; + + if (irActive) + { + var maxFillers = (int)((InnerRelease.Left - GCD) / SkSGCDLength) + 1 - effectiveIRStacks; + var canDelayFC = maxFillers > 0 && !CanFitSkSGCD(BurstWindowIn, maxFillers); + return canDelayFC ? GCDPriority.DelayFC : GCDPriority.FlexibleIR; + } + else if (ncActive) + { + return NascentChaos.Left > BurstWindowIn ? GCDPriority.DelayFC : GCDPriority.FlexibleFC; + } + else + { + return GCDPriority.DelayFC; + } + } + public enum GCDPriority + { + None = 0, + Standard = 100, + Gauge = 300, + PrimalRuination = 400, + DelayFC = 390, + FlexibleFC = 470, + FlexibleIR = 490, + PrimalRend = 500, + BuffedFC = 550, + BuffedIR = 570, + NeedTempest = 650, + AvoidDropCombo = 660, + AvoidOvercapInfuriateIR = 670, + AvoidOvercapInfuriateNext = 680, + NeedGauge = 700, + LastChanceFC = 770, + LastChanceIC = 780, + Opener = 800, + ForcedTomahawk = 870, + ForcedCombo = 880, + ForcedPR = 890, + ForcedGCD = 900, + GapclosePR = 990, + } + public enum OGCDPriority + { + None = 0, + Standard = 100, + Onslaught = 500, + PrimalWrath = 550, + Infuriate = 570, + Upheaval = 580, + InnerRelease = 590, + Potion = 900, + Gapclose = 980, + ForcedOGCD = 1100, //Enough to put it past CDPlanner's "Automatic" priority, which is really only Medium priority + } + #endregion + + #region Upgrade Paths + private AID BestFellCleave => Unlocked(AID.FellCleave) ? AID.FellCleave : AID.InnerBeast; + private AID BestDecimate => Unlocked(AID.Decimate) ? AID.Decimate : (ShouldUseAOECircle(5).OnFourOrMore || !Unlocked(AID.FellCleave)) ? AID.SteelCyclone : AID.FellCleave; + private AID BestGaugeSpender => ShouldUseAOE ? BestDecimate : BestFellCleave; + private AID BestInnerRelease => Unlocked(AID.InnerRelease) ? AID.InnerRelease : AID.Berserk; + private AID UpheavalOrOrogeny => ShouldUseAOE ? BestOrogeny : AID.Upheaval; + private AID BestOrogeny => Unlocked(AID.Orogeny) ? AID.Orogeny : AID.Upheaval; + private SID BestBerserk => Unlocked(AID.InnerRelease) ? SID.InnerRelease : SID.Berserk; + #endregion + + #region Module Variables + public float TwoMinuteLeft; + public float TwoMinuteIn; + public float BurstWindowLeft; + public float BurstWindowIn; + public bool ForceEye; + public bool ForcePath; + public bool KeepAt30s; + public byte BeastGauge; + public bool ShouldUseAOE; + public int NumSplashTargets; + public Enemy? BestSplashTargets; + public Enemy? BestSplashTarget; + public (float CD, bool IsReady) Upheaval; + public (float CD, bool IsReady) Orogeny; + public (float Left, int Stacks) BurgeoningFury; + public (float Left, bool IsActive) NascentChaos; + public (float CD, bool IsReady) Onslaught; + public (float Left, bool IsActive, bool IsReady) PrimalRend; + public (float Left, bool IsActive, bool IsReady) PrimalWrath; + public (float Left, bool IsActive, bool IsReady) PrimalRuination; + public (float Left, bool IsActive, bool NeedsRefresh, bool KeepAt30s) SurgingTempest; + public (float TotalCD, float ChargeCD, bool HasCharges, bool IsReady) Infuriate; + public (float Left, int Stacks, float CD, bool IsActive, bool IsReady) InnerRelease; + #endregion + public bool IsRiskingGauge() + { + if (BeastGauge >= 90 && //if 90 + ComboLastMove is AID.Maim) //next is Storm's Path, which overcaps. We need spender here + return true; + if (BeastGauge >= 100) + { + if (Unlocked(TraitID.MasteringTheBeast) && + ComboLastMove is AID.Overpower) + return true; + if (ComboLastMove is AID.HeavySwing) + return true; + } + + return false; + } + public override void Execution(StrategyValues strategy, Enemy? primaryTarget) //Executes our actions + { + #region Variables + + #region Strategy Definitions + var bg = strategy.Option(Track.Gauge); + var bgStrat = bg.As(); //Retrieve Gauge strategy + var st = strategy.Option(Track.SurgingTempest); + var stStrat = st.As(); //Retrieve SurgingTempest strategy + var uo = strategy.Option(Track.Upheaval); + var uoStrat = uo.As(); //Retrieve Upheaval strategy + var ir = strategy.Option(Track.InnerRelease); + var irStrat = ir.As(); //Retrieve InnerRelease strategy + var inf = strategy.Option(Track.Infuriate); + var infStrat = inf.As(); //Retrieve Infuriate strategy + var prend = strategy.Option(Track.PrimalRend); + var prendStrat = prend.As(); //Retrieve InnerRelease combo strategy + var pwrath = strategy.Option(Track.PrimalWrath); + var pwrathStrat = pwrath.As(); //Retrieve PrimalWrath strategy + var pruin = strategy.Option(Track.PrimalRuination); + var pruinStrat = pruin.As(); //Retrieve PrimalRuination strategy + var ons = strategy.Option(Track.Onslaught); + var onsStrat = ons.As(); //Retrieve Onslaught strategy + var Tomahawk = strategy.Option(Track.Tomahawk); + var TomahawkStrat = Tomahawk.As(); //Retrieve Tomahawk strategy + ForceEye = stStrat is SurgingTempestStrategy.ForceEye; + ForcePath = stStrat is SurgingTempestStrategy.ForcePath; + KeepAt30s = stStrat is SurgingTempestStrategy.At30s; + #endregion + + #region Gauge + var gauge = World.Client.GetGauge(); //Retrieve WAR gauge + BeastGauge = gauge.BeastGauge; + #endregion + + #region Cooldowns + SurgingTempest.Left = StatusRemaining(Player, SID.SurgingTempest, 60); //Retrieve current SurgingTempest time left + SurgingTempest.IsActive = SurgingTempest.Left > 0.1f; //Checks if SurgingTempest is active + SurgingTempest.KeepAt30s = SurgingTempest.Left <= 30; //Checks if SurgingTempest needs to be refreshed once less than 30s + //TODO: optimize + SurgingTempest.NeedsRefresh = ShouldRefreshTempest(stStrat); //Checks if SurgingTempest needs to be refreshed, roughly 4 GCDs to refresh it + + Upheaval.CD = TotalCD(AID.Upheaval); //Retrieve current Upheaval cooldown + Upheaval.IsReady = Unlocked(AID.Upheaval) && Upheaval.CD < 0.6f; //Upheaval ability + + Orogeny.CD = TotalCD(AID.Orogeny); //Retrieve current Orogeny cooldown + Orogeny.IsReady = Unlocked(AID.Orogeny) && Orogeny.CD < 0.6f; //Orogeny ability + + BurgeoningFury.Stacks = StacksRemaining(Player, SID.BurgeoningFury, 30); //Retrieve current BurgeoningFury stacks + + PrimalRend.Left = StatusRemaining(Player, SID.PrimalRend, 20); //Retrieve current Primal Rend time left + PrimalRend.IsActive = PrimalRend.Left > 0.1f; //Checks if Primal Rend is active + PrimalRend.IsReady = Unlocked(AID.PrimalRend) && PrimalRend.Left > 0.1f; //Primal Rend ability + + PrimalWrath.Left = StatusRemaining(Player, SID.Wrathful, 30); //Retrieve current Primal Wrath time left + PrimalWrath.IsActive = PrimalWrath.Left > 0.1f; //Checks if Primal Wrath is active + PrimalWrath.IsReady = Unlocked(AID.PrimalWrath) && PrimalWrath.Left > 0.1f; //Primal Wrath ability + + PrimalRuination.Left = StatusRemaining(Player, SID.PrimalRuinationReady, 20); //Retrieve current Primal Ruination time left + PrimalRuination.IsActive = PrimalRuination.Left > 0.1f; //Checks if Primal Ruination is active + PrimalRuination.IsReady = Unlocked(AID.PrimalRuination) && PrimalRuination.Left > 0.1f; //Primal Ruination ability + + InnerRelease.Stacks = StacksRemaining(Player, BestBerserk, 15); //Retrieve current InnerRelease stacks + InnerRelease.CD = TotalCD(BestInnerRelease); //Retrieve current InnerRelease cooldown + InnerRelease.IsActive = InnerRelease.Stacks > 0; //Checks if InnerRelease is active + InnerRelease.IsReady = Unlocked(BestInnerRelease) && InnerRelease.CD < 0.6f; //InnerRelease ability + + NascentChaos.Left = StatusRemaining(Player, SID.NascentChaos, 30); + NascentChaos.IsActive = NascentChaos.Left > 0.1f; + + Onslaught.CD = TotalCD(AID.Onslaught); //Retrieve current Onslaught cooldown + Onslaught.IsReady = Unlocked(AID.Onslaught) && Onslaught.CD < 60.6f; //Onslaught ability + + Infuriate.TotalCD = TotalCD(AID.Infuriate); //Retrieve current Infuriate cooldown + Infuriate.HasCharges = Infuriate.TotalCD <= 60; //Checks if Infuriate has charges + Infuriate.IsReady = Unlocked(AID.Infuriate) && Infuriate.HasCharges && !PlayerHasEffect(SID.NascentChaos, 30); //Infuriate ability + Infuriate.ChargeCD = Infuriate.TotalCD * 0.5f; // This gives 60s for one charge + if (Unlocked(TraitID.EnhancedInfuriate)) + { + if (LastActionUsed(AID.FellCleave) || LastActionUsed(AID.Decimate) || LastActionUsed(AID.InnerChaos) || LastActionUsed(AID.ChaoticCyclone)) + { + Infuriate.TotalCD -= 5f; + } + //If the cooldown drops to 0, but TotalCD isn't 0, reset ChargeCD to 60 + //Technically, this should mean that charges are not capped, and therefore the timer is still rolling + if (Infuriate.ChargeCD <= 0 && Infuriate.TotalCD > 0) + { + Infuriate.ChargeCD = 60f; + } + } + #endregion + + ShouldUseAOE = ShouldUseAOECircle(5).OnThreeOrMore; + (BestSplashTargets, NumSplashTargets) = GetBestTarget(primaryTarget, 20, IsSplashTarget); + BestSplashTarget = Unlocked(AID.PrimalRend) && NumSplashTargets >= 2 ? BestSplashTargets : primaryTarget; + (TwoMinuteLeft, TwoMinuteIn) = EstimateRaidBuffTimings(primaryTarget?.Actor); + #endregion + + #region Full Rotation Execution + + #region Standard Rotations + if (strategy.Automatic()) + { + QueueGCD(BestRotation(), //queue the next single-target combo action only if combo is finished + TargetChoice(strategy.Option(SharedTrack.AOE)) //Get target choice + ?? primaryTarget?.Actor, //if none, pick primary target + GCDPriority.Standard); //with priority for 123/10 combo actions + } + if (strategy.ForceST()) //if Force Single Target option is picked + { + QueueGCD(ST(), + TargetChoice(strategy.Option(SharedTrack.AOE)) //Get target choice + ?? primaryTarget?.Actor, //if none, pick primary target + GCDPriority.Standard); //with priority for 123/10 combo actions + } + if (strategy.ForceAOE()) //if Force AOE option is picked + { + QueueGCD(AOE(), + Player, + GCDPriority.Standard); //with priority for 123/10 combo actions + } + #endregion + + #region Cooldowns + if (!strategy.HoldAll()) //if not holding cooldowns + { + if (!strategy.HoldCDs()) //if holding cooldowns + { + if (!strategy.HoldBuffs()) + { + if (ShouldUseInnerRelease(irStrat, primaryTarget)) + QueueOGCD(BestInnerRelease, + Player, + irStrat is OGCDStrategy.Force + or OGCDStrategy.AnyWeave + or OGCDStrategy.EarlyWeave + or OGCDStrategy.LateWeave + ? OGCDPriority.ForcedOGCD + : OGCDPriority.InnerRelease); + } + if (ShouldUseUpheavalOrOrogeny(uoStrat, primaryTarget)) + { + if (uoStrat is UpheavalStrategy.Automatic) + QueueOGCD(UpheavalOrOrogeny, + TargetChoice(uo) ?? primaryTarget?.Actor, + uoStrat is UpheavalStrategy.ForceUpheaval + or UpheavalStrategy.ForceOrogeny + ? OGCDPriority.ForcedOGCD + : OGCDPriority.Upheaval); + if (uoStrat is UpheavalStrategy.OnlyUpheaval) + QueueOGCD(AID.Upheaval, + TargetChoice(uo) ?? primaryTarget?.Actor, + uoStrat is UpheavalStrategy.ForceUpheaval + ? OGCDPriority.ForcedOGCD + : OGCDPriority.Upheaval); + if (uoStrat is UpheavalStrategy.OnlyOrogeny) + QueueOGCD(BestOrogeny, + TargetChoice(uo) ?? primaryTarget?.Actor, + uoStrat is UpheavalStrategy.ForceOrogeny + ? OGCDPriority.ForcedOGCD + : OGCDPriority.Upheaval); + } + if (ShouldUseInfuriate(infStrat, primaryTarget)) + QueueOGCD(AID.Infuriate, + Player, + infStrat is InfuriateStrategy.Force + or InfuriateStrategy.ForceOvercap + ? OGCDPriority.ForcedOGCD + : OGCDPriority.Infuriate); + + if (ShouldUsePrimalRend(prendStrat, primaryTarget)) + QueueGCD(AID.PrimalRend, + TargetChoice(prend) ?? BestSplashTarget?.Actor, + prendStrat is PrimalRendStrategy.Force + or PrimalRendStrategy.ASAP + or PrimalRendStrategy.ASAPNotMoving + ? GCDPriority.ForcedGCD + : GCDPriority.PrimalRend); + + if (ShouldUsePrimalWrath(pwrathStrat, primaryTarget)) + QueueGCD(AID.PrimalWrath, + TargetChoice(pwrath) ?? BestSplashTarget?.Actor, + pwrathStrat is OGCDStrategy.Force + or OGCDStrategy.AnyWeave + or OGCDStrategy.EarlyWeave + or OGCDStrategy.LateWeave + ? OGCDPriority.ForcedOGCD + : OGCDPriority.PrimalWrath); + + if (ShouldUsePrimalRuination(pruinStrat, primaryTarget)) + QueueGCD(AID.PrimalRuination, + TargetChoice(pruin) ?? BestSplashTarget?.Actor, + pruinStrat is GCDStrategy.Force + ? GCDPriority.ForcedGCD + : GCDPriority.PrimalRuination); + if (ShouldUseOnslaught(onsStrat, primaryTarget)) + QueueOGCD(AID.Onslaught, + TargetChoice(ons) ?? primaryTarget?.Actor, + onsStrat is OnslaughtStrategy.Force + or OnslaughtStrategy.GapClose + ? OGCDPriority.ForcedOGCD + : OGCDPriority.Standard); + } + if (!strategy.HoldGauge()) + { + if (ShouldUseGauge(bgStrat, primaryTarget)) + { + if (bgStrat is GaugeStrategy.Automatic) + QueueGCD(BestGaugeSpender, + TargetChoice(bg) ?? primaryTarget?.Actor, + bgStrat is GaugeStrategy.ForceST + or GaugeStrategy.ForceAOE + ? GCDPriority.ForcedGCD + : FellCleave()); + if (bgStrat is GaugeStrategy.OnlyST) + QueueGCD(AID.FellCleave, + TargetChoice(bg) ?? primaryTarget?.Actor, + bgStrat is GaugeStrategy.ForceST + ? GCDPriority.ForcedGCD + : IsRiskingGauge() + ? GCDPriority.NeedGauge + : GCDPriority.Gauge); + if (bgStrat is GaugeStrategy.OnlyAOE) + QueueGCD(AID.Decimate, + Unlocked(AID.Decimate) + ? Player + : TargetChoice(bg) ?? primaryTarget?.Actor, + bgStrat is GaugeStrategy.ForceAOE + ? GCDPriority.ForcedGCD + : IsRiskingGauge() + ? GCDPriority.NeedGauge + : GCDPriority.Gauge); + } + } + } + if (ShouldUseTomahawk(TomahawkStrat, primaryTarget)) + QueueGCD(AID.Tomahawk, + TargetChoice(Tomahawk) ?? primaryTarget?.Actor, + GCDPriority.Standard); + if (ShouldUsePotion(strategy.Option(Track.Potion).As())) + Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, + Player, + ActionQueue.Priority.VeryHigh + (int)OGCDPriority.ForcedOGCD, + 0, + GCD - 0.9f); + #endregion + + #endregion + } + + #region Rotation Helpers + private AID BestRotation() => ComboLastMove switch + { + AID.StormEye => ShouldUseAOE ? AOE() : ST(), + AID.StormPath => ShouldUseAOE ? AOE() : ST(), + AID.Maim => ST(), + AID.HeavySwing => ST(), + AID.MythrilTempest => ShouldUseAOE ? AOE() : ST(), + AID.Overpower => AOE(), + _ => ShouldUseAOE ? AOE() : ST(), + }; + private AID ST() => ComboLastMove switch + { + AID.Maim => ForceEye ? AID.StormEye : ForcePath ? AID.StormPath : SurgingTempest.NeedsRefresh ? AID.StormEye : AID.StormPath, + AID.HeavySwing => AID.Maim, + _ => AID.HeavySwing, + }; + private AID AOE() => ComboLastMove switch + { + AID.Overpower => AID.MythrilTempest, + _ => AID.Overpower, + }; + private int GaugeGainedFromAction(AID aid) => aid switch + { + AID.Maim or AID.StormEye => 10, + AID.StormPath => 20, + AID.MythrilTempest => Unlocked(TraitID.MasteringTheBeast) ? 20 : 0, + _ => 0 + }; + + #endregion + + #region Cooldown Helpers + private bool ShouldUseGauge(GaugeStrategy strategy, Enemy? target) => strategy switch + { + GaugeStrategy.Automatic => ShouldSpendGauge(GaugeStrategy.Automatic, target), + GaugeStrategy.OnlyST => ShouldSpendGauge(GaugeStrategy.Automatic, target), + GaugeStrategy.OnlyAOE => ShouldSpendGauge(GaugeStrategy.Automatic, target), + GaugeStrategy.ForceST => Unlocked(AID.FellCleave) && (BeastGauge >= 50 || InnerRelease.IsActive || PlayerHasEffect(SID.NascentChaos, 30)), + GaugeStrategy.ForceAOE => Unlocked(AID.Decimate) && (BeastGauge >= 50 || InnerRelease.IsActive || PlayerHasEffect(SID.NascentChaos, 30)), + GaugeStrategy.Conserve => false, + _ => false + }; + private bool ShouldSpendGauge(GaugeStrategy strategy, Enemy? target) => strategy switch + { + GaugeStrategy.Automatic => Player.InCombat && target != null && In3y(target?.Actor) && Unlocked(BestFellCleave) && SurgingTempest.IsActive && (BeastGauge >= 50 || InnerRelease.Stacks > 0), + _ => false + }; + public bool ShouldDumpGauge(Enemy? target) => BeastGauge >= 50 && + (TargetHPP(target?.Actor) <= 3 || InnerRelease.CD <= (SkSGCDLength * 2) + 0.5f && !NascentChaos.IsActive); + + public bool ShouldRefreshTempest(SurgingTempestStrategy strategy) + { + if (!Unlocked(AID.StormEye) || strategy is SurgingTempestStrategy.Delay || strategy is SurgingTempestStrategy.ForcePath) + return false; + + if (strategy is SurgingTempestStrategy.Automatic) + return SurgingTempest.Left <= 10; + + if (strategy is SurgingTempestStrategy.At30s) + return SurgingTempest.Left <= 30; + + if (strategy is SurgingTempestStrategy.ForceEye) + return SurgingTempest.Left >= 0; + + return false; + } + private bool ShouldUseUpheavalOrOrogeny(UpheavalStrategy strategy, Enemy? target) => strategy switch + { + UpheavalStrategy.Automatic => ShouldSpendUpheavalOrOrogeny(UpheavalStrategy.Automatic, target), + UpheavalStrategy.OnlyUpheaval => ShouldSpendUpheavalOrOrogeny(UpheavalStrategy.Automatic, target), + UpheavalStrategy.OnlyOrogeny => ShouldSpendUpheavalOrOrogeny(UpheavalStrategy.Automatic, target), + UpheavalStrategy.ForceUpheaval => Upheaval.IsReady, + UpheavalStrategy.ForceOrogeny => Orogeny.IsReady, + UpheavalStrategy.Delay => false, + _ => false + }; + private bool ShouldSpendUpheavalOrOrogeny(UpheavalStrategy strategy, Enemy? target) => strategy switch + { + UpheavalStrategy.Automatic => Player.InCombat && target != null && CanWeaveIn && In3y(target?.Actor) && SurgingTempest.IsActive && + Upheaval.IsReady && (CombatTimer < 30 && ComboLastMove is AID.StormEye || CombatTimer >= 30), + _ => false + }; + private bool ShouldUseInnerRelease(OGCDStrategy strategy, Enemy? target) => strategy switch + { + OGCDStrategy.Automatic => Player.InCombat && target != null && CanWeaveIn && SurgingTempest.IsActive && InnerRelease.IsReady, + OGCDStrategy.Force => InnerRelease.IsReady, + OGCDStrategy.AnyWeave => InnerRelease.IsReady && CanWeaveIn, + OGCDStrategy.EarlyWeave => InnerRelease.IsReady && CanEarlyWeaveIn, + OGCDStrategy.LateWeave => InnerRelease.IsReady && CanLateWeaveIn, + OGCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUseInfuriate(InfuriateStrategy strategy, Enemy? target) + { + if (strategy == InfuriateStrategy.Delay || CanFitSkSGCD(NascentChaos.Left)) + return false; + if (strategy == InfuriateStrategy.Force) + return true; + if (strategy == InfuriateStrategy.ForceOvercap && Infuriate.TotalCD <= World.Client.AnimationLock) + return true; + + if (BeastGauge > 50) + return false; + if (target == null) + return false; + + var irActive = CanFitSkSGCD(InnerRelease.Left); + if (!Unlocked(AID.InnerRelease)) + { + if (irActive) + return true; + + if (!CanFitSkSGCD(Infuriate.TotalCD, 4)) + return true; + + return false; + } + + var unlockedNC = Unlocked(AID.ChaoticCyclone); + if (unlockedNC && irActive && !CanFitSkSGCD(InnerRelease.Left, InnerRelease.Stacks)) + return false; + + var maxInfuriateCD = GCD + SkSGCDLength; + if (BeastGauge + GaugeGainedFromAction(NextGCD) > 50) + { + var numFCsToBurnGauge = 1; + if (irActive) + numFCsToBurnGauge += InnerRelease.Stacks; + else if (!CanFitSkSGCD(InnerRelease.CD, 1)) + numFCsToBurnGauge += 3; + maxInfuriateCD += (SkSGCDLength + (Unlocked(TraitID.EnhancedInfuriate) ? 5 : 0)) * numFCsToBurnGauge; + } + if (NextGCD is AID.FellCleave or AID.InnerBeast or AID.SteelCyclone or AID.Decimate) + { + maxInfuriateCD += (Unlocked(TraitID.EnhancedInfuriate) ? 5 : 0); + } + if (Infuriate.TotalCD < maxInfuriateCD) + return true; + + if (irActive && unlockedNC) + return false; + + var (gaugeGained, costedGCDs) = NextGCD switch + { + AID.HeavySwing => (20, 3), + AID.Maim => (20, 2), + AID.StormEye => (10, 1), + AID.Overpower => (20, 2), + AID.MythrilTempest => (20, 1), + _ => (30, 4) + }; + if (BeastGauge + gaugeGained + 50 > 100 && !CanFitSkSGCD(SurgingTempest.Left, costedGCDs)) + return false; + + return CanFitSkSGCD(BurstWindowLeft); + } + private bool ShouldUsePrimalRend(PrimalRendStrategy strategy, Enemy? target) => strategy switch + { + PrimalRendStrategy.Automatic => Player.InCombat && target != null && PrimalRend.IsReady && InnerRelease.Stacks <= 2, + PrimalRendStrategy.ASAP => PrimalRend.IsReady, + PrimalRendStrategy.ASAPNotMoving => PrimalRend.IsReady && !IsMoving, + PrimalRendStrategy.AfterBF => PrimalRend.IsReady && InnerRelease.Stacks == 0 && BurgeoningFury.Stacks == 0, + PrimalRendStrategy.LastSecond => PrimalRend.Left <= SkSGCDLength + 0.5f, + PrimalRendStrategy.GapClose => !In3y(target?.Actor), + PrimalRendStrategy.Force => PrimalRend.IsReady, + PrimalRendStrategy.Delay => false, + _ => false + }; + private bool ShouldUsePrimalWrath(OGCDStrategy strategy, Enemy? target) => strategy switch + { + OGCDStrategy.Automatic => Player.InCombat && target != null && CanWeaveIn && PrimalWrath.IsReady, + OGCDStrategy.Force => PrimalWrath.IsReady, + OGCDStrategy.AnyWeave => PrimalWrath.IsReady && CanWeaveIn, + OGCDStrategy.EarlyWeave => PrimalWrath.IsReady && CanEarlyWeaveIn, + OGCDStrategy.LateWeave => PrimalWrath.IsReady && CanLateWeaveIn, + OGCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUsePrimalRuination(GCDStrategy strategy, Enemy? target) => strategy switch + { + GCDStrategy.Automatic => Player.InCombat && target != null && In3y(target?.Actor) && PrimalRuination.IsReady, + GCDStrategy.Force => PrimalRuination.IsReady, + GCDStrategy.Delay => false, + _ => false + }; + private bool ShouldUseOnslaught(OnslaughtStrategy strategy, Enemy? target) => strategy switch + { + OnslaughtStrategy.Automatic => Player.InCombat && target != null && In3y(target?.Actor) && Unlocked(AID.Onslaught) && Onslaught.CD <= 60.5f && InnerRelease.IsActive, + OnslaughtStrategy.Hold0 => Unlocked(AID.Onslaught) && Onslaught.IsReady, + OnslaughtStrategy.Hold1 => Unlocked(AID.Onslaught) && Onslaught.CD <= 30.5f, + OnslaughtStrategy.Hold2 => Unlocked(AID.Onslaught) && Onslaught.CD < 0.6f, + OnslaughtStrategy.GapClose => !In3y(target?.Actor), + OnslaughtStrategy.Force => Unlocked(AID.Onslaught) && Onslaught.IsReady, + OnslaughtStrategy.Delay => false, + _ => false + }; + private bool ShouldUseTomahawk(TomahawkStrategy strategy, Enemy? target) => strategy switch + { + TomahawkStrategy.OpenerFar => (Player.InCombat || World.Client.CountdownRemaining < 0.8f) && IsFirstGCD() && !In3y(target?.Actor), + TomahawkStrategy.OpenerForce => (Player.InCombat || World.Client.CountdownRemaining < 0.8f) && IsFirstGCD(), + TomahawkStrategy.Force => true, + TomahawkStrategy.Allow => !In3y(target?.Actor), + TomahawkStrategy.Forbid => false, + _ => false + }; + private bool ShouldUsePotion(PotionStrategy strategy) => strategy switch + { + PotionStrategy.AlignWithRaidBuffs => InnerRelease.CD < 5, + PotionStrategy.Immediate => true, + _ => false + }; + #endregion +}