diff --git a/BossMod/Autorotation/Standard/xan/AI/DeepDungeon.cs b/BossMod/Autorotation/Standard/xan/AI/DeepDungeon.cs new file mode 100644 index 0000000000..379c9cb974 --- /dev/null +++ b/BossMod/Autorotation/Standard/xan/AI/DeepDungeon.cs @@ -0,0 +1,199 @@ +namespace BossMod.Autorotation.xan; + +public class DeepDungeonAI(RotationModuleManager manager, Actor player) : AIBase(manager, player) +{ + public enum Track { Potion, Kite } + + public static RotationModuleDefinition Definition() + { + var def = new RotationModuleDefinition("Deep Dungeon AI", "Utilities for deep dungeon - potion/pomander user", "AI (xan)", "xan", RotationModuleQuality.Basic, new BitMask(~0ul), 100, CanUseWhileRoleplaying: true); + + def.AbilityTrack(Track.Potion, "Potion"); + def.AbilityTrack(Track.Kite, "Kite enemies"); + + return def; + } + + enum OID : uint + { + Unei = 0x3E1A, + } + + enum Transformation : uint + { + None, + Manticore, + Succubus, + Kuribu, + Dreadnaught + } + + enum SID : uint + { + Transfiguration = 565, + ItemPenalty = 1094, + } + + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + { + if (World.DeepDungeon.DungeonId == 0) + return; + + var transformation = Transformation.None; + if (Player.FindStatus(SID.Transfiguration) is { } status) + { + transformation = (status.Extra & 0xFF) switch + { + 42 => Transformation.Manticore, + 43 => Transformation.Succubus, + 49 => Transformation.Kuribu, + 244 => Transformation.Dreadnaught, + _ => Transformation.None + }; + } + + if (transformation != Transformation.None) + { + DoTransformActions(strategy, primaryTarget, transformation); + return; + } + + if (IsRanged && !Player.InCombat && primaryTarget is Actor target && !target.InCombat && !target.IsAlly) + // bandaid fix to help deal with constant LOS issues + Hints.GoalZones.Add(Hints.GoalSingleTarget(target, 3, 0.1f)); + + SetupKiteZone(strategy, primaryTarget); + + if (Player.FindStatus(SID.ItemPenalty) != null) + return; + + var (regenAction, potAction) = World.DeepDungeon.DungeonId switch + { + DeepDungeonState.DungeonType.POTD => (ActionDefinitions.IDSustainingPotion, ActionDefinitions.IDMaxPotion), + DeepDungeonState.DungeonType.HOH => (ActionDefinitions.IDEmpyreanPotion, ActionDefinitions.IDSuperPotion), + DeepDungeonState.DungeonType.EO => (ActionDefinitions.IDOrthosPotion, ActionDefinitions.IDHyperPotion), + _ => (default, default) + }; + + if (regenAction != default && ShouldPotion(strategy)) + Hints.ActionsToExecute.Push(regenAction, Player, ActionQueue.Priority.Medium); + + if (potAction != default && Player.PredictedHPRatio <= 0.3f) + Hints.ActionsToExecute.Push(potAction, Player, ActionQueue.Priority.VeryHigh); + } + + private bool IsRanged => Player.Class.GetRole() is Role.Ranged or Role.Healer; + + private static readonly HashSet NoMeleeAutos = [ + // hoh + 0x22C3, // heavenly onibi + 0x22C5, // heavenly dhruva + 0x22C6, // heavenly sai taisui + 0x22DC, // heavenly dogu + 0x22DE, // heavenly ganseki + 0x22ED, // heavenly kongorei + 0x22EF, // heavenly maruishi + 0x22F3, // heavenly rachimonai + 0x22FC, // heavenly doguzeri + 0x2320, // heavenly nuppeppo (WHM) (uses stone) + + // orthos + 0x3DCC, // orthos imp + 0x3DCE, // orthos fachan + 0x3DD2, // orthos water sprite + 0x3DD4, // orthos microsystem + 0x3DD5, // orthosystem β + 0x3DE0, // orthodemolisher + 0x3DE2, // orthodroid + 0x3DFD, // orthos apa + 0x3E10, // orthos ice sprite + 0x3E5C, // orthos ahriman + 0x3E62, // orthos abyss + 0x3E63, // orthodrone + 0x3E64, // orthosystem γ + 0x3E66, // orthosystem α + ]; + + private void SetupKiteZone(StrategyValues strategy, Actor? primaryTarget) + { + if (!IsRanged || primaryTarget == null || !Player.InCombat || !strategy.Enabled(Track.Kite)) + return; + + // wew + if (NoMeleeAutos.Contains(primaryTarget.OID)) + return; + + // assume we don't need to kite if mob is busy casting (TODO: some mob spells can be cast while moving, maybe there's a column in sheets for it) + if (primaryTarget.CastInfo != null) + return; + + float maxRange = 25; + float maxKite = 9; + + var primaryPos = primaryTarget.Position; + var total = maxRange + Player.HitboxRadius + primaryTarget.HitboxRadius; + var totalKite = maxKite + Player.HitboxRadius + primaryTarget.HitboxRadius; + float goalFactor = 0.05f; + Hints.GoalZones.Add(pos => + { + var dist = (pos - primaryPos).Length(); + return dist <= total && dist >= totalKite ? goalFactor : 0; + }); + } + + private void DoTransformActions(StrategyValues strategy, Actor? primaryTarget, Transformation t) + { + if (primaryTarget == null) + return; + + Func goal; + ActionID attack; + int numTargets; + var castTime = 0f; + + switch (t) + { + case Transformation.Manticore: + goal = Hints.GoalSingleTarget(primaryTarget, 3); + numTargets = 1; + attack = ActionID.MakeSpell(Roleplay.AID.Pummel); + break; + case Transformation.Succubus: + goal = Hints.GoalSingleTarget(primaryTarget, 25); + numTargets = Hints.NumPriorityTargetsInAOECircle(primaryTarget.Position, 5); + attack = ActionID.MakeSpell(Roleplay.AID.VoidFireII); + castTime = 2.5f; + break; + case Transformation.Kuribu: + // heavenly judge is ground targeted + goal = Hints.GoalSingleTarget(primaryTarget.Position, 25); + numTargets = Hints.NumPriorityTargetsInAOECircle(primaryTarget.Position, 6); + attack = ActionID.MakeSpell(Roleplay.AID.HeavenlyJudge); + castTime = 2.5f; + break; + case Transformation.Dreadnaught: + goal = Hints.GoalSingleTarget(primaryTarget, 3); + numTargets = 1; + attack = ActionID.MakeSpell(Roleplay.AID.Rotosmash); + break; + default: + return; + } + + if (numTargets == 0) + return; + + Hints.GoalZones.Add(goal); + if (castTime == 0 || Hints.MaxCastTimeEstimate >= (castTime - 0.5f)) + Hints.ActionsToExecute.Push(attack, primaryTarget, ActionQueue.Priority.High, targetPos: primaryTarget.PosRot.XYZ()); + } + + private bool ShouldPotion(StrategyValues strategy) + { + if (World.Actors.Any(w => w.OID == (uint)OID.Unei) || !strategy.Enabled(Track.Potion)) + return false; + + var ratio = Player.ClassCategory is ClassCategory.Tank ? 0.4f : 0.6f; + return Player.PredictedHPRatio < ratio && Player.FindStatus(648) == null && Player.InCombat; + } +} diff --git a/BossMod/Autorotation/Standard/xan/AI/Healer.cs b/BossMod/Autorotation/Standard/xan/AI/Healer.cs index 8a93c7aeff..8a3b68cfa7 100644 --- a/BossMod/Autorotation/Standard/xan/AI/Healer.cs +++ b/BossMod/Autorotation/Standard/xan/AI/Healer.cs @@ -130,7 +130,7 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, private void UseGCD(AID action, Actor? target, int extraPriority = 0) where AID : Enum => UseGCD(ActionID.MakeSpell(action), target, extraPriority); private void UseGCD(ActionID action, Actor? target, int extraPriority = 0) - => Hints.ActionsToExecute.Push(action, target, ActionQueue.Priority.High + 500 + extraPriority); // TODO[cast-time]-xan: verify all callers + => Hints.ActionsToExecute.Push(action, target, ActionQueue.Priority.High + 500 + extraPriority); private void UseOGCD(AID action, Actor? target, int extraPriority = 0) where AID : Enum => UseOGCD(ActionID.MakeSpell(action), target, extraPriority); diff --git a/BossMod/Autorotation/Standard/xan/AI/Melee.cs b/BossMod/Autorotation/Standard/xan/AI/Melee.cs index f1ce2d73c7..9aadb095d1 100644 --- a/BossMod/Autorotation/Standard/xan/AI/Melee.cs +++ b/BossMod/Autorotation/Standard/xan/AI/Melee.cs @@ -25,7 +25,7 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.SecondWind), Player, ActionQueue.Priority.Medium); // bloodbath - if (strategy.Enabled(Track.Bloodbath) && Player.InCombat && Player.PredictedHPRatio <= 0.75) + if (strategy.Enabled(Track.Bloodbath) && Player.InCombat && Player.PredictedHPRatio <= 0.3) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Bloodbath), Player, ActionQueue.Priority.Medium); // low blow @@ -66,7 +66,7 @@ private void ExecLB(StrategyValues strategy, Actor? primaryTarget) case 1: break; case 2: - Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Bladedance), primaryTarget, ActionQueue.Priority.VeryHigh, castTime: 3); + Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Bladedance), primaryTarget, ActionQueue.Priority.VeryHigh); break; case 3: var lb3 = Player.Class switch @@ -80,7 +80,7 @@ private void ExecLB(StrategyValues strategy, Actor? primaryTarget) _ => default }; if (lb3 != default) - Hints.ActionsToExecute.Push(lb3, primaryTarget, ActionQueue.Priority.VeryHigh, castTime: 4.5f); + Hints.ActionsToExecute.Push(lb3, primaryTarget, ActionQueue.Priority.VeryHigh); break; } } diff --git a/BossMod/Autorotation/Standard/xan/AI/Ranged.cs b/BossMod/Autorotation/Standard/xan/AI/Ranged.cs index 08833bb73b..688d1f039e 100644 --- a/BossMod/Autorotation/Standard/xan/AI/Ranged.cs +++ b/BossMod/Autorotation/Standard/xan/AI/Ranged.cs @@ -46,11 +46,11 @@ private void ExecLB(StrategyValues strategy, Actor? primaryTarget) { case 1: if (lbTarget(2) is Actor a) - Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.BigShot), a, ActionQueue.Priority.VeryHigh, castTime: 2); + Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.BigShot), a, ActionQueue.Priority.VeryHigh); break; case 2: if (lbTarget(2.5f) is Actor b) - Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Desperado), b, ActionQueue.Priority.VeryHigh, castTime: 3); + Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Desperado), b, ActionQueue.Priority.VeryHigh); break; case 3: var lb3 = Player.Class switch @@ -61,7 +61,7 @@ private void ExecLB(StrategyValues strategy, Actor? primaryTarget) _ => default }; if (lbTarget(4) is Actor c && lb3 != default) - Hints.ActionsToExecute.Push(lb3, c, ActionQueue.Priority.VeryHigh, castTime: 4.5f); + Hints.ActionsToExecute.Push(lb3, c, ActionQueue.Priority.VeryHigh); break; } } diff --git a/BossMod/Autorotation/Standard/xan/Basexan.cs b/BossMod/Autorotation/Standard/xan/Basexan.cs index 51365875c1..fa6cabf586 100644 --- a/BossMod/Autorotation/Standard/xan/Basexan.cs +++ b/BossMod/Autorotation/Standard/xan/Basexan.cs @@ -1,4 +1,5 @@ -using static BossMod.AIHints; +using System.Diagnostics.CodeAnalysis; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -71,6 +72,15 @@ public bool CanWeave(AID aid, int extraGCDs = 0, float extraFixedDelay = 0) protected int NextGCDPrio; protected uint MP; + protected AID HighestUnlocked(params AID[] actions) + { + foreach (var act in actions) + if (Unlocked(act)) + return act; + + return default; + } + protected AID ComboLastMove => (AID)(object)World.Client.ComboState.Action; protected void PushGCD

(AID aid, Actor? target, P priority, float delay = 0) where P : Enum @@ -114,11 +124,11 @@ protected bool PushAction(AID aid, Actor? target, float priority, float delay) if ((uint)(object)aid == 0) return false; - if (!CanCast(aid)) // TODO[cast-time]-xan: don't do this, it's now not reliable, instead queue all cast options at different prios + if (!CanCast(aid)) return false; var def = ActionDefinitions.Instance.Spell(aid); - if (def == null) + if (def == null || !def.IsUnlocked(World, Player)) return false; if (def.Range != 0 && target == null) @@ -137,7 +147,7 @@ protected bool PushAction(AID aid, Actor? target, float priority, float delay) targetPos = target.PosRot.XYZ(); } - Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, priority, delay: delay, targetPos: targetPos); // TODO[cast-time]-xan: verify all callers + Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, priority, delay: delay, targetPos: targetPos); return true; } @@ -367,9 +377,14 @@ protected void UpdatePositionals(Enemy? enemy, ref (Positional pos, bool imm) po private void EstimateCastTime() { - MaxCastTime = Hints.MaxCastTime; // TODO[cast-time]-xan: this is now wrong, needs to be reviewed and fixed (queue all actions with casttime=time they need) + MaxCastTime = Hints.MaxCastTimeEstimate; + + if (Player.PendingKnockbacks.Count > 0) + { + MaxCastTime = 0f; + return; + } - // TODO[cast-time]-xan: this is not really correct in most cases: even if target is a gaze source, it's possible to start casting then rotate to be >45 && <75 degrees and finish cast successfully; the gaze avoidance tweak handles that var forbiddenDir = Hints.ForbiddenDirections.Where(d => Player.Rotation.AlmostEqual(d.center, d.halfWidth.Rad)).Select(d => d.activation).DefaultIfEmpty(DateTime.MinValue).Min(); if (forbiddenDir > World.CurrentTime) { @@ -379,12 +394,33 @@ private void EstimateCastTime() } } + private float? _prevCountdown; + private DateTime _cdLockout; + + [SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "determinism is intentional here")] + private void PretendCountdown() + { + if (CountdownRemaining == null) + { + _cdLockout = DateTime.MinValue; + _prevCountdown = null; + } + else if (_prevCountdown == null) + { + var wait = (float)new Random((int)World.Frame.Index).NextDouble() + 0.5f; + _cdLockout = World.FutureTime(wait); + _prevCountdown = CountdownRemaining; + } + } + public sealed override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { NextGCD = default; NextGCDPrio = 0; PlayerTarget = Hints.FindEnemy(primaryTarget); + PretendCountdown(); + var pelo = Player.FindStatus(ClassShared.SID.Peloton); PelotonLeft = pelo != null ? StatusDuration(pelo.Value.ExpireAt) : 0; SwiftcastLeft = MathF.Max(StatusLeft(ClassShared.SID.Swiftcast), StatusLeft(ClassShared.SID.LostChainspell)); @@ -410,6 +446,9 @@ public sealed override void Execute(StrategyValues strategy, ref Actor? primaryT // TODO max MP can be higher in eureka/bozja MP = (uint)Math.Clamp(Player.PredictedMPRaw, 0, 10000); + if (_cdLockout > World.CurrentTime) + return; + if (Player.MountId is not (103 or 117 or 128)) Exec(strategy, PlayerTarget); } diff --git a/BossMod/Autorotation/Standard/xan/Melee/DRG.cs b/BossMod/Autorotation/Standard/xan/Melee/DRG.cs index 7afa7f8bfb..2b6c719000 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/DRG.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/DRG.cs @@ -138,17 +138,17 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) break; case AID.Disembowel: case AID.SpiralBlow: - PushGCD(AID.ChaosThrust, primaryTarget); + PushGCD(HighestUnlocked(AID.ChaoticSpring, AID.ChaosThrust), primaryTarget); break; case AID.VorpalThrust: case AID.LanceBarrage: - PushGCD(AID.FullThrust, primaryTarget); + PushGCD(HighestUnlocked(AID.HeavensThrust, AID.FullThrust), primaryTarget); break; case AID.TrueThrust: case AID.RaidenThrust: if (PowerSurge < 10) - PushGCD(AID.Disembowel, primaryTarget); - PushGCD(AID.VorpalThrust, primaryTarget); + PushGCD(HighestUnlocked(AID.SpiralBlow, AID.Disembowel), primaryTarget); + PushGCD(HighestUnlocked(AID.LanceBarrage, AID.VorpalThrust), primaryTarget); break; } } @@ -167,11 +167,14 @@ private void OGCD(StrategyValues strategy, Enemy? primaryTarget) var moveOk = MoveOk(strategy); var posOk = PosLockOk(strategy); + var bestSingleTarget = primaryTarget.Priority >= 0 ? primaryTarget : null; + + bool posCheck(float animationLock) => Hints.PositionStoredIn > AnimLock + animationLock; if (NextPositionalImminent && !NextPositionalCorrect) Hints.ActionsToExecute.Push(ActionID.MakeSpell(AID.TrueNorth), Player, ActionQueue.Priority.Low - 20, delay: GCD - 0.8f); - if (strategy.BuffsOk()) + if (strategy.BuffsOk() && PowerSurge > GCD) { PushOGCD(AID.LanceCharge, Player); PushOGCD(AID.BattleLitany, Player); @@ -183,35 +186,43 @@ private void OGCD(StrategyValues strategy, Enemy? primaryTarget) if (CanWeave(AID.LanceCharge)) return; - if (LanceCharge > GCD && ShouldLifeSurge()) - PushOGCD(AID.LifeSurge, Player); - - if (StarcrossReady > 0) - PushOGCD(AID.Starcross, primaryTarget); - - if (LotD > AnimLock && moveOk) - PushOGCD(AID.Stardiver, BestDiveTarget); + if (ShouldWT(strategy)) + PushOGCD(AID.WyrmwindThrust, BestLongAOETarget); if (NastrondReady == 0) PushOGCD(AID.Geirskogul, BestLongAOETarget); - if (DiveReady == 0 && posOk) - PushOGCD(AID.Jump, primaryTarget); + if (DiveReady == 0 && posOk && posCheck(0.6f)) + PushOGCD(AID.Jump, bestSingleTarget); - if (moveOk && strategy.BuffsOk()) + if (LanceCharge > GCD && ShouldLifeSurge()) + PushOGCD(AID.LifeSurge, Player); + + if (moveOk && strategy.BuffsOk() && posCheck(0.8f)) PushOGCD(AID.DragonfireDive, BestDiveTarget); if (NastrondReady > 0) PushOGCD(AID.Nastrond, BestLongAOETarget); + if (LotD > AnimLock && moveOk && posCheck(1.5f)) + { + // stardiver: 1.5 + delay + // regular GCD: 0.6 + delay + // some conditions like DD haste (and maybe bozja?) can reduce GCD to 2.1s or lower, making stardiver weave impossible + if (GCDLength > 2.1f + 2 * AnimationLockDelay) + PushOGCD(AID.Stardiver, BestDiveTarget); + else if (GCD > 0) + PushGCD(AID.Stardiver, BestDiveTarget, 3); + } + + if (StarcrossReady > 0) + PushOGCD(AID.Starcross, BestDiveTarget); + if (DragonsFlight > 0) PushOGCD(AID.RiseOfTheDragon, BestDiveTarget); if (DiveReady > 0) - PushOGCD(AID.MirageDive, primaryTarget); - - if (Focus == 2) - PushOGCD(AID.WyrmwindThrust, BestLongAOETarget); + PushOGCD(AID.MirageDive, bestSingleTarget); } private bool ShouldLifeSurge() @@ -219,41 +230,24 @@ private bool ShouldLifeSurge() if (LifeSurge > 0) return false; - if (NumAOETargets > 2 && Unlocked(AID.DoomSpike)) - { - // coerthan torment is always our strongest aoe GCD (draconian fury just gives eyeball) - if (Unlocked(AID.CoerthanTorment)) - return ComboLastMove == AID.SonicThrust; - - if (Unlocked(AID.SonicThrust)) - return ComboLastMove == AID.DoomSpike; - - // doom spike is our only AOE skill at this level, always use - return true; - } - else + return NextGCD switch { - // 440 potency, level 64 - if (Unlocked(AID.Drakesbane)) - { - var ok = ComboLastMove is AID.WheelingThrust or AID.FangAndClaw; - - // also 440 potency, level 86 - if (Unlocked(AID.HeavensThrust)) - ok |= ComboLastMove is AID.VorpalThrust or AID.LanceBarrage; - - return ok; - } + // highest potency at max level (full thrust is still highest potency before it gets upgraded) + AID.CoerthanTorment or AID.Drakesbane or AID.HeavensThrust or AID.FullThrust => true, - // 380 potency, level 26 - if (Unlocked(AID.FullThrust)) - return ComboLastMove is AID.VorpalThrust; + // highest potency before Full Thrust is unlocked at 26 + AID.VorpalThrust => !Unlocked(AID.FullThrust), - // below level 26, strongest GCD is vorpal thrust - return ComboLastMove is AID.TrueThrust; - } + // fallbacks for AOE rotation + AID.SonicThrust => !Unlocked(AID.CoerthanTorment), + AID.DoomSpike => !Unlocked(AID.SonicThrust), + _ => false, + }; } + private bool ShouldWT(StrategyValues strategy) + => Focus == 2 && (LotD > AnimLock || NextGCD is AID.RaidenThrust or AID.DraconianFury); + private bool MoveOk(StrategyValues strategy) => strategy.Option(Track.Dive).As() == DiveStrategy.Allow; private bool PosLockOk(StrategyValues strategy) => strategy.Option(Track.Dive).As() != DiveStrategy.NoLock; diff --git a/BossMod/Autorotation/Standard/xan/Melee/MNK.cs b/BossMod/Autorotation/Standard/xan/Melee/MNK.cs index 99e33e370c..48fa9b7b7b 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/MNK.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/MNK.cs @@ -228,7 +228,7 @@ public enum Form { None, OpoOpo, Raptor, Coeurl } return (Positional.Any, false); var pos = Unlocked(AID.Demolish) && CoeurlStacks == 0 ? Positional.Rear : Positional.Flank; - var imm = EffectiveForm == Form.Coeurl && NextGCD is not AID.WindsReply and not AID.FiresReply; + var imm = NextGCD is AID.Demolish or AID.SnapPunch or AID.PouncingCoeurl; return (pos, imm); } @@ -328,9 +328,6 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) EffectiveForm = GetEffectiveForm(strategy); - var pos = NextPositional; - UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); - Meditate(strategy, primaryTarget); FormShift(strategy, primaryTarget); @@ -345,8 +342,6 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) return; } - GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.ArmOfTheDestroyer, AOEBreakpoint, positional: pos.Item1, maximumActionRange: 20); - UseBlitz(strategy, currentBlitz); FiresReply(strategy); WindsReply(); @@ -391,6 +386,11 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) Prep(strategy); + var pos = NextPositional; + UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); + + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.ArmOfTheDestroyer, AOEBreakpoint, positional: pos.Item1, maximumActionRange: 20); + if (Player.InCombat) OGCD(strategy, primaryTarget); } diff --git a/BossMod/Autorotation/Standard/xan/Ranged/MCH.cs b/BossMod/Autorotation/Standard/xan/Ranged/MCH.cs index 09de5caa3f..9e5c6daa19 100644 --- a/BossMod/Autorotation/Standard/xan/Ranged/MCH.cs +++ b/BossMod/Autorotation/Standard/xan/Ranged/MCH.cs @@ -95,13 +95,14 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) (BestChainsawTarget, NumSawTargets) = SelectTarget(strategy, primaryTarget, 25, Is25yRectTarget); NumFlamethrowerTargets = Hints.NumPriorityTargetsInAOECone(Player.Position, 8, Player.Rotation.ToDirection(), 45.Degrees()); - OGCD(strategy, primaryTarget); - if (IsPausedForFlamethrower) return; if (CountdownRemaining > 0) { + if (CountdownRemaining < 5 && ReassembleLeft == 0) + PushGCD(AID.Reassemble, Player); + if (CountdownRemaining < 1.15f) { PushGCD(AID.AirAnchor, primaryTarget); @@ -122,69 +123,66 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) if (FMFLeft > GCD) PushGCD(AID.FullMetalField, BestRangedAOETarget); - if (NumAOETargets > 3) + if (NumAOETargets > 2) PushGCD(AID.AutoCrossbow, BestAOETarget); - PushGCD(AID.HeatBlast, primaryTarget); - - // we don't use any other gcds during overheat - return; + PushGCD(HighestUnlocked(AID.BlazingShot, AID.HeatBlast), primaryTarget); } + else + { + if (ExcavatorLeft > GCD) + PushGCD(AID.Excavator, BestRangedAOETarget); - if (ExcavatorLeft > GCD) - PushGCD(AID.Excavator, BestRangedAOETarget); - - var toolOk = strategy.Simple(Track.Tools) != OffensiveStrategy.Delay; + var toolOk = strategy.Simple(Track.Tools) != OffensiveStrategy.Delay; - if (toolOk) - { - if (ReadyIn(AID.AirAnchor) <= GCD) - PushGCD(AID.AirAnchor, primaryTarget, priority: 20); + if (toolOk) + { + if (ReadyIn(AID.AirAnchor) <= GCD) + PushGCD(AID.AirAnchor, primaryTarget, priority: 20); - if (ReadyIn(AID.ChainSaw) <= GCD) - PushGCD(AID.ChainSaw, BestChainsawTarget, 10); + if (ReadyIn(AID.ChainSaw) <= GCD) + PushGCD(AID.ChainSaw, BestChainsawTarget, 10); - if (ReadyIn(AID.Bioblaster) <= GCD && NumAOETargets > 2) - PushGCD(AID.Bioblaster, BestAOETarget, priority: MaxChargesIn(AID.Bioblaster) <= GCD ? 20 : 2); + if (ReadyIn(AID.Bioblaster) <= GCD && NumAOETargets > 1) + PushGCD(AID.Bioblaster, BestAOETarget, priority: MaxChargesIn(AID.Bioblaster) <= GCD ? 20 : 2); - if (ReadyIn(AID.Drill) <= GCD) - PushGCD(AID.Drill, primaryTarget, priority: MaxChargesIn(AID.Drill) <= GCD ? 20 : 2); - } + if (ReadyIn(AID.Drill) <= GCD) + PushGCD(AID.Drill, primaryTarget, priority: MaxChargesIn(AID.Drill) <= GCD ? 20 : 2); - // TODO work out priorities - if (FMFLeft > GCD && ExcavatorLeft == 0) - PushGCD(AID.FullMetalField, BestRangedAOETarget); + // different cdgroup fsr + if (!Unlocked(AID.AirAnchor) && ReadyIn(AID.HotShot) <= GCD) + PushGCD(AID.HotShot, primaryTarget); + } - if (ReassembleLeft > GCD && NumAOETargets > 3) - PushGCD(AID.Scattergun, BestAOETarget); + // TODO work out priorities + if (FMFLeft > GCD && ExcavatorLeft == 0) + PushGCD(AID.FullMetalField, BestRangedAOETarget); - // different cdgroup? - if (!Unlocked(AID.AirAnchor) && ReadyIn(AID.HotShot) <= GCD && toolOk) - PushGCD(AID.HotShot, primaryTarget); + var breakpoint = Unlocked(AID.Scattergun) ? 2 : 1; - if (NumAOETargets > 2 && Unlocked(AID.SpreadShot)) - PushGCD(AID.SpreadShot, BestAOETarget); - else - { - if (ComboLastMove == AID.SlugShot) - PushGCD(AID.CleanShot, primaryTarget); + if (NumAOETargets > breakpoint && Unlocked(AID.SpreadShot)) + PushGCD(HighestUnlocked(AID.Scattergun, AID.SpreadShot), BestAOETarget); + else + { + if (ComboLastMove == AID.SlugShot) + PushGCD(HighestUnlocked(AID.HeatedCleanShot, AID.CleanShot), primaryTarget); - if (ComboLastMove == AID.SplitShot) - PushGCD(AID.SlugShot, primaryTarget); + if (ComboLastMove == AID.SplitShot) + PushGCD(HighestUnlocked(AID.HeatedSlugShot, AID.SlugShot), primaryTarget); - PushGCD(AID.SplitShot, primaryTarget); + PushGCD(HighestUnlocked(AID.HeatedSplitShot, AID.SplitShot), primaryTarget); + } } + + OGCD(strategy, primaryTarget); } private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { - if (CountdownRemaining is > 0 and < 5 && ReassembleLeft == 0) - PushOGCD(AID.Reassemble, Player); - - if (CountdownRemaining == null && !Player.InCombat && Player.DistanceToHitbox(primaryTarget) <= 25 && NextToolCharge == 0 && ReassembleLeft == 0 && !Overheated) - PushGCD(AID.Reassemble, Player, 30); + if (CountdownRemaining == null && !Player.InCombat && Player.DistanceToHitbox(primaryTarget) <= 25 && ReassembleLeft == 0 && !Overheated && AlwaysReassemble(NextGCD)) + PushGCD(AID.Reassemble, Player, priority: 50); - if (IsPausedForFlamethrower || !Player.InCombat || primaryTarget == null) + if (!Player.InCombat || primaryTarget == null) return; if (ShouldWildfire(strategy)) @@ -238,31 +236,42 @@ private bool ShouldReassemble(StrategyValues strategy, Enemy? primaryTarget) if (ReassembleLeft > 0 || !Unlocked(AID.Reassemble) || Overheated || primaryTarget == null) return false; - if (NumAOETargets > 3 && Unlocked(AID.SpreadShot)) + if (AlwaysReassemble(NextGCD)) return true; - if (RaidBuffsIn < 10 && RaidBuffsIn > GCD) - return false; - - if (!Unlocked(AID.Drill)) - return ComboLastMove == (Unlocked(AID.CleanShot) ? AID.SlugShot : AID.SplitShot); + return NextGCD switch + { + // AOE actions (TODO review usage, the wording in the balance guide is contradictory) + AID.SpreadShot or AID.Scattergun or AID.AutoCrossbow => true, + // highest potency before 58 + AID.CleanShot => !Unlocked(AID.Drill), + // highest potency before 26 + AID.HotShot => !Unlocked(AID.CleanShot), + + _ => false + }; + } - // past 58 we only reassemble on tool charges so don't bother - if (strategy.Simple(Track.Tools) == OffensiveStrategy.Delay) - return false; + private bool AlwaysReassemble(AID action) => action is AID.Drill or AID.AirAnchor or AID.ChainSaw or AID.Excavator; - return NextToolCharge <= GCD; - } + private int BatteryFromAction(AID action) => action switch + { + AID.ChainSaw or AID.AirAnchor or AID.Excavator or AID.HotShot => 20, + AID.CleanShot or AID.HeatedCleanShot => 10, + _ => 0 + }; private bool ShouldMinion(StrategyValues strategy, Enemy? primaryTarget) { if (!Unlocked(AID.RookAutoturret) || primaryTarget == null || HasMinion || Battery < 50 || ShouldWildfire(strategy)) return false; + var almostFull = Battery == 90 && BatteryFromAction(NextGCD) == 20; + return strategy.Option(Track.Queen).As() switch { QueenStrategy.MinGauge => true, - QueenStrategy.FullGauge => Battery == 100, + QueenStrategy.FullGauge => Battery == 100 || almostFull, // allow early summon, queen doesn't start autoing for 5 seconds QueenStrategy.RaidBuffsOnly => RaidBuffsLeft > 10 || RaidBuffsIn < 5, _ => false, diff --git a/BossMod/Autorotation/Standard/xan/Tanks/PLD.cs b/BossMod/Autorotation/Standard/xan/Tanks/PLD.cs index 35ec9d45fa..5ceaa1fccb 100644 --- a/BossMod/Autorotation/Standard/xan/Tanks/PLD.cs +++ b/BossMod/Autorotation/Standard/xan/Tanks/PLD.cs @@ -229,7 +229,7 @@ private void UseHS(StrategyValues strategy, Enemy? primaryTarget) var prio = strategy.Option(Track.HolySpirit).As() switch { - HSStrategy.Standard => useStandard ? GCDPriority.DMHS : GCDPriority.None, + HSStrategy.Standard => useStandard ? GCDPriority.DMHS : divineMight ? GCDPriority.HS : GCDPriority.None, HSStrategy.ForceDM => divineMight ? GCDPriority.Force : GCDPriority.None, HSStrategy.Force => GCDPriority.Force, HSStrategy.Ranged => useStandard ? GCDPriority.DMHS : GCDPriority.HS,