From 76c632169e12c454f6223eb64f40a98c15fe7ce7 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:36:18 -0400 Subject: [PATCH] mnk changes --- BossMod/Autorotation/xan/Basexan.cs | 3 + BossMod/Autorotation/xan/Melee/MNK.cs | 241 ++++++++++++++++++++------ 2 files changed, 189 insertions(+), 55 deletions(-) diff --git a/BossMod/Autorotation/xan/Basexan.cs b/BossMod/Autorotation/xan/Basexan.cs index 35c4704553..83a56d9516 100644 --- a/BossMod/Autorotation/xan/Basexan.cs +++ b/BossMod/Autorotation/xan/Basexan.cs @@ -312,6 +312,9 @@ protected void UpdatePositionals(Actor? target, (Positional pos, bool imm) posit public sealed override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, float forceMovementIn, bool isMoving) { + NextGCD = default; + NextGCDPrio = 0; + var pelo = Player.FindStatus(BossMod.BRD.SID.Peloton); PelotonLeft = pelo != null ? StatusDuration(pelo.Value.ExpireAt) : 0; SwiftcastLeft = StatusLeft(BossMod.WHM.SID.Swiftcast); diff --git a/BossMod/Autorotation/xan/Melee/MNK.cs b/BossMod/Autorotation/xan/Melee/MNK.cs index c959d9298d..65a160dacf 100644 --- a/BossMod/Autorotation/xan/Melee/MNK.cs +++ b/BossMod/Autorotation/xan/Melee/MNK.cs @@ -5,13 +5,27 @@ namespace BossMod.Autorotation.xan; public sealed class MNK(RotationModuleManager manager, Actor player) : Attackxan(manager, player) { - public enum Track { Potion = SharedTrack.Count, SSS } + public enum Track { Potion = SharedTrack.Count, SSS, Meditation, FiresReply } public enum PotionStrategy { Manual, PreBuffs, Now } + public enum MeditationStrategy + { + Safe, + Greedy, + Force, + Delay + } + public enum FRStrategy + { + Automatic, + Ranged, + Force, + Delay + } public static RotationModuleDefinition Definition() { @@ -25,6 +39,18 @@ public static RotationModuleDefinition Definition() def.DefineSimple(Track.SSS, "SixSidedStar"); + def.Define(Track.Meditation).As("Meditate") + .AddOption(MeditationStrategy.Safe, "Use out of combat, during countdown, or if no enemies are targetable") + .AddOption(MeditationStrategy.Greedy, "Allow using when primary enemy is targetable, but out of range") + .AddOption(MeditationStrategy.Force, "Use even if enemy is in melee range") + .AddOption(MeditationStrategy.Delay, "Do not use"); + + def.Define(Track.FiresReply).As("FiresReply") + .AddOption(FRStrategy.Automatic, "Use after Opo GCD") + .AddOption(FRStrategy.Ranged, "Use when out of melee range, or if about to expire") + .AddOption(FRStrategy.Force, "Use ASAP") + .AddOption(FRStrategy.Delay, "Do not use"); + return def; } @@ -41,7 +67,8 @@ public enum Form { None, OpoOpo, Raptor, Coeurl } public float FormLeft; // 0 if no form, 30 max public float BlitzLeft; // 20 max - public float PerfectBalanceLeft; // 20 max + public float PerfectBalanceLeft => PerfectBalance.Left; + public (float Left, int Stacks) PerfectBalance; public float FormShiftLeft; // 30 max public float BrotherhoodLeft; // 20 max public float FireLeft; // 20 max @@ -85,12 +112,67 @@ public enum Form { None, OpoOpo, Raptor, Coeurl } public bool CanFormShift => Unlocked(AID.FormShift) && PerfectBalanceLeft == 0; - public const int AOEBreakpoint = 4; + // TODO incorporate crit calculation - rockbreaker is a gain on 3 at 22.1% crit + public int AOEBreakpoint => EffectiveForm == Form.OpoOpo ? 3 : 4; public bool UseAOE => NumAOETargets >= AOEBreakpoint; - private (Positional, bool) NextPositional => UseAOE - ? (Positional.Any, false) - : (CoeurlStacks > 0 ? Positional.Flank : Positional.Rear, EffectiveForm == Form.Coeurl); + public int BuffedGCDsLeft => FireLeft > GCD ? (int)MathF.Floor((FireLeft - GCD) / AttackGCDLength) + 1 : 0; + public int PBGCDsLeft => PerfectBalance.Stacks + (NextChargeIn(AID.PerfectBalance) <= GCD ? 3 : 0); + + private (Positional, bool) NextPositional + { + get + { + if (UseAOE) + return (Positional.Any, false); + + var pos = CoeurlStacks > 0 ? Positional.Flank : Positional.Rear; + var imm = EffectiveForm == Form.Coeurl && NextGCD is not AID.WindsReply and not AID.FiresReply; + + return (pos, imm); + } + } + + public enum GCDPriority + { + None = -1, + Meditate = 50, + WindRanged = 100, + FireRanged = 200, + Basic = 300, + AOE = 400, + SSS = 500, + FiresReply = 700, + WindsReply = 800, + Blitz = 900, + MeditateForce = 950, + } + + // some monk OGCDs will be queued with higher prio than what user presses manually - the rotation is very drift-sensitive and monk has much less time to weave than other classes do + public enum OGCDPriority + { + None = -1, + TrueNorth = 100, + TFC = 150, + Potion = 200, + RiddleOfWind = 300, + ManualOGCD = 1901, // included for reference, not used here - actual value is 1901 + Low (2000) + 100 (in base class) = 4001 + RiddleOfFire = 1910, + Brotherhood = 1915, + PerfectBalance = 1920 + } + + private float GetApplicationDelay(AID action) => action switch + { + AID.SixSidedStar => 0.62f, + AID.DragonKick => 1.29f, + AID.ForbiddenChakra => 1.48f, + AID.Demolish => 1.60f, + // add more if needed + _ => 0 + }; + + public override string DescribeState() => $"F={BuffedGCDsLeft}, PB={PBGCDsLeft}"; public override void Exec(StrategyValues strategy, Actor? primaryTarget) { @@ -107,7 +189,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) RaptorStacks = gauge.RaptorStacks; CoeurlStacks = gauge.CoeurlStacks; - PerfectBalanceLeft = StatusLeft(SID.PerfectBalance); + PerfectBalance = Status(SID.PerfectBalance); FormShiftLeft = StatusLeft(SID.FormlessFist); FireLeft = StatusLeft(SID.RiddleOfFire); WindsReplyLeft = StatusLeft(SID.WindsRumination); @@ -141,7 +223,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) UpdatePositionals(primaryTarget, NextPositional, TrueNorthLeft > GCD); - OGCD(strategy, primaryTarget); + Meditate(strategy, primaryTarget); if (Chakra < 5 && Unlocked(AID.SteeledMeditation) && (!Player.InCombat || primaryTarget == null)) PushGCD(AID.SteeledMeditation, Player); @@ -151,64 +233,59 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (CountdownRemaining is > 2 and < 11.8f && FormShiftLeft == 0) PushGCD(AID.FormShift, Player); - if (CountdownRemaining < 0.4 && Player.DistanceToHitbox(primaryTarget) is > 3 and < 25) + if (CountdownRemaining < 1 && Player.DistanceToHitbox(primaryTarget) is > 3 and < 25) PushGCD(AID.Thunderclap, primaryTarget); + // uncomment/fix once we are able to manually delay starting autoattacks + //if (Player.DistanceToHitbox(primaryTarget) < 3 && CountdownRemaining < GetApplicationDelay(AID.DragonKick)) + //{ + // Hints.ForcedTarget = null; + // PushGCD(AID.DragonKick, primaryTarget); + //} + return; } if (NumBlitzTargets > 0) - PushGCD(currentBlitz, BestBlitzTarget); + PushGCD(currentBlitz, BestBlitzTarget, GCDPriority.Blitz); - // demo opener might be optimal sometimes - //if (FormShiftLeft > _state.GCD && CoeurlStacks == 0) - // PushGCD(AID.Demolish, primaryTarget); + FiresReply(strategy); + WindsReply(); - if (PerfectBalanceLeft == 0 && BlitzLeft == 0) - { - if (FormShiftLeft == 0 && FiresReplyLeft > GCD) - PushGCD(AID.FiresReply, BestRangedTarget); - - if (WindsReplyLeft > GCD) - PushGCD(AID.WindsReply, BestLineTarget); - } - - if (UseAOE && Unlocked(AID.ArmOfTheDestroyer)) + if (UseAOE) { if (EffectiveForm == Form.Coeurl) - PushGCD(AID.Rockbreaker, Player); + PushGCD(AID.Rockbreaker, Player, GCDPriority.AOE); if (EffectiveForm == Form.Raptor) - PushGCD(AID.FourPointFury, Player); + PushGCD(AID.FourPointFury, Player, GCDPriority.AOE); - PushGCD(AID.ArmOfTheDestroyer, Player); + PushGCD(AID.ArmOfTheDestroyer, Player, GCDPriority.AOE); } - else + + switch (EffectiveForm) { - switch (EffectiveForm) - { - case Form.Coeurl: - PushGCD(CoeurlStacks == 0 && Unlocked(AID.Demolish) ? AID.Demolish : AID.SnapPunch, primaryTarget); break; - case Form.Raptor: - PushGCD(RaptorStacks == 0 && Unlocked(AID.TwinSnakes) ? AID.TwinSnakes : AID.TrueStrike, primaryTarget); break; - default: - PushGCD(OpoStacks == 0 && Unlocked(AID.DragonKick) ? AID.DragonKick : AID.Bootshine, primaryTarget); break; - } + case Form.Coeurl: + PushGCD(CoeurlStacks == 0 && Unlocked(AID.Demolish) ? AID.Demolish : AID.SnapPunch, primaryTarget, GCDPriority.Basic); break; + case Form.Raptor: + PushGCD(RaptorStacks == 0 && Unlocked(AID.TwinSnakes) ? AID.TwinSnakes : AID.TrueStrike, primaryTarget, GCDPriority.Basic); break; + default: + PushGCD(OpoStacks == 0 && Unlocked(AID.DragonKick) ? AID.DragonKick : AID.Bootshine, primaryTarget, GCDPriority.Basic); break; } switch (strategy.Simple(Track.SSS)) { case OffensiveStrategy.Force: - PushGCD(AID.SixSidedStar, primaryTarget, 500); + PushGCD(AID.SixSidedStar, primaryTarget, GCDPriority.SSS); break; case OffensiveStrategy.Automatic: - if (!CanFitGCD(DowntimeIn - SSSApplicationDelay, 1)) - PushGCD(AID.SixSidedStar, primaryTarget, 500); + if (!CanFitGCD(DowntimeIn - GetApplicationDelay(AID.SixSidedStar), 1)) + PushGCD(AID.SixSidedStar, primaryTarget, GCDPriority.SSS); break; } - } - private const float SSSApplicationDelay = 0.62f; + OGCD(strategy, primaryTarget); + } private Form EffectiveForm { @@ -217,9 +294,12 @@ private Form EffectiveForm if (PerfectBalanceLeft == 0) return CurrentForm; - // hack: allow double lunar opener - only in boss fights - // trash packs should get regular lunar solar - var forceDoubleLunar = CombatTimer < 30 && !UseAOE; + // force lunar PB iff we are in opener, have lunar nadi already, and this is our last PB charge, aka double lunar opener + // if we have lunar but this is NOT our last charge, it means we came out of downtime with lunar nadi (i.e. dungeon), so solar -> pr is optimal + // this condition is unfortunately a little contrived. there are no other general cases in the monk rotation where we want to overwrite a lunar, as it's overall a dps loss + // NextChargeIn(PerfectBalance) > GCD is also not quite correct. ideally this would test whether a PB charge will come up during the riddle of fire window + // but in fights with extended downtime, nadis will already be explicitly planned out, so this isn't super important + var forceDoubleLunar = CombatTimer < 30 && HasLunar && NextChargeIn(AID.PerfectBalance) > GCD; var forcedSolar = ForcedSolar || HasLunar && !HasSolar && !forceDoubleLunar; var canCoeurl = forcedSolar; @@ -240,7 +320,7 @@ private Form EffectiveForm private void QueuePB(StrategyValues strategy) { - if (CurrentForm != Form.Raptor || BeastChakra[0] != BeastChakraType.None || FiresReplyLeft > GCD) + if (CurrentForm != Form.Raptor || BeastChakra[0] != BeastChakraType.None || NextGCD == AID.FiresReply) return; // prevent odd window double blitz @@ -249,7 +329,7 @@ private void QueuePB(StrategyValues strategy) return; if (CanWeave(AID.RiddleOfFire, 3) || CanFitGCD(FireLeft, 3)) - PushAction(AID.PerfectBalance, Player, ActionQueue.Priority.ManualOGCD + 1, 0); + PushOGCD(AID.PerfectBalance, Player, OGCDPriority.PerfectBalance); } private void OGCD(StrategyValues strategy, Actor? primaryTarget) @@ -258,37 +338,88 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) return; if (strategy.Option(Track.Potion).As() == PotionStrategy.Now) - Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, Player, ActionQueue.Priority.ManualOGCD - 1); + Potion(); if (strategy.BuffsOk()) { if (strategy.Option(Track.Potion).As() == PotionStrategy.PreBuffs && CanWeave(AID.Brotherhood, 4)) - Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, Player, ActionQueue.Priority.ManualOGCD - 1, GCD - 0.9f); + Potion(); QueuePB(strategy); if (CombatTimer >= 10 || BeastCount == 2) - PushAction(AID.Brotherhood, Player, ActionQueue.Priority.ManualOGCD + 3, 0); + PushOGCD(AID.Brotherhood, Player, OGCDPriority.Brotherhood); if (ShouldRoF) - PushAction(AID.RiddleOfFire, Player, ActionQueue.Priority.ManualOGCD + 2, GCD - 0.8f); + PushOGCD(AID.RiddleOfFire, Player, OGCDPriority.RiddleOfFire, GCD - EarliestRoF(AnimationLockDelay)); - if (CD(AID.RiddleOfFire) > 0) - PushOGCD(AID.RiddleOfWind, Player); + if (!CanWeave(AID.RiddleOfFire)) + PushOGCD(AID.RiddleOfWind, Player, OGCDPriority.RiddleOfWind); if (NextPositionalImminent && !NextPositionalCorrect) - PushOGCD(AID.TrueNorth, Player, delay: ShouldRoF ? 0 : GCD - 0.8f); + PushOGCD(AID.TrueNorth, Player, OGCDPriority.TrueNorth, ShouldRoF ? 0 : GCD - 0.8f); } if (Chakra >= 5) { if (NumLineTargets >= 3) - PushOGCD(AID.HowlingFist, BestLineTarget); + PushOGCD(AID.HowlingFist, BestLineTarget, OGCDPriority.TFC); - PushOGCD(AID.SteelPeak, primaryTarget); + PushOGCD(AID.SteelPeak, primaryTarget, OGCDPriority.TFC); } } + private void Meditate(StrategyValues strategy, Actor? primaryTarget) + { + if (Chakra >= 5 || !Unlocked(AID.SteeledMeditation)) + return; + + var prio = strategy.Option(Track.Meditation).As() switch + { + MeditationStrategy.Force => GCDPriority.MeditateForce, + MeditationStrategy.Safe => Player.InCombat && primaryTarget != null ? GCDPriority.None : GCDPriority.Meditate, + MeditationStrategy.Greedy => Player.DistanceToHitbox(primaryTarget) > 3 ? GCDPriority.Meditate : GCDPriority.None, + _ => GCDPriority.None, + }; + + PushGCD(AID.SteeledMeditation, Player, prio); + } + + private void FiresReply(StrategyValues strategy) + { + if (FiresReplyLeft <= GCD) + return; + + var prio = strategy.Option(Track.FiresReply).As() switch + { + FRStrategy.Automatic => CurrentForm == Form.Raptor ? GCDPriority.FiresReply : GCDPriority.None, + FRStrategy.Ranged => CanFitGCD(FiresReplyLeft, 1) ? GCDPriority.FireRanged : GCDPriority.FiresReply, + FRStrategy.Force => GCDPriority.FiresReply, + _ => GCDPriority.None + }; + + PushGCD(AID.FiresReply, BestRangedTarget, prio); + } + + private void WindsReply() + { + if (WindsReplyLeft <= GCD || PerfectBalanceLeft > GCD || BlitzLeft > GCD) + return; + + var prio = GCDPriority.WindRanged; + + // use early during buffs, or use now if about to expire + if (FireLeft > GCD || !CanFitGCD(WindsReplyLeft, 1)) + prio = GCDPriority.WindsReply; + + PushGCD(AID.WindsReply, BestLineTarget, prio); + } + + private float DesiredFireWindow => GCDLength * 10; + private float EarliestRoF(float estimatedDelay) => MathF.Max(estimatedDelay + 0.6f, 20.6f - DesiredFireWindow); + + private void Potion() => Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, Player, ActionQueue.Priority.Low + 100 + (float)OGCDPriority.Potion); + private bool ShouldRoF => CanWeave(AID.RiddleOfFire) && !CanWeave(AID.Brotherhood); private bool IsEnlightenmentTarget(Actor primary, Actor other) => Hints.TargetInAOERect(other, Player.Position, Player.DirectionTo(primary), 10, 2);