From 479a3230428eb78b860ad167852dbaa0fcbe17e0 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:53:17 -0400 Subject: [PATCH 1/2] monk 3 --- BossMod/Autorotation/MNK/MNKActions.cs | 42 +- BossMod/Autorotation/MNK/MNKConfig.cs | 3 - BossMod/Autorotation/MNK/MNKDefinitions.cs | 5 + BossMod/Autorotation/MNK/MNKRotation.cs | 585 ++++++++++++++++----- BossMod/CooldownPlanner/PlanDefinitions.cs | 21 +- 5 files changed, 521 insertions(+), 135 deletions(-) diff --git a/BossMod/Autorotation/MNK/MNKActions.cs b/BossMod/Autorotation/MNK/MNKActions.cs index ed493ec157..df3920e53a 100644 --- a/BossMod/Autorotation/MNK/MNKActions.cs +++ b/BossMod/Autorotation/MNK/MNKActions.cs @@ -1,4 +1,5 @@ -using Dalamud.Game.ClientState.JobGauge.Types; +using System; +using Dalamud.Game.ClientState.JobGauge.Types; namespace BossMod.MNK; @@ -7,6 +8,7 @@ class Actions : CommonActions public const int AutoActionST = AutoActionFirstCustom + 0; public const int AutoActionAOE = AutoActionFirstCustom + 1; public const int AutoActionFiller = AutoActionFirstCustom + 2; + public const int AutoActionSTQOpener = AutoActionFirstCustom + 3; private readonly Rotation.State _state; private readonly Rotation.Strategy _strategy; @@ -54,28 +56,37 @@ protected override void UpdateInternalState(int autoAction) { UpdatePlayerState(); FillCommonStrategy(_strategy, CommonDefinitions.IDPotionStr); + _strategy.NumBlitzTargets = NumTargetsHitByBlitz(); _strategy.ApplyStrategyOverrides(Autorot.Bossmods.ActiveModule?.PlanExecution?.ActiveStrategyOverrides(Autorot.Bossmods.ActiveModule.StateMachine) ?? []); _strategy.NumPointBlankAOETargets = autoAction == AutoActionST ? 0 : NumTargetsHitByPBAOE(); _strategy.NumEnlightenmentTargets = Autorot.PrimaryTarget != null && autoAction != AutoActionST && _state.Unlocked(AID.HowlingFist) ? NumTargetsHitByEnlightenment(Autorot.PrimaryTarget) : 0; + _strategy.UseAOE = _strategy.NumPointBlankAOETargets >= 3; + _strategy.UseSTQOpener = autoAction == AutoActionSTQOpener; + if (autoAction == AutoActionFiller) { _strategy.FireUse = Rotation.Strategy.FireStrategy.Delay; _strategy.WindUse = CommonRotation.Strategy.OffensiveAbilityUse.Delay; _strategy.BrotherhoodUse = CommonRotation.Strategy.OffensiveAbilityUse.Delay; _strategy.PerfectBalanceUse = CommonRotation.Strategy.OffensiveAbilityUse.Delay; + _strategy.TrueNorthUse = CommonRotation.Strategy.OffensiveAbilityUse.Delay; } + FillStrategyPositionals(_strategy, Rotation.GetNextPositional(_state, _strategy), _state.TrueNorthLeft > _state.GCD); } protected override void QueueAIActions() { - if (_state.Unlocked(AID.SteelPeak)) - SimulateManualActionForAI(ActionID.MakeSpell(AID.Meditation), Player, !Player.InCombat && _state.Chakra < 5); if (_state.Unlocked(AID.SecondWind)) SimulateManualActionForAI(ActionID.MakeSpell(AID.SecondWind), Player, Player.InCombat && Player.HPMP.CurHP < Player.HPMP.MaxHP * 0.5f); if (_state.Unlocked(AID.Bloodbath)) SimulateManualActionForAI(ActionID.MakeSpell(AID.Bloodbath), Player, Player.InCombat && Player.HPMP.CurHP < Player.HPMP.MaxHP * 0.8f); + if (_state.Unlocked(AID.Meditation)) + SimulateManualActionForAI(ActionID.MakeSpell(AID.Meditation), Player, !Player.InCombat && _state.Chakra < 5); + // TODO: this ends up being super annoying in some cases, maybe reconsider conditions + // if (_state.Unlocked(AID.FormShift)) + // SimulateManualActionForAI(ActionID.MakeSpell(AID.FormShift), Player, !Player.InCombat && _state.FormShiftLeft == 0 && _state.PerfectBalanceLeft == 0); } protected override NextAction CalculateAutomaticGCD() @@ -88,14 +99,14 @@ protected override NextAction CalculateAutomaticGCD() protected override NextAction CalculateAutomaticOGCD(float deadline) { - if (!Rotation.HaveTarget(_state, _strategy) || AutoAction < AutoActionAIFight) + if (AutoAction < AutoActionAIFight) return new(); ActionID res = new(); if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot - res = Rotation.GetNextBestOGCD(_state, _strategy, deadline - _state.OGCDSlotLength); + res = Rotation.GetNextBestOGCD(_state, _strategy, deadline - _state.OGCDSlotLength, deadline); if (!res && _state.CanWeave(deadline)) // second/only ogcd slot - res = Rotation.GetNextBestOGCD(_state, _strategy, deadline); + res = Rotation.GetNextBestOGCD(_state, _strategy, deadline, deadline); return MakeResult(res, Autorot.PrimaryTarget); } @@ -107,6 +118,7 @@ private void UpdatePlayerState() _state.Chakra = gauge.Chakra; _state.BeastChakra = gauge.BeastChakra; _state.Nadi = gauge.Nadi; + _state.BlitzLeft = gauge.BlitzTimeRemaining / 1000f; (_state.Form, _state.FormLeft) = DetermineForm(); _state.DisciplinedFistLeft = StatusDetails(Player, SID.DisciplinedFist, Player.InstanceID).Left; @@ -116,6 +128,14 @@ private void UpdatePlayerState() _state.FireLeft = StatusDetails(Player, SID.RiddleOfFire, Player.InstanceID).Left; _state.TrueNorthLeft = StatusDetails(Player, SID.TrueNorth, Player.InstanceID).Left; + // these are functionally the same as far as the rotation is concerned + _state.LostExcellenceLeft = MathF.Max( + StatusDetails(Player, SID.LostExcellence, Player.InstanceID).Left, + StatusDetails(Player, SID.Memorable, Player.InstanceID).Left + ); + _state.FoPLeft = StatusDetails(Player, SID.LostFontofPower, Player.InstanceID).Left; + _state.HsacLeft = StatusDetails(Player, SID.BannerHonoredSacrifice, Player.InstanceID).Left; + _state.TargetDemolishLeft = StatusDetails(Autorot.PrimaryTarget, SID.Demolish, Player.InstanceID).Left; } @@ -139,6 +159,7 @@ private void OnConfigModified(MNKConfig config) SupportedSpell(AID.Bootshine).PlaceholderForAuto = config.FullRotation ? AutoActionST : AutoActionNone; SupportedSpell(AID.ArmOfTheDestroyer).PlaceholderForAuto = SupportedSpell(AID.ShadowOfTheDestroyer).PlaceholderForAuto = config.FullRotation ? AutoActionAOE : AutoActionNone; SupportedSpell(AID.TrueStrike).PlaceholderForAuto = config.FillerRotation ? AutoActionFiller : AutoActionNone; + SupportedSpell(AID.SnapPunch).PlaceholderForAuto = config.FullRotation ? AutoActionSTQOpener : AutoActionNone; // combo replacement SupportedSpell(AID.FourPointFury).TransformAction = config.AOECombos ? () => ActionID.MakeSpell(Rotation.GetNextComboAction(_state, _strategy)) : null; @@ -149,11 +170,16 @@ private void OnConfigModified(MNKConfig config) SupportedSpell(AID.Thunderclap).TransformTarget = config.SmartThunderclap ? (act) => Autorot.SecondaryTarget ?? act : null; - _strategy.PreCombatFormShift = config.AutoFormShift; - // smart targets } + private int NumTargetsHitByBlitz() + { + if (_state.BestBlitz is AID.TornadoKick or AID.PhantomRush) + return Autorot.PrimaryTarget == null ? 0 : Autorot.Hints.NumPriorityTargetsInAOECircle(Autorot.PrimaryTarget.Position, 5); + return Autorot.Hints.NumPriorityTargetsInAOECircle(Player.Position, 5); + } + private int NumTargetsHitByPBAOE() => Autorot.Hints.NumPriorityTargetsInAOECircle(Player.Position, 5); private int NumTargetsHitByEnlightenment(Actor primary) => Autorot.Hints.NumPriorityTargetsInAOERect(Player.Position, (primary.Position - Player.Position).Normalized(), 10, _state.Unlocked(AID.Enlightenment) ? 2 : 1); } diff --git a/BossMod/Autorotation/MNK/MNKConfig.cs b/BossMod/Autorotation/MNK/MNKConfig.cs index 6cf9590fe9..652b1fdef4 100644 --- a/BossMod/Autorotation/MNK/MNKConfig.cs +++ b/BossMod/Autorotation/MNK/MNKConfig.cs @@ -17,7 +17,4 @@ class MNKConfig : ConfigNode [PropertyDisplay("Delay Thunderclap if already in melee range of target")] public bool PreventCloseDash = true; - - [PropertyDisplay("Use Form Shift out of combat")] - public bool AutoFormShift = false; } diff --git a/BossMod/Autorotation/MNK/MNKDefinitions.cs b/BossMod/Autorotation/MNK/MNKDefinitions.cs index 365dc9c049..7802eea656 100644 --- a/BossMod/Autorotation/MNK/MNKDefinitions.cs +++ b/BossMod/Autorotation/MNK/MNKDefinitions.cs @@ -116,6 +116,11 @@ public enum SID : uint Stun = 2, // applied by Leg Sweep to target FormlessFist = 2513, // applied by Form Shift to self SixSidedStar = 2514, // applied by Six-Sided Star to self + + LostFontofPower = 2346, + BannerHonoredSacrifice = 2327, + LostExcellence = 2564, + Memorable = 2565, } public static class Definitions diff --git a/BossMod/Autorotation/MNK/MNKRotation.cs b/BossMod/Autorotation/MNK/MNKRotation.cs index 5da002c0f3..89d2e7aaa9 100644 --- a/BossMod/Autorotation/MNK/MNKRotation.cs +++ b/BossMod/Autorotation/MNK/MNKRotation.cs @@ -7,6 +7,11 @@ public static class Rotation { public enum Form { None, OpoOpo, Raptor, Coeurl } + private static readonly float SSSApplicationDelay = 0.62f; + + // make configurable? idk? only rotation devs would care about this + private static readonly bool Debug = false; + // full state needed for determining next action public class State(WorldState ws) : CommonRotation.PlayerState(ws) { @@ -14,6 +19,7 @@ public class State(WorldState ws) : CommonRotation.PlayerState(ws) public BeastChakra[] BeastChakra = []; public Nadi Nadi; public Form Form; + public float BlitzLeft; // 20 max public float FormLeft; // 0 if no form, 30 max public float DisciplinedFistLeft; // 15 max public float LeadenFistLeft; // 30 max @@ -22,14 +28,20 @@ public class State(WorldState ws) : CommonRotation.PlayerState(ws) public float FormShiftLeft; // 30 max public float FireLeft; // 20 max public float TrueNorthLeft; // 10 max + public float LostExcellenceLeft; // 60(?) max + public float FoPLeft; // 30 max + public float HsacLeft; // 15 max + + public bool HasLunar => Nadi.HasFlag(Nadi.LUNAR); + public bool HasSolar => Nadi.HasFlag(Nadi.SOLAR); + public bool HasBothNadi => HasLunar && HasSolar; - public bool HaveLunar => Nadi.HasFlag(Nadi.LUNAR); - public bool HaveSolar => Nadi.HasFlag(Nadi.SOLAR); + public bool CanFormShift => Unlocked(AID.FormShift) && PerfectBalanceLeft == 0; public int BeastCount => BeastChakra.Count(x => x != Dalamud.Game.ClientState.JobGauge.Enums.BeastChakra.NONE); - public bool ForcedLunar => BeastChakra[0] == Dalamud.Game.ClientState.JobGauge.Enums.BeastChakra.OPOOPO && BeastChakra[1] == Dalamud.Game.ClientState.JobGauge.Enums.BeastChakra.OPOOPO; - public bool ForcedSolar => BeastChakra.Any(x => x is not Dalamud.Game.ClientState.JobGauge.Enums.BeastChakra.NONE and not Dalamud.Game.ClientState.JobGauge.Enums.BeastChakra.OPOOPO); + public bool ForcedLunar => BeastCount > 1 && BeastChakra[0] == BeastChakra[1] && !HasBothNadi; + public bool ForcedSolar => BeastCount > 1 && BeastChakra[0] != BeastChakra[1] && !HasBothNadi; // upgrade paths public AID BestForbiddenChakra => Unlocked(AID.ForbiddenChakra) ? AID.ForbiddenChakra : AID.SteelPeak; @@ -45,7 +57,7 @@ public AID BestBlitz if (BeastCount != 3) return AID.MasterfulBlitz; - if (HaveLunar && HaveSolar) + if (HasLunar && HasSolar) return BestPhantomRush; var bc = BeastChakra; @@ -63,19 +75,30 @@ public AID BestBlitz public override string ToString() { - return $"RB={RaidBuffsLeft:f1}, Demo={TargetDemolishLeft:f1}, DF={DisciplinedFistLeft:f1}, Form={Form}/{FormLeft:f1}, LFist={LeadenFistLeft:f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}/{UnlockProgress}"; + return $"RB={RaidBuffsLeft:f1}, Demo={TargetDemolishLeft:f1}, DF={DisciplinedFistLeft:f1}, Blitz={BlitzLeft:f1}, Form={Form}/{FormLeft:f1}, LFist={LeadenFistLeft:f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}/{UnlockProgress}"; } } // strategy configuration public class Strategy : CommonRotation.Strategy { - public int NumPointBlankAOETargets; // range 5 around self - public int NumEnlightenmentTargets; // range 10 width 2/4 rect + public int NumBlitzTargets; // 5y around self + public int NumPointBlankAOETargets; // 5y around self + public int NumEnlightenmentTargets; // 10y/4y rect public bool UseAOE; - public bool PreCombatFormShift; + public bool UseSTQOpener; + + public enum FormShiftStrategy : uint + { + [PropertyDisplay("Use if there are no targets in range")] + Automatic = 0, + [PropertyDisplay("Do not use")] + Delay = 1 + } + + public FormShiftStrategy FormShiftUse; public enum DashStrategy : uint { @@ -94,16 +117,31 @@ public enum DashStrategy : uint public enum NadiChoice : uint { Automatic = 0, // lunar -> solar - [PropertyDisplay("Lunar", 0xFFDB8BCA)] Lunar = 1, - [PropertyDisplay("Solar", 0xFF8EE6FA)] - Solar = 2 + Solar = 2, + [PropertyDisplay("Lunar (downtime)", 0xA0DB8BCA)] + LunarDowntime = 3, + [PropertyDisplay("Solar (downtime)", 0xA08EE6FA)] + SolarDowntime = 4 } public NadiChoice NextNadi; + public enum FormChoice : uint + { + Automatic = 0, + [PropertyDisplay("Opo-Opo", 0xFF3B34DA)] + Opo = 1, + [PropertyDisplay("Raptor", 0xFF38D17B)] + Raptor = 2, + [PropertyDisplay("Coeurl", 0xFFA264D7)] + Coeurl = 3, + } + + public FormChoice FormShiftForm; + public enum FireStrategy : uint { Automatic = 0, // use on cooldown-ish if something is targetable @@ -115,15 +153,55 @@ public enum FireStrategy : uint Force = 2, [PropertyDisplay("Delay until Brotherhood is off cooldown")] - DelayUntilBrotherhood = 3 + DelayUntilBrotherhood = 3, + + [PropertyDisplay("Delay until 1 Beast Chakra is opened")] + DelayBeast1 = 4, + + [PropertyDisplay("Delay until 2 Beast Chakra are opened")] + DelayBeast2 = 5, + + [PropertyDisplay("Delay until 3 Beast Chakra are opened")] + DelayBeast3 = 6 } public FireStrategy FireUse; + + public enum BlitzStrategy : uint + { + // use when available + Automatic = 0, + [PropertyDisplay("Delay")] + Delay = 1, + [PropertyDisplay("Delay until at least two targets are in range")] + DelayUntilMultiTarget = 2, + } + public BlitzStrategy BlitzUse; + + public enum DragonKickStrategy : uint + { + // standard rotation, use in opo-opo form to proc leaden fist + Automatic = 0, + [PropertyDisplay("Replace all GCDs unless Leaden Fist is active or Disciplined Fist will expire")] + Filler = 1, + } + public OffensiveAbilityUse WindUse; public OffensiveAbilityUse BrotherhoodUse; + public OffensiveAbilityUse TFCUse; + public OffensiveAbilityUse MeditationUse; public OffensiveAbilityUse PerfectBalanceUse; + public FormChoice PBForm1; + public FormChoice PBForm2; + public FormChoice PBForm3; public OffensiveAbilityUse SSSUse; public OffensiveAbilityUse TrueNorthUse; + public OffensiveAbilityUse DisciplinedFistUse; + public OffensiveAbilityUse DemolishUse; + public DragonKickStrategy DragonKickUse; + public OffensiveAbilityUse PotionUse; + + public float ActualFightEndIn => FightEndIn == 0 ? 10000f : FightEndIn; public override string ToString() { @@ -132,27 +210,51 @@ public override string ToString() public void ApplyStrategyOverrides(uint[] overrides) { - if (overrides.Length >= 8) + if (overrides.Length >= 18) { DashUse = (DashStrategy)overrides[0]; TrueNorthUse = (OffensiveAbilityUse)overrides[1]; - NextNadi = (NadiChoice)overrides[2]; - FireUse = (FireStrategy)overrides[3]; - WindUse = (OffensiveAbilityUse)overrides[4]; - BrotherhoodUse = (OffensiveAbilityUse)overrides[5]; - PerfectBalanceUse = (OffensiveAbilityUse)overrides[6]; - SSSUse = (OffensiveAbilityUse)overrides[7]; + DisciplinedFistUse = (OffensiveAbilityUse)overrides[2]; + DemolishUse = (OffensiveAbilityUse)overrides[3]; + NextNadi = (NadiChoice)overrides[4]; + FireUse = (FireStrategy)overrides[5]; + WindUse = (OffensiveAbilityUse)overrides[6]; + BrotherhoodUse = (OffensiveAbilityUse)overrides[7]; + TFCUse = (OffensiveAbilityUse)overrides[8]; + MeditationUse = (OffensiveAbilityUse)overrides[9]; + PerfectBalanceUse = (OffensiveAbilityUse)overrides[10]; + PBForm1 = (FormChoice)overrides[11]; + PBForm2 = (FormChoice)overrides[12]; + PBForm3 = (FormChoice)overrides[13]; + FormShiftUse = (FormShiftStrategy)overrides[14]; + FormShiftForm = (FormChoice)overrides[15]; + BlitzUse = (BlitzStrategy)overrides[16]; + DragonKickUse = (DragonKickStrategy)overrides[17]; + SSSUse = (OffensiveAbilityUse)overrides[18]; + PotionUse = (OffensiveAbilityUse)overrides[19]; } else { DashUse = DashStrategy.Automatic; TrueNorthUse = OffensiveAbilityUse.Automatic; + DisciplinedFistUse = OffensiveAbilityUse.Automatic; + DemolishUse = OffensiveAbilityUse.Automatic; NextNadi = NadiChoice.Automatic; FireUse = FireStrategy.Automatic; WindUse = OffensiveAbilityUse.Automatic; BrotherhoodUse = OffensiveAbilityUse.Automatic; + TFCUse = OffensiveAbilityUse.Automatic; + MeditationUse = OffensiveAbilityUse.Automatic; PerfectBalanceUse = OffensiveAbilityUse.Automatic; + PBForm1 = FormChoice.Automatic; + PBForm2 = FormChoice.Automatic; + PBForm3 = FormChoice.Automatic; + FormShiftUse = FormShiftStrategy.Automatic; + FormShiftForm = FormChoice.Automatic; + BlitzUse = BlitzStrategy.Automatic; + DragonKickUse = DragonKickStrategy.Automatic; SSSUse = OffensiveAbilityUse.Automatic; + PotionUse = OffensiveAbilityUse.Automatic; } } } @@ -188,11 +290,20 @@ public static AID GetRaptorFormAction(State state, Strategy strategy) // during fire windows, if next GCD is demo, force refresh to align loop; we can't use a lunar PB unless // DF + demo are close to max duration, since DF only lasts about 7-8 GCDs and a blitz window is 5 - if (rofIsAligned && WillDemolishExpire(state, strategy, 4)) + if (rofIsAligned && NeedDemolishRefresh(state, strategy, 4)) + return AID.TwinSnakes; + + // force refresh if we anticipate another PB use in this buff window + if ( + state.FireLeft >= state.GCD + state.AttackGCDTime * 3 && + state.CanWeave(state.CD(CDGroup.PerfectBalance) - 40, 0.6f, state.GCD + state.AttackGCDTime) && + state.PerfectBalanceLeft == 0 && + state.HasSolar + ) return AID.TwinSnakes; // normal refresh - if (WillDFExpire(state, 3)) + if (NeedDFRefresh(state, strategy, 3)) return AID.TwinSnakes; return AID.TrueStrike; @@ -207,7 +318,7 @@ public static AID GetCoeurlFormAction(State state, Strategy strategy) return AID.Rockbreaker; // normal refresh - if (!strategy.ForbidDOTs && state.Unlocked(AID.Demolish) && WillDemolishExpire(state, strategy, 3)) + if (!strategy.ForbidDOTs && state.Unlocked(AID.Demolish) && NeedDemolishRefresh(state, strategy, 3)) return AID.Demolish; return AID.SnapPunch; @@ -227,44 +338,84 @@ public static AID GetNextComboAction(State state, Strategy strategy) public static AID GetNextBestGCD(State state, Strategy strategy) { - if (strategy.CombatTimer < 0) + // tradeoff here between always using meditation + form shift when not in combat ("optimal") versus only using + // them during countdowns (mostly optimal). + // the tradeoff is that "not in combat" includes the scenario of manually targeting an enemy you want to attack, + // even if they're already in melee range, which incurs an annoying 3s delay + // maybe AI mode should separately handle the out of combat form shift + meditate usage? + if (strategy.CombatTimer is < 0 and > -100) { if (state.Chakra < 5 && state.Unlocked(AID.Meditation)) return AID.Meditation; - if (strategy.CombatTimer > -20 && state.FormShiftLeft < 5 && state.Unlocked(AID.FormShift)) + if ( + strategy.FormShiftUse == Strategy.FormShiftStrategy.Automatic + && state.FormShiftLeft < 3 + && state.CanFormShift + ) return AID.FormShift; - if (strategy.PreCombatFormShift && state.FormShiftLeft < 2 && state.Unlocked(AID.FormShift)) - return AID.FormShift; + if (strategy.CombatTimer > -10) + { + // form shift on countdown. TODO: ignore Never here? don't think there's ever any reason not to use it on countdown + if ( + strategy.FormShiftUse == Strategy.FormShiftStrategy.Automatic + && strategy.CombatTimer < -9 + && state.FormShiftLeft < 15 + && state.Unlocked(AID.FormShift) + ) + return AID.FormShift; - if (strategy.CombatTimer > -100) return AID.None; + } } if (!HaveTarget(state, strategy)) { - if (state.Chakra < 5 && state.Unlocked(AID.Meditation)) + if (state.Chakra < 5 && state.Unlocked(AID.Meditation) && strategy.MeditationUse != CommonRotation.Strategy.OffensiveAbilityUse.Delay) return AID.Meditation; + if (strategy.FormShiftUse == Strategy.FormShiftStrategy.Automatic && state.CanFormShift && state.FormShiftLeft < 3) + return AID.FormShift; + + if (strategy.NextNadi == Strategy.NadiChoice.LunarDowntime && state.BeastCount < 3 && state.PerfectBalanceLeft > 0) + return AID.ShadowOfTheDestroyer; + + if (strategy.NextNadi == Strategy.NadiChoice.SolarDowntime && state.PerfectBalanceLeft > 0) + return state.BeastCount switch + { + 0 => AID.ShadowOfTheDestroyer, + 1 => AID.FourPointFury, + 2 => AID.Rockbreaker, + _ => AID.None + }; + return AID.None; } - if (state.Unlocked(AID.SixSidedStar) && strategy.SSSUse == Strategy.OffensiveAbilityUse.Force) + if (state.RangeToTarget > 3 && strategy.DashUse == Strategy.DashStrategy.GapClose && state.CD(CDGroup.Thunderclap) <= 60 && state.Unlocked(AID.Thunderclap)) + return AID.Thunderclap; + + if (state.Unlocked(AID.SixSidedStar) && strategy.SSSUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) return AID.SixSidedStar; - if (state.BestBlitz != AID.MasterfulBlitz) + if (strategy.UseSTQOpener && state.LostExcellenceLeft > 0 && state.FoPLeft == 0) + return AID.SixSidedStar; + + if (state.BestBlitz != AID.MasterfulBlitz && strategy.NumBlitzTargets > 0 && ShouldBlitz(state, strategy)) return state.BestBlitz; // TODO: calculate optimal DK spam before SSS if ( - strategy.SSSUse == Strategy.OffensiveAbilityUse.Automatic - && strategy.FightEndIn > state.GCD - && strategy.FightEndIn < state.GCD + state.AttackGCDTime + strategy.SSSUse == CommonRotation.Strategy.OffensiveAbilityUse.Automatic + && strategy.ActualFightEndIn < state.GCD + state.AttackGCDTime + SSSApplicationDelay && state.Unlocked(AID.SixSidedStar) ) return AID.SixSidedStar; + if (state.Unlocked(AID.DragonKick) && ShouldDKSpam(state, strategy)) + return AID.DragonKick; + return GetNextComboAction(state, strategy); } @@ -292,12 +443,12 @@ public static (Positional, bool) GetNextPositional(State state, Strategy strateg if (isCastingGcd && !formIsPending && state.PerfectBalanceLeft == 0) gcdsUntilCoeurl -= 1; - var willDemolish = state.Unlocked(AID.Demolish) && WillDemolishExpire(state, strategy, gcdsUntilCoeurl); + var willDemolish = state.Unlocked(AID.Demolish) && NeedDemolishRefresh(state, strategy, gcdsUntilCoeurl); return (willDemolish ? Positional.Rear : Positional.Flank, curForm == Form.Coeurl); } - public static ActionID GetNextBestOGCD(State state, Strategy strategy, float deadline) + public static ActionID GetNextBestOGCD(State state, Strategy strategy, float deadline, float finalOGCDDeadline) { // TODO: potion @@ -311,29 +462,73 @@ public static ActionID GetNextBestOGCD(State state, Strategy strategy, float dea ) return ActionID.MakeSpell(AID.Thunderclap); + if (strategy.PotionUse == CommonRotation.Strategy.OffensiveAbilityUse.Force && state.CanWeave(state.PotionCD, 1.1f, deadline)) + return CommonDefinitions.IDPotionStr; + return new(); } + if (strategy.UseSTQOpener && HaveTarget(state, strategy)) + { + var hsac = BozjaActionID.GetNormal(BozjaHolsterID.BannerHonoredSacrifice); + var fop = BozjaActionID.GetNormal(BozjaHolsterID.LostFontOfPower); + var ex = BozjaActionID.GetNormal(BozjaHolsterID.LostExcellence); + + var hsacInBag = state.BozjaHolster[(int)BozjaHolsterID.BannerHonoredSacrifice] > 0; + var hsacSlot = state.FindDutyActionSlot(hsac, fop); + var exSlot = state.FindDutyActionSlot(ex, fop); + + if (state.LostExcellenceLeft > 0) + { + if (state.HsacLeft > 0) + { + if (state.FoPLeft > 0) + { + if (state.CanWeave(state.PotionCD, 0.6f, deadline)) + return CommonDefinitions.IDPotionStr; + } + + if (state.CanWeave(state.DutyActionCD(fop), 0.6f, deadline)) + return fop; + } + + if (state.CanWeave(state.DutyActionCD(hsac), 0.6f, deadline)) + return hsac; + + if (hsacSlot < 0) + return ActionID.MakeBozjaHolster(BozjaHolsterID.BannerHonoredSacrifice, exSlot); + } + + if (state.Form == Form.Raptor && hsacInBag && exSlot >= 0 && state.CanWeave(state.DutyActionCD(ex), 0.6f, deadline)) + return ex; + } + if (state.GCD <= 0.800f && ShouldUseRoF(state, strategy, deadline)) - return ActionID.MakeSpell(AID.RiddleOfFire); + { + // this is checked separately here because other functions (notably ShouldUsePB) make decisions + // based on whether RoF is expected to be off cooldown by a given time + var shouldRoFDelayed = strategy.FireUse switch + { + Strategy.FireStrategy.DelayBeast1 => state.BeastCount >= 1, + Strategy.FireStrategy.DelayBeast2 => state.BeastCount >= 2, + Strategy.FireStrategy.DelayBeast3 => state.BeastCount == 3, + _ => true + }; + if (shouldRoFDelayed) + return ActionID.MakeSpell(AID.RiddleOfFire); + } - if (ShouldUsePB(state, strategy, deadline)) - return ActionID.MakeSpell(AID.PerfectBalance); + if (strategy.PotionUse == CommonRotation.Strategy.OffensiveAbilityUse.Force && state.CanWeave(state.PotionCD, 1.1f, deadline)) + return CommonDefinitions.IDPotionStr; if (ShouldUseBrotherhood(state, strategy, deadline)) return ActionID.MakeSpell(AID.Brotherhood); + if (ShouldUsePB(state, strategy, deadline)) + return ActionID.MakeSpell(AID.PerfectBalance); + // 2. steel peek, if have chakra - if ( - state.Unlocked(AID.SteelPeak) - && state.Chakra == 5 - && state.CanWeave(CDGroup.SteelPeak, 0.6f, deadline) - && (state.CD(CDGroup.RiddleOfFire) > 0 // prevent early use in opener - || strategy.FireUse == Strategy.FireStrategy.Delay - || strategy.FireUse == Strategy.FireStrategy.DelayUntilBrotherhood - || !state.Unlocked(AID.RiddleOfFire) - ) - ) + if (ShouldUseTFC(state, strategy, deadline)) { // L15 Steel Peak is 180p // L40 Howling Fist is 100p/target => HF at 2+ targets @@ -352,10 +547,10 @@ public static ActionID GetNextBestOGCD(State state, Strategy strategy, float dea if (ShouldUseRoW(state, strategy, deadline)) return ActionID.MakeSpell(AID.RiddleOfWind); - if (ShouldUseTrueNorth(state, strategy) && state.CanWeave(state.CD(CDGroup.TrueNorth) - 45, 0.6f, deadline)) + if (ShouldUseTrueNorth(state, strategy, finalOGCDDeadline) && state.CanWeave(state.CD(CDGroup.TrueNorth) - 45, 0.6f, deadline)) return ActionID.MakeSpell(AID.TrueNorth); - if (ShouldDash(state, strategy, deadline)) + if (ShouldDash(state, strategy)) return ActionID.MakeSpell(AID.Thunderclap); // no suitable oGCDs... @@ -366,20 +561,37 @@ private static Form GetEffectiveForm(State state, Strategy strategy) { if (state.PerfectBalanceLeft > state.GCD) { + Strategy.FormChoice[] formOverrides = [strategy.PBForm1, strategy.PBForm2, strategy.PBForm3]; + switch (formOverrides[state.BeastCount]) + { + case Strategy.FormChoice.Opo: + return Form.OpoOpo; + case Strategy.FormChoice.Coeurl: + return Form.Coeurl; + case Strategy.FormChoice.Raptor: + return Form.Raptor; + default: + break; + } + + bool canCoeurl, canRaptor, canOpo; + var nextNadi = strategy.NextNadi; // if a blitz is already in progress, finish it even if buffs would fall off in the process, since celestial revolution is always a mistake var forcedLunar = nextNadi == Strategy.NadiChoice.Lunar || state.ForcedLunar; var forcedSolar = nextNadi == Strategy.NadiChoice.Solar || state.ForcedSolar; - var canCoeurl = !forcedLunar; - var canRaptor = !forcedLunar; - // slightly annoying conditional because this is always true in lunar, but only true in solar if we haven't used it yet, just like the others - var canOpo = !state.ForcedSolar || state.BeastChakra.All(b => b != BeastChakra.OPOOPO); - - foreach (var chak in state.BeastChakra) - { - canCoeurl &= chak != BeastChakra.COEURL; - canRaptor &= chak != BeastChakra.RAPTOR; - } + canCoeurl = !forcedLunar; + canRaptor = !forcedLunar; + canOpo = true; + + if (!state.HasBothNadi) + foreach (var chak in state.BeastChakra) + { + canCoeurl &= chak != BeastChakra.COEURL; + canRaptor &= chak != BeastChakra.RAPTOR; + if (forcedSolar) + canOpo &= chak != BeastChakra.OPOOPO; + } // big pile of conditionals to check whether this is a forced solar (buffs are running out). // odd windows are planned out such that buffed demo was used right before perfect balance, so this @@ -387,23 +599,25 @@ private static Form GetEffectiveForm(State state, Strategy strategy) // see ShouldUsePB for more context if (canCoeurl && canRaptor) { - if (WillDemolishExpire(state, strategy, 2)) + if (state.DisciplinedFistLeft == 0) + return Form.Raptor; + if (NeedDemolishRefresh(state, strategy, 2)) return Form.Coeurl; - if (WillDFExpire(state, 2)) + if (NeedDFRefresh(state, strategy, 2)) return Form.Raptor; } else if (canCoeurl) { - if (state.BeastCount == 1 && WillDemolishExpire(state, strategy, 1)) + if (state.BeastCount == 1 && NeedDemolishRefresh(state, strategy, 1)) return Form.Coeurl; - else if (state.BeastCount == 2 && WillDemolishExpire(state, strategy, 5)) + else if (state.BeastCount == 2 && NeedDemolishRefresh(state, strategy, 5)) return Form.Coeurl; } else if (canRaptor) { - if (state.BeastCount == 1 && WillDFExpire(state, 1)) + if (state.BeastCount == 1 && NeedDFRefresh(state, strategy, 1)) return Form.Raptor; - else if (state.BeastCount == 2 && WillDFExpire(state, 4)) + else if (state.BeastCount == 2 && NeedDFRefresh(state, strategy, 4)) return Form.Raptor; } @@ -416,7 +630,9 @@ private static Form GetEffectiveForm(State state, Strategy strategy) // if we try to delay both lunar/solar until RoF is up, like the standard opener (which is just BH3), // pre-PB demolish will fall off for multiple GCDs; // so early non-demo solar is the only way to prevent clipping - var isBH2 = state.FireLeft == 0 && (forcedSolar || !state.HaveSolar); + + // TODO: full demo is more potency than any single gcd, so we should use opo before demo if a refresh is imminent + var isBH2 = state.FireLeft == 0 && (forcedSolar || !state.HasSolar) && state.Unlocked(AID.RiddleOfFire); if (isBH2) return canRaptor ? Form.Raptor : canCoeurl ? Form.Coeurl : Form.OpoOpo; @@ -424,33 +640,56 @@ private static Form GetEffectiveForm(State state, Strategy strategy) } if (state.FormShiftLeft > state.GCD) + { + switch (strategy.FormShiftForm) + { + case Strategy.FormChoice.Automatic: + break; + case Strategy.FormChoice.Coeurl: + return Form.Coeurl; + case Strategy.FormChoice.Raptor: + return Form.Raptor; + default: + return Form.OpoOpo; + } + + if (NeedDemolishRefresh(state, strategy, 2) && state.DisciplinedFistLeft > state.GCD) + return Form.Coeurl; + return Form.OpoOpo; + } return state.Form; } - private static bool ShouldDash(State state, Strategy strategy, float deadline) - { - if ( - state.RangeToTarget <= 3 - || !state.Unlocked(AID.Thunderclap) - || !state.CanWeave(state.CD(CDGroup.Thunderclap) - 60, 0.6f, deadline) - || strategy.DashUse == Strategy.DashStrategy.Forbid - ) - return false; + private static bool ShouldBlitz(State state, Strategy strategy) + => state.DisciplinedFistLeft > state.GCD && + strategy.BlitzUse switch + { + Strategy.BlitzStrategy.Delay => false, + Strategy.BlitzStrategy.DelayUntilMultiTarget => strategy.NumBlitzTargets > 1 || state.BlitzLeft < state.AttackGCDTime, + _ => true, + }; - if (strategy.DashUse == Strategy.DashStrategy.GapClose) - return true; + private static bool ShouldDKSpam(State state, Strategy strategy) + => strategy.DragonKickUse switch + { + Strategy.DragonKickStrategy.Filler => state.LeadenFistLeft == 0 && state.DisciplinedFistLeft > state.GCD, + _ => false, + }; - // someone early pulled - if ( - strategy.DashUse == Strategy.DashStrategy.Automatic - && strategy.CombatTimer > 0 - && strategy.CombatTimer < 3 - ) - return true; + private static bool ShouldDash(State state, Strategy strategy) + { + if (!state.Unlocked(AID.Thunderclap) || state.CD(CDGroup.Thunderclap) > 60) + return false; - return false; + return strategy.DashUse switch + { + Strategy.DashStrategy.Automatic => strategy.CombatTimer > 0 && strategy.CombatTimer < 1 && state.RangeToTarget > 3, + Strategy.DashStrategy.Forbid => false, + Strategy.DashStrategy.GapClose => state.RangeToTarget > 3, + _ => false, + }; } private static bool ShouldUseRoF(State state, Strategy strategy, float deadline) @@ -465,6 +704,9 @@ private static bool ShouldUseRoF(State state, Strategy strategy, float deadline) if (strategy.FireUse == Strategy.FireStrategy.Force) return true; + if (!HaveTarget(state, strategy) || strategy.ActualFightEndIn < 20) + return false; + // prevent early use in standard opener return state.DisciplinedFistLeft > state.GCD; } @@ -473,14 +715,17 @@ private static bool ShouldUseRoW(State state, Strategy strategy, float deadline) { if ( !state.Unlocked(AID.RiddleOfWind) - || strategy.WindUse == Strategy.OffensiveAbilityUse.Delay + || strategy.WindUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay || !state.CanWeave(CDGroup.RiddleOfWind, 0.6f, deadline) ) return false; - if (strategy.WindUse == Strategy.OffensiveAbilityUse.Force) + if (strategy.WindUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) return true; + if (!HaveTarget(state, strategy) || strategy.ActualFightEndIn < 15) + return false; + // thebalance recommends using RoW like an oGCD dot, so we use on cooldown as long as buffs have been used first return state.CD(CDGroup.RiddleOfFire) > 0 && state.CD(CDGroup.Brotherhood) > 0; } @@ -489,18 +734,26 @@ private static bool ShouldUseBrotherhood(State state, Strategy strategy, float d { if ( !state.Unlocked(AID.Brotherhood) - || strategy.BrotherhoodUse == Strategy.OffensiveAbilityUse.Delay + || strategy.BrotherhoodUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay || !state.CanWeave(CDGroup.Brotherhood, 0.6f, deadline) ) return false; - if (strategy.BrotherhoodUse == Strategy.OffensiveAbilityUse.Force) + if (strategy.BrotherhoodUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) return true; - // opener timing mostly important as long as rof is used first, we just want to align with party buffs - - // the default opener is bhood after first bootshine - // later uses can be asap - return !strategy.UseAOE && state.CD(CDGroup.RiddleOfFire) > 0 && (state.LeadenFistLeft == 0 || strategy.CombatTimer > 30); + if (!HaveTarget(state, strategy) || strategy.ActualFightEndIn < 15) + return false; + + return !strategy.UseAOE + && state.CD(CDGroup.RiddleOfFire) > 0 + && ( + // opener timing mostly important as long as rof is used first, we just want to align with party buffs - + // the default opener is bhood after first bootshine + state.LeadenFistLeft == 0 + // later uses can be asap + || strategy.CombatTimer > 30 + ); } private static bool ShouldUsePB(State state, Strategy strategy, float deadline) @@ -509,78 +762,170 @@ private static bool ShouldUsePB(State state, Strategy strategy, float deadline) state.PerfectBalanceLeft > 0 || !state.Unlocked(AID.PerfectBalance) || !state.CanWeave(state.CD(CDGroup.PerfectBalance) - 40, 0.6f, deadline) - || strategy.PerfectBalanceUse == Strategy.OffensiveAbilityUse.Delay + || strategy.PerfectBalanceUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay ) - return false; + return LogWhy(false, "PB", $"PBLeft = {state.PerfectBalanceLeft}, cd = {state.CD(CDGroup.PerfectBalance)}"); - if (strategy.PerfectBalanceUse == Strategy.OffensiveAbilityUse.Force) - return true; + if (strategy.PerfectBalanceUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) + return LogWhy(true, "PB", "forced"); + + if (!HaveTarget(state, strategy) || strategy.ActualFightEndIn < state.GCD + state.AttackGCDTime * 3) + return LogWhy(false, "PB", $"target={HaveTarget(state, strategy)}, fight end={strategy.ActualFightEndIn}"); // with enough haste/low enough GCD (< 1.6, currently exclusive to bozja), double lunar is possible without dropping buffs // via lunar -> opo -> snakes -> pb -> lunar // this is the only time PB use is not directly after an opo GCD if (state.Form == Form.Coeurl && state.FireLeft > deadline + state.AttackGCDTime * 3) - return !WillDFExpire(state, 5) && !WillDemolishExpire(state, strategy, 3); + return LogWhy( + !NeedDFRefresh(state, strategy, 5) && !NeedDemolishRefresh(state, strategy, 3), + "PB", + $"nonstandard (coeurl) lunar, DF={state.DisciplinedFistLeft}, Demo={state.TargetDemolishLeft}" + ); if (state.Form != Form.Raptor) - return false; + return LogWhy(false, "PB", "not in raptor"); // bh1 and bh3 even windows where RoF is used no earlier than 2 GCDs before this; also odd windows where // natural demolish happens during RoF - // before level 68/RoF unlock, we have nothing to plan our blitzes around, so just use PB whenever it's off cooldown + // before level 68 (RoF unlock) we have nothing to plan our blitzes around, so just use PB whenever it's off cooldown // as long as buffs won't fall off + // TODO: before level 60 (blitz unlock) PB is just a free opo GCD generator so use it right after DF + demo if (ShouldUseRoF(state, strategy, deadline) || state.FireLeft > deadline + state.AttackGCDTime * 3 || !state.Unlocked(AID.RiddleOfFire)) { if (!CanSolar(state, strategy)) - return !WillDFExpire(state, 5) && !WillDemolishExpire(state, strategy, 6); + { + return LogWhy( + !NeedDFRefresh(state, strategy, 5) && !NeedDemolishRefresh(state, strategy, 6), + "PB", + $"BH1 (RoF active or imminent), solar unavailable, DF={state.DisciplinedFistLeft}, Demo={state.TargetDemolishLeft}" + ); + } // see haste note above; delay standard even window PB2 in favor of double lunar - if (WillDFExpire(state, 3) && !WillDemolishExpire(state, strategy, 5)) - return false; + if (NeedDFRefresh(state, strategy, 3) && !NeedDemolishRefresh(state, strategy, 4)) + return LogWhy(false, "PB", $"BH1 (RoF active or imminent), DF expiring = {state.DisciplinedFistLeft}"); - return true; + return LogWhy(true, "PB", "BH1 (RoF active or imminent)"); } // odd windows where natural demolish happens before RoF, at most 3 GCDs prior - raptor GCD is forced to // be twin snakes if this is the case, so we don't need to check DF timer if (!CanSolar(state, strategy) && ShouldUseRoF(state, strategy, state.GCD + state.AttackGCDTime)) - return !WillDemolishExpire(state, strategy, 7); - - // bhood 2 window: natural demolish happens in the middle of RoF. i don't remember exactly why this is the rule - // but the first blitz has to be RP + return LogWhy( + !NeedDemolishRefresh(state, strategy, 7), + "PB", + $"odd window, solar unavailable, RoF imminent, demo = {state.TargetDemolishLeft}" + ); + + // bhood 2 window: natural demolish happens in the middle of RoF. it's possible that only the blitz itself + // gets the RoF buff, so BH2 consists of + // 1. PB -> "weak" non-OPO gcds until RoF is active + // 2. RoF -> RP + // 3. opo, DF, demolish + // 4. PB -> lunar if ( CanSolar(state, strategy) && !ShouldUseRoF(state, strategy, deadline) && ShouldUseRoF(state, strategy, deadline + state.AttackGCDTime * 3) ) - return !WillDemolishExpire(state, strategy, 7); + return LogWhy(!NeedDemolishRefresh(state, strategy, 7), "PB", $"BH2 (early unbuffed solar), demo = {state.TargetDemolishLeft}"); - return false; + // forced solar (cdplan or because we would otherwise overcap lunar) + // (we are guaranteed to be in raptor form due to conditional above) + if ((strategy.NextNadi == Strategy.NadiChoice.Solar || state.HasLunar && !state.HasSolar) && state.CD(CDGroup.RiddleOfFire) == 0) + return LogWhy(true, "PB", "Solar forced"); + + return LogWhy(false, "PB", "fallback"); } - private static bool ShouldUseTrueNorth(State state, Strategy strategy) + private static bool ShouldUseTrueNorth(State state, Strategy strategy, float lastOgcdDeadline) { if ( - strategy.TrueNorthUse == Strategy.OffensiveAbilityUse.Delay + strategy.TrueNorthUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay || state.TrueNorthLeft > state.AnimationLock ) return false; - if (strategy.TrueNorthUse == Strategy.OffensiveAbilityUse.Force) + if (strategy.TrueNorthUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) + return true; + if (!HaveTarget(state, strategy)) + return false; + + var positionalIsWrong = strategy.NextPositionalImminent && !strategy.NextPositionalCorrect; + + // always late weave true north if possible (it's annoying for it to be used immediately) + // but prioritize Riddle of Fire over it + if (ShouldUseRoF(state, strategy, lastOgcdDeadline)) + return positionalIsWrong; + else + return positionalIsWrong && state.GCD <= 0.800; + } + + private static bool ShouldUseTFC(State state, Strategy strategy, float deadline) + { + if ( + !state.Unlocked(AID.SteelPeak) + || state.Chakra < 5 + || strategy.TFCUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay + || !state.CanWeave(CDGroup.SteelPeak, 0.6f, deadline) + ) + return false; + + if (strategy.TFCUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) return true; - return strategy.NextPositionalImminent && !strategy.NextPositionalCorrect; + // prevent early use in opener + return state.CD(CDGroup.RiddleOfFire) > 0 || !state.Unlocked(AID.RiddleOfFire); } + // UseAOE is only true if enemies are in range public static bool HaveTarget(State state, Strategy strategy) => state.TargetingEnemy || strategy.UseAOE; - private static bool WillDemolishExpire(State state, Strategy strategy, int gcds) => !strategy.UseAOE && WillStatusExpire(state, gcds, state.TargetDemolishLeft); - private static bool WillDFExpire(State state, int gcds) => WillStatusExpire(state, gcds, state.DisciplinedFistLeft); - private static bool WillStatusExpire(State state, int gcds, float statusDuration) => statusDuration < state.GCD + (state.AttackGCDTime * gcds); + private static bool NeedDemolishRefresh(State state, Strategy strategy, int gcds) + { + // don't care + if (strategy.UseAOE) + return false; + + if (strategy.DemolishUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) + return true; + + if (strategy.DemolishUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay) + return false; + + if (WillStatusExpire(state, gcds, state.TargetDemolishLeft)) + // snap is 280 (if flank) potency + // demo is 310 (if rear) potency after 3 ticks: 100 + 70 * 3 + // TODO: this should actually be calculating from the time when we expect to refresh demolish, rather than naively adding duration to the current one, but it probably works for most purposes? + return true; // strategy.ActualFightEndIn > state.TargetDemolishLeft + 9; + + return false; + } + + private static bool NeedDFRefresh(State state, Strategy strategy, int gcds) + { + if (strategy.DisciplinedFistUse == CommonRotation.Strategy.OffensiveAbilityUse.Force) + return true; + + if (strategy.DisciplinedFistUse == CommonRotation.Strategy.OffensiveAbilityUse.Delay) + return false; + + return WillStatusExpire(state, gcds, state.DisciplinedFistLeft); + } + + private static bool WillStatusExpire(State state, int gcds, float statusDuration) + => statusDuration < state.GCD + state.AttackGCDTime * gcds; private static bool CanSolar(State state, Strategy strategy) => strategy.NextNadi switch { Strategy.NadiChoice.Solar => true, Strategy.NadiChoice.Lunar => false, - _ => !state.HaveSolar + _ => !state.HasSolar }; + + private static T LogWhy(T value, string tag, string message) + { + if (Debug) + Service.Log($"[{tag}] {value}: {message}"); + return value; + } } diff --git a/BossMod/CooldownPlanner/PlanDefinitions.cs b/BossMod/CooldownPlanner/PlanDefinitions.cs index 276c2948fc..c35cc71330 100644 --- a/BossMod/CooldownPlanner/PlanDefinitions.cs +++ b/BossMod/CooldownPlanner/PlanDefinitions.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace BossMod; public static class PlanDefinitions @@ -110,14 +113,24 @@ private static ClassData DefineMNK() c.CooldownTracks.Add(new("Mantra", ActionID.MakeSpell(MNK.AID.Mantra), 42)); c.StrategyTracks.Add(new("Dash", typeof(MNK.Rotation.Strategy.DashStrategy))); c.StrategyTracks.Add(new("TrueN", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); + c.StrategyTracks.Add(new("DF", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); + c.StrategyTracks.Add(new("Demo", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); c.StrategyTracks.Add(new("Nadi", typeof(MNK.Rotation.Strategy.NadiChoice))); c.StrategyTracks.Add(new("RoF", typeof(MNK.Rotation.Strategy.FireStrategy))); c.StrategyTracks.Add(new("RoW", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); - c.StrategyTracks.Add(new("BHood", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); - c.StrategyTracks.Add( - new("PerfBal", typeof(CommonRotation.Strategy.OffensiveAbilityUse)) - ); + c.StrategyTracks.Add(new("BH", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); + c.StrategyTracks.Add(new("TFC", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); + c.StrategyTracks.Add(new("Meditate", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); + c.StrategyTracks.Add(new("PB", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); + c.StrategyTracks.Add(new("PB1", typeof(MNK.Rotation.Strategy.FormChoice))); + c.StrategyTracks.Add(new("PB2", typeof(MNK.Rotation.Strategy.FormChoice))); + c.StrategyTracks.Add(new("PB3", typeof(MNK.Rotation.Strategy.FormChoice))); + c.StrategyTracks.Add(new("FS", typeof(MNK.Rotation.Strategy.FormShiftStrategy))); + c.StrategyTracks.Add(new("FSForm", typeof(MNK.Rotation.Strategy.FormChoice))); + c.StrategyTracks.Add(new("Blitz", typeof(MNK.Rotation.Strategy.BlitzStrategy))); + c.StrategyTracks.Add(new("DK", typeof(MNK.Rotation.Strategy.DragonKickStrategy))); c.StrategyTracks.Add(new("SSS", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); + c.StrategyTracks.Add(new("Potion", typeof(CommonRotation.Strategy.OffensiveAbilityUse))); return c; } From 2b46e2cefa02c3176f4dd2c4adbdbdc5b91cee6a Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:03:24 -0400 Subject: [PATCH 2/2] i can count --- BossMod/Autorotation/MNK/MNKRotation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BossMod/Autorotation/MNK/MNKRotation.cs b/BossMod/Autorotation/MNK/MNKRotation.cs index 89d2e7aaa9..add0019de8 100644 --- a/BossMod/Autorotation/MNK/MNKRotation.cs +++ b/BossMod/Autorotation/MNK/MNKRotation.cs @@ -210,7 +210,7 @@ public override string ToString() public void ApplyStrategyOverrides(uint[] overrides) { - if (overrides.Length >= 18) + if (overrides.Length >= 20) { DashUse = (DashStrategy)overrides[0]; TrueNorthUse = (OffensiveAbilityUse)overrides[1];