From 1c19ab932d968f21cc441968ce173badae75fcac Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:38:13 -0400 Subject: [PATCH 1/3] ma cheenist --- BossMod/ActionQueue/Ranged/MCH.cs | 132 +++++--- .../ActionTweaks/ClassActions/MCHConfig.cs | 8 + BossMod/Autorotation/xan/DNC.cs | 6 +- BossMod/Autorotation/xan/MCH.cs | 311 ++++++++++++++++++ BossMod/Autorotation/xan/xbase.cs | 56 ++-- BossMod/Data/Actor.cs | 2 + 6 files changed, 438 insertions(+), 77 deletions(-) create mode 100644 BossMod/ActionTweaks/ClassActions/MCHConfig.cs create mode 100644 BossMod/Autorotation/xan/MCH.cs diff --git a/BossMod/ActionQueue/Ranged/MCH.cs b/BossMod/ActionQueue/Ranged/MCH.cs index 77fdf068a..e6118cdf8 100644 --- a/BossMod/ActionQueue/Ranged/MCH.cs +++ b/BossMod/ActionQueue/Ranged/MCH.cs @@ -5,46 +5,51 @@ public enum AID : uint None = 0, Sprint = ClassShared.AID.Sprint, - SatelliteBeam = 4245, // LB3, 4.5s cast, range 30, AOE 30+R width 8 rect, targets=hostile, animLock=???, castAnimLock=3.700 - SplitShot = 2866, // L1, instant, GCD, range 25, single-target, targets=hostile, animLock=??? - SlugShot = 2868, // L2, instant, GCD, range 25, single-target, targets=hostile, animLock=??? - HotShot = 2872, // L4, instant, 40.0s CD (group 7/57), range 25, single-target, targets=hostile - Reassemble = 2876, // L10, instant, 55.0s CD (group 21/72), range 0, single-target, targets=self - GaussRound = 2874, // L15, instant, 30.0s CD (group 9/70) (2? charges), range 25, single-target, targets=hostile - SpreadShot = 2870, // L18, instant, GCD, range 12, AOE 12+R ?-degree cone, targets=hostile, animLock=??? - CleanShot = 2873, // L26, instant, GCD, range 25, single-target, targets=hostile, animLock=??? - Hypercharge = 17209, // L30, instant, 10.0s CD (group 4), range 0, single-target, targets=self - HeatBlast = 7410, // L35, instant, GCD, range 25, single-target, targets=hostile - RookAutoturret = 2864, // L40, instant, 6.0s CD (group 3), range 0, single-target, targets=self - RookOverdrive = 7415, // L40, instant, 15.0s CD (group 3), range 25, single-target, targets=self, animLock=??? - Detonator = 16766, // L45, instant, 1.0s CD (group 0), range 25, single-target, targets=self - Wildfire = 2878, // L45, instant, 120.0s CD (group 19), range 25, single-target, targets=hostile - Ricochet = 2890, // L50, instant, 30.0s CD (group 10/71) (2? charges), range 25, AOE 5 circle, targets=hostile - AutoCrossbow = 16497, // L52, instant, GCD, range 12, AOE 12+R ?-degree cone, targets=hostile - HeatedSplitShot = 7411, // L54, instant, GCD, range 25, single-target, targets=hostile - Tactician = 16889, // L56, instant, 120.0s CD (group 23), range 0, AOE 30 circle, targets=self - Drill = 16498, // L58, instant, 20.0s CD (group 6/57), range 25, single-target, targets=hostile - HeatedSlugShot = 7412, // L60, instant, GCD, range 25, single-target, targets=hostile - Dismantle = 2887, // L62, instant, 120.0s CD (group 18), range 25, single-target, targets=hostile - HeatedCleanShot = 7413, // L64, instant, GCD, range 25, single-target, targets=hostile - BarrelStabilizer = 7414, // L66, instant, 120.0s CD (group 20), range 0, single-target, targets=self - Flamethrower = 7418, // L70, instant, 60.0s CD (group 12/57), range 0, single-target, targets=self - Bioblaster = 16499, // L72, instant, 20.0s CD (group 6/57), range 12, AOE 12+R ?-degree cone, targets=hostile - AirAnchor = 16500, // L76, instant, 40.0s CD (group 8/57), range 25, single-target, targets=hostile - QueenOverdrive = 16502, // L80, instant, 15.0s CD (group 1), range 30, single-target, targets=self - AutomatonQueen = 16501, // L80, instant, 6.0s CD (group 1), range 0, single-target, targets=self - Scattergun = 25786, // L82, instant, GCD, range 12, AOE 12+R ?-degree cone, targets=hostile - ChainSaw = 25788, // L90, instant, 60.0s CD (group 11/57), range 25, AOE 25+R width 4 rect, targets=hostile + SatelliteBeam = 4245, // LB3, 4.5s cast, range 30, AOE 30+R width 8 rect, targets=Hostile, animLock=3.700s? + SplitShot = 2866, // L1, instant, GCD, range 25, single-target, targets=Hostile + SlugShot = 2868, // L2, instant, GCD, range 25, single-target, targets=Hostile + HotShot = 2872, // L4, instant, 40.0s CD (group 7/57), range 25, single-target, targets=Hostile + Reassemble = 2876, // L10, instant, 55.0s CD (group 17/72), range 0, single-target, targets=Self + GaussRound = 2874, // L15, instant, 30.0s CD (group 14/70) (2 charges), range 25, single-target, targets=Hostile + SpreadShot = 2870, // L18, instant, GCD, range 12, AOE 12+R ?-degree cone, targets=Hostile + CleanShot = 2873, // L26, instant, GCD, range 25, single-target, targets=Hostile + Hypercharge = 17209, // L30, instant, 10.0s CD (group 1), range 0, single-target, targets=Self + HeatBlast = 7410, // L35, instant, GCD, range 25, single-target, targets=Hostile + RookAutoturret = 2864, // L40, instant, 6.0s CD (group 2), range 0, single-target, targets=Self + RookOverdrive = 7415, // L40, instant, 15.0s CD (group 3), range 25, single-target, targets=Self + Detonator = 16766, // L45, instant, 1.0s CD (group 0), range 25, single-target, targets=Self + Wildfire = 2878, // L45, instant, 120.0s CD (group 19), range 25, single-target, targets=Hostile + Ricochet = 2890, // L50, instant, 30.0s CD (group 15/71) (2 charges), range 25, AOE 5 circle, targets=Hostile + AutoCrossbow = 16497, // L52, instant, GCD, range 12, AOE 12+R ?-degree cone, targets=Hostile + HeatedSplitShot = 7411, // L54, instant, GCD, range 25, single-target, targets=Hostile + Tactician = 16889, // L56, instant, 120.0s CD (group 21), range 0, AOE 30 circle, targets=Self + Drill = 16498, // L58, instant, 20.0s CD (group 4/57), range 25, single-target, targets=Hostile + HeatedSlugShot = 7412, // L60, instant, GCD, range 25, single-target, targets=Hostile + Dismantle = 2887, // L62, instant, 120.0s CD (group 18), range 25, single-target, targets=Hostile + HeatedCleanShot = 7413, // L64, instant, GCD, range 25, single-target, targets=Hostile + BarrelStabilizer = 7414, // L66, instant, 120.0s CD (group 20), range 0, single-target, targets=Self + BlazingShot = 36978, // L68, instant, GCD, range 25, single-target, targets=Hostile + Flamethrower = 7418, // L70, instant, 60.0s CD (group 12/57), range 0, single-target, targets=Self + Bioblaster = 16499, // L72, instant, 20.0s CD (group 4/57), range 12, AOE 12+R ?-degree cone, targets=Hostile + AirAnchor = 16500, // L76, instant, 40.0s CD (group 8/57), range 25, single-target, targets=Hostile + AutomatonQueen = 16501, // L80, instant, 6.0s CD (group 2), range 0, single-target, targets=Self + QueenOverdrive = 16502, // L80, instant, 15.0s CD (group 3), range 30, single-target, targets=Self + Scattergun = 25786, // L82, instant, GCD, range 12, AOE 12+R ?-degree cone, targets=Hostile + ChainSaw = 25788, // L90, instant, 60.0s CD (group 11/57), range 25, AOE 25+R width 4 rect, targets=Hostile + DoubleCheck = 36979, // L92, instant, 30.0s CD (group 14/70) (3? charges), range 25, AOE 5 circle, targets=Hostile, animLock=??? + Checkmate = 36980, // L92, instant, 30.0s CD (group 15/71) (3? charges), range 25, AOE 5 circle, targets=Hostile, animLock=??? + Excavator = 36981, // L96, instant, GCD, range 25, AOE 5 circle, targets=Hostile, animLock=??? + FullMetalField = 36982, // L100, instant, GCD, range 25, AOE 5 circle, targets=Hostile, animLock=??? // Shared - BigShot = ClassShared.AID.BigShot, // LB1, 2.0s cast, range 30, AOE 30+R width 4 rect, targets=hostile, castAnimLock=3.100 - Desperado = ClassShared.AID.Desperado, // LB2, 3.0s cast, range 30, AOE 30+R width 5 rect, targets=hostile, castAnimLock=3.100 - LegGraze = ClassShared.AID.LegGraze, // L6, instant, 30.0s CD (group 42), range 25, single-target, targets=hostile - SecondWind = ClassShared.AID.SecondWind, // L8, instant, 120.0s CD (group 49), range 0, single-target, targets=self - FootGraze = ClassShared.AID.FootGraze, // L10, instant, 30.0s CD (group 41), range 25, single-target, targets=hostile - Peloton = ClassShared.AID.Peloton, // L20, instant, 5.0s CD (group 40), range 0, AOE 30 circle, targets=self - HeadGraze = ClassShared.AID.HeadGraze, // L24, instant, 30.0s CD (group 43), range 25, single-target, targets=hostile - ArmsLength = ClassShared.AID.ArmsLength, // L32, instant, 120.0s CD (group 48), range 0, single-target, targets=self + BigShot = ClassShared.AID.BigShot, // LB1, 2.0s cast, range 30, AOE 30+R width 4 rect, targets=Hostile, animLock=3.100s? + Desperado = ClassShared.AID.Desperado, // LB2, 3.0s cast, range 30, AOE 30+R width 5 rect, targets=Hostile, animLock=3.100s? + LegGraze = ClassShared.AID.LegGraze, // L6, instant, 30.0s CD (group 42), range 25, single-target, targets=Hostile + SecondWind = ClassShared.AID.SecondWind, // L8, instant, 120.0s CD (group 49), range 0, single-target, targets=Self + FootGraze = ClassShared.AID.FootGraze, // L10, instant, 30.0s CD (group 41), range 25, single-target, targets=Hostile + Peloton = ClassShared.AID.Peloton, // L20, instant, 5.0s CD (group 40), range 0, AOE 30 circle, targets=Self + HeadGraze = ClassShared.AID.HeadGraze, // L24, instant, 30.0s CD (group 43), range 25, single-target, targets=Hostile + ArmsLength = ClassShared.AID.ArmsLength, // L32, instant, 120.0s CD (group 48), range 0, single-target, targets=Self } public enum TraitID : uint @@ -55,51 +60,78 @@ public enum TraitID : uint SplitShotMastery = 288, // L54 SlugShotMastery = 289, // L60 CleanShotMastery = 290, // L64 + HeatBlastMastery = 603, // L68 ChargedActionMastery = 292, // L74 HotShotMastery = 291, // L76 EnhancedWildfire = 293, // L78 Promotion = 294, // L80 SpreadShotMastery = 449, // L82 - EnhancedReassemble = 450, // L84 MarksmansMastery = 517, // L84 + EnhancedReassemble = 450, // L84 QueensGambit = 451, // L86 EnhancedTactician = 452, // L88 + DoubleBarrelMastery = 604, // L92 + EnhancedSecondWind = 642, // L94 + EnhancedMultiweapon = 605, // L94 + MarksmansMasteryII = 658, // L94 + EnhancedMultiweaponII = 606, // L96 + EnhancedTacticianII = 607, // L98 + EnhancedBarrelStabilizer = 608, // L100 +} + +public enum SID : uint +{ + None = 0, + Reassembled = 851, // applied by Reassemble to self + Overheated = 2688, // applied by Hypercharge to self + WildfirePlayer = 1946, // applied by Wildfire to self + WildfireTarget = 861, // applied by Wildfire to target + Dismantled = 860, // applied by Dismantle to target + Hypercharged = 3864, // applied by Barrel Stabilizer to self + Flamethrower = 1205, // applied by Flamethrower to self + ExcavatorReady = 3865, // applied by Chain Saw to self + FullMetalMachinist = 3866, // applied by Hypercharge to self } public sealed class Definitions : IDisposable { public Definitions(ActionDefinitions d) { - d.RegisterSpell(AID.SatelliteBeam, true, castAnimLock: 3.70f); // animLock=???, castAnimLock=3.700 - d.RegisterSpell(AID.SplitShot, true); // animLock=??? - d.RegisterSpell(AID.SlugShot, true); // animLock=??? + d.RegisterSpell(AID.SatelliteBeam, true, castAnimLock: 3.70f); // animLock=3.700s? + d.RegisterSpell(AID.SplitShot, true); + d.RegisterSpell(AID.SlugShot, true); d.RegisterSpell(AID.HotShot, true); d.RegisterSpell(AID.Reassemble, true); - d.RegisterSpell(AID.GaussRound, true, maxCharges: 2); - d.RegisterSpell(AID.SpreadShot, true); // animLock=??? - d.RegisterSpell(AID.CleanShot, true); // animLock=??? + d.RegisterSpell(AID.GaussRound, true, maxCharges: 3); + d.RegisterSpell(AID.SpreadShot, true); + d.RegisterSpell(AID.CleanShot, true); d.RegisterSpell(AID.Hypercharge, true); d.RegisterSpell(AID.HeatBlast, true); d.RegisterSpell(AID.RookAutoturret, true); - d.RegisterSpell(AID.RookOverdrive, true); // animLock=??? + d.RegisterSpell(AID.RookOverdrive, true); d.RegisterSpell(AID.Detonator, true); d.RegisterSpell(AID.Wildfire, true); - d.RegisterSpell(AID.Ricochet, true, maxCharges: 2); + d.RegisterSpell(AID.Ricochet, true, maxCharges: 3); d.RegisterSpell(AID.AutoCrossbow, true); d.RegisterSpell(AID.HeatedSplitShot, true); d.RegisterSpell(AID.Tactician, true); - d.RegisterSpell(AID.Drill, true); + d.RegisterSpell(AID.Drill, true, maxCharges: 2); d.RegisterSpell(AID.HeatedSlugShot, true); d.RegisterSpell(AID.Dismantle, true); d.RegisterSpell(AID.HeatedCleanShot, true); d.RegisterSpell(AID.BarrelStabilizer, true); + d.RegisterSpell(AID.BlazingShot, true); d.RegisterSpell(AID.Flamethrower, true); - d.RegisterSpell(AID.Bioblaster, true); + d.RegisterSpell(AID.Bioblaster, true, maxCharges: 2); d.RegisterSpell(AID.AirAnchor, true); - d.RegisterSpell(AID.QueenOverdrive, true); d.RegisterSpell(AID.AutomatonQueen, true); + d.RegisterSpell(AID.QueenOverdrive, true); d.RegisterSpell(AID.Scattergun, true); d.RegisterSpell(AID.ChainSaw, true); + d.RegisterSpell(AID.DoubleCheck, true, maxCharges: 3); // animLock=??? + d.RegisterSpell(AID.Checkmate, true, maxCharges: 3); // animLock=??? + d.RegisterSpell(AID.Excavator, true); // animLock=??? + d.RegisterSpell(AID.FullMetalField, true); // animLock=??? Customize(d); } diff --git a/BossMod/ActionTweaks/ClassActions/MCHConfig.cs b/BossMod/ActionTweaks/ClassActions/MCHConfig.cs new file mode 100644 index 000000000..6caad86bf --- /dev/null +++ b/BossMod/ActionTweaks/ClassActions/MCHConfig.cs @@ -0,0 +1,8 @@ +namespace BossMod; + +[ConfigDisplay(Parent = typeof(ActionTweaksConfig))] +class MCHConfig : ConfigNode +{ + [PropertyDisplay("Pause autorotation while channeling Flamethrower")] + public bool PauseForFlamethrower = false; +} diff --git a/BossMod/Autorotation/xan/DNC.cs b/BossMod/Autorotation/xan/DNC.cs index 0ff0739ae..5b91246e6 100644 --- a/BossMod/Autorotation/xan/DNC.cs +++ b/BossMod/Autorotation/xan/DNC.cs @@ -274,7 +274,7 @@ private bool ShouldSpendFeathers(StrategyValues strategy) public override void Exec(StrategyValues strategy, Actor? primaryTarget) { - var targeting = strategy.Option(Track.Targeting); + var targeting = strategy.Option(Track.Targeting).As(); SelectPrimaryTarget(targeting, ref primaryTarget, range: 25); _state.UpdateCommon(primaryTarget); @@ -324,10 +324,10 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.ClosedPosition, partner); CalcNextBestGCD(strategy, primaryTarget); - QueueOGCD((deadline, _) => CalcNextBestOGCD(strategy, primaryTarget, deadline)); + QueueOGCD(deadline => CalcNextBestOGCD(strategy, primaryTarget, deadline)); } - private bool IsFan4Target(Actor primary, Actor other) => Hints.TargetInAOECone(other, Player.Position, 15, (primary.Position - Player.Position).Normalized(), 60.Degrees()); + private bool IsFan4Target(Actor primary, Actor other) => Hints.TargetInAOECone(other, Player.Position, 15, Player.DirectionTo(primary), 60.Degrees()); private Actor? FindDancePartner() => World.Party.WithoutSlot().Exclude(Player).MaxBy(p => p.Class switch { diff --git a/BossMod/Autorotation/xan/MCH.cs b/BossMod/Autorotation/xan/MCH.cs new file mode 100644 index 000000000..b7bbeb03a --- /dev/null +++ b/BossMod/Autorotation/xan/MCH.cs @@ -0,0 +1,311 @@ +using BossMod.MCH; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; + +namespace BossMod.Autorotation.xan; +public sealed class MCH(RotationModuleManager manager, Actor player) : xbase(manager, player) +{ + public enum Track { AOE, Targeting, Buffs } + + public static RotationModuleDefinition Definition() + { + var def = new RotationModuleDefinition("MCH", "Machinist", "xan", RotationModuleQuality.Basic, BitMask.Build(Class.MCH), 100); + + def.DefineAOE(Track.AOE); + def.DefineTargeting(Track.Targeting); + def.DefineSimple(Track.Buffs, "Buffs").AddAssociatedActions(AID.BarrelStabilizer, AID.Wildfire); + + return def; + } + + public int Heat; // max 100 + public int Battery; // max 100 + public float OverheatLeft; // max 10s + public bool Overheated; + public bool HasMinion; + + public float ReassembleLeft; // max 5s + public float WildfireLeft; // max 10s + public float HyperchargedLeft; // max 30s + public float ExcavatorLeft; // max 30s + public float FMFLeft; // max 30s + + public bool Flamethrower; + + public int NumAOETargets; + public int NumRangedAOETargets; + public int NumSawTargets; + public int NumFlamethrowerTargets; + + private Actor? BestAOETarget; + private Actor? BestRangedAOETarget; + private Actor? BestChainsawTarget; + + private bool IsPausedForFlamethrower => Service.Config.Get().PauseForFlamethrower && Flamethrower; + + private void CalcNextBestGCD(StrategyValues strategy, Actor? primaryTarget) + { + if (IsPausedForFlamethrower) + return; + + if (_state.CountdownRemaining > 0) + { + if (_state.CountdownRemaining < 0.4f && Unlocked(AID.AirAnchor)) + PushGCD(AID.AirAnchor, primaryTarget); + + return; + } + + if (Overheated) + { + if (FMFLeft > _state.GCD) + PushGCD(AID.FullMetalField, BestRangedAOETarget); + + if (NumAOETargets > 3 && Unlocked(AID.AutoCrossbow)) + PushGCD(AID.AutoCrossbow, BestAOETarget); + + if (Unlocked(AID.HeatBlast)) + PushGCD(AID.HeatBlast, primaryTarget); + + // we don't use any other gcds during overheat + return; + } + + if (ExcavatorLeft > _state.GCD) + PushGCD(AID.Excavator, BestRangedAOETarget); + + if (Unlocked(AID.AirAnchor) && _state.CD(AID.AirAnchor) <= _state.GCD) + PushGCD(AID.AirAnchor, primaryTarget, 20); + + if (Unlocked(AID.ChainSaw) && _state.CD(AID.ChainSaw) <= _state.GCD) + PushGCD(AID.ChainSaw, BestChainsawTarget, 10); + + if (Unlocked(AID.Drill) && _state.CD(AID.Drill) - 20 <= _state.GCD) + { + if (Unlocked(AID.Bioblaster) && NumAOETargets > 2) + PushGCD(AID.Bioblaster, BestAOETarget); + + PushGCD(AID.Drill, primaryTarget, _state.CD(AID.Drill) <= _state.GCD ? 20 : 0); + } + + // TODO work out priorities + if (FMFLeft > _state.GCD && ExcavatorLeft == 0) + PushGCD(AID.FullMetalField, BestRangedAOETarget); + + if (ReassembleLeft > _state.GCD && NumAOETargets > 3) + PushGCD(AID.Scattergun, BestAOETarget); + + if (!Unlocked(AID.AirAnchor) && Unlocked(AID.HotShot) && _state.CD(AID.HotShot) <= _state.GCD) + PushGCD(AID.HotShot, primaryTarget); + + if (NumAOETargets > 2 && Unlocked(AID.SpreadShot)) + { + if (NumFlamethrowerTargets > 2 && Unlocked(AID.Flamethrower) && _state.CD(AID.Flamethrower) < _state.GCD) + { + PushGCD(AID.Flamethrower, Player); + return; + } + + PushGCD(AID.SpreadShot, BestAOETarget); + } + else + { + if ((AID)_state.ComboLastAction == AID.SlugShot && Unlocked(AID.CleanShot)) + PushGCD(AID.CleanShot, primaryTarget); + + if ((AID)_state.ComboLastAction == AID.SplitShot && Unlocked(AID.SlugShot)) + PushGCD(AID.SlugShot, primaryTarget); + + PushGCD(AID.SplitShot, primaryTarget); + } + } + + private void CalcNextBestOGCD(StrategyValues strategy, Actor? primaryTarget, float deadline) + { + if (_state.CountdownRemaining is > 0 and < 5 && ReassembleLeft == 0 && _state.CD(AID.Reassemble) < 55) + PushOGCD(AID.Reassemble, Player); + + if (IsPausedForFlamethrower || !Player.InCombat || primaryTarget == null) + return; + + if (ShouldReassemble(strategy, primaryTarget) && _state.CanWeave(_state.CD(AID.Reassemble) - 55, 0.6f, deadline)) + PushOGCD(AID.Reassemble, Player); + + if (ShouldWildfire(strategy, deadline) && _state.GCD < 0.8f) + PushOGCD(AID.Wildfire, primaryTarget); + + if (ShouldStabilize(strategy, deadline)) + PushOGCD(AID.BarrelStabilizer, Player); + + UseCharges(strategy, primaryTarget, deadline); + + if (ShouldMinion(strategy, primaryTarget) && _state.CanWeave(AID.RookAutoturret, 0.6f, deadline)) + PushOGCD(AID.RookAutoturret, Player); + + if (ShouldHypercharge(strategy, deadline)) + PushOGCD(AID.Hypercharge, Player); + } + + // TODO this argument name sucks ass + private float NextToolCD(bool untilCap) + => MathF.Min( + Unlocked(AID.Drill) ? _state.CD(AID.Drill) - (untilCap && Unlocked(TraitID.EnhancedMultiweapon) ? 0 : 20) : float.MaxValue, + MathF.Min( + Unlocked(AID.AirAnchor) ? _state.CD(AID.AirAnchor) : float.MaxValue, + Unlocked(AID.ChainSaw) ? _state.CD(AID.ChainSaw) : float.MaxValue + ) + ); + + // cooldown at max charges: <=30 up to level 73, 0 otherwise + private float MaxGaussCD => _state.CD(AID.GaussRound) - (Unlocked(TraitID.ChargedActionMastery) ? 0 : 30); + private float MaxRicochetCD => _state.CD(AID.Ricochet) - (Unlocked(TraitID.ChargedActionMastery) ? 0 : 30); + + private void UseCharges(StrategyValues strategy, Actor? primaryTarget, float deadline) + { + var gaussRoundCD = _state.CD(AID.GaussRound) - 60; + var ricochetCD = _state.CD(AID.Ricochet) - 60; + + var canGauss = Unlocked(AID.GaussRound) && _state.CanWeave(gaussRoundCD, 0.6f, deadline); + var canRicochet = Unlocked(AID.Ricochet) && _state.CanWeave(ricochetCD, 0.6f, deadline); + + if (canGauss && _state.CanWeave(MaxGaussCD, 0.6f, deadline)) + PushOGCD(AID.GaussRound, Unlocked(AID.DoubleCheck) ? BestRangedAOETarget : primaryTarget); + + if (canRicochet && _state.CanWeave(MaxRicochetCD, 0.6f, deadline)) + PushOGCD(AID.Ricochet, BestRangedAOETarget); + + var useAllCharges = _state.RaidBuffsLeft > 0 || _state.RaidBuffsIn > 9000 || Overheated || !Unlocked(AID.Hypercharge); + if (!useAllCharges) + return; + + // this is a little awkward but we want to try to keep the cooldowns of both actions within range of each other + if (canGauss && canRicochet) + { + if (gaussRoundCD > ricochetCD) + UseRicochet(primaryTarget); + else + UseGauss(primaryTarget); + } + else if (canGauss) + UseGauss(primaryTarget); + else if (canRicochet) + UseRicochet(primaryTarget); + } + + private void UseGauss(Actor? primaryTarget) => PushOGCD(AID.GaussRound, Unlocked(AID.DoubleCheck) ? BestRangedAOETarget : primaryTarget, -50); + private void UseRicochet(Actor? primaryTarget) => PushOGCD(AID.Ricochet, BestRangedAOETarget, -50); + + private bool ShouldReassemble(StrategyValues strategy, Actor? primaryTarget) + { + if (ReassembleLeft > 0 || !Unlocked(AID.Reassemble) || Overheated || primaryTarget == null) + return false; + + if (NumAOETargets > 3 && Unlocked(AID.SpreadShot)) + return true; + + if (_state.RaidBuffsIn < 10 && _state.RaidBuffsIn > _state.GCD) + return false; + + if (!Unlocked(AID.Drill)) + return (AID)_state.ComboLastAction == AID.SlugShot; + + return NextToolCD(untilCap: false) <= _state.GCD; + } + + private bool ShouldMinion(StrategyValues strategy, Actor? primaryTarget) + { + if (!Unlocked(AID.RookAutoturret) || primaryTarget == null || HasMinion || Battery < 50) + return false; + + // todo tweak anticipated window, queen doesnt start autoing for 5 seconds + return _state.RaidBuffsIn > 50 || _state.RaidBuffsIn < _state.GCD || _state.RaidBuffsLeft > 10; + } + + private bool ShouldHypercharge(StrategyValues strategy, float deadline) + { + if (!Unlocked(AID.Hypercharge) + // no gauge + || HyperchargedLeft == 0 && Heat < 50 + // already active, can't use again + || Overheated + // reassemble would be wasted on heat blast or crossbow + || ReassembleLeft > _state.GCD + || !_state.CanWeave(AID.Hypercharge, 0.6f, deadline)) + return false; + + // hack for CD alignment in opener - wait for wildfire application + if (CombatTimer < 10 && _state.CD(AID.Wildfire) < 10) + return false; + + /* A full segment of Hypercharge is exactly three GCDs worth of time, or 7.5 seconds. Because of this, you should never enter Hypercharge if Chainsaw, Drill or Air Anchor has less than eight seconds on their cooldown timers. Doing so will cause the Chainsaw, Drill or Air Anchor cooldowns to drift, which leads to a loss of DPS and will more than likely cause issues down the line in your rotation when you reach your rotational reset at Wildfire. + */ + return NextToolCD(untilCap: true) > _state.GCD + 7.5f; + } + + private bool ShouldWildfire(StrategyValues strategy, float deadline) + { + if (!Unlocked(AID.Wildfire) || !_state.CanWeave(AID.Wildfire, 0.6f, deadline) || strategy.Option(Track.Buffs).As() == OffensiveStrategy.Delay) + return false; + + // hack for opener - delay until all 4 tool charges are used + if (CombatTimer < 60) + return NextToolCD(untilCap: false) > _state.GCD; + + return FMFLeft == 0; + } + + private bool ShouldStabilize(StrategyValues strategy, float deadline) + { + if (!Unlocked(AID.BarrelStabilizer) || !_state.CanWeave(AID.BarrelStabilizer, 0.6f, deadline) || strategy.Option(Track.Buffs).As() == OffensiveStrategy.Delay) + return false; + + return _state.CD(AID.Drill) > 0; + } + + public override void Exec(StrategyValues strategy, Actor? primaryTarget) + { + var targeting = strategy.Option(Track.Targeting).As(); + + var wildfireTarget = Hints.PriorityTargets.FirstOrDefault(x => x.Actor.FindStatus(SID.WildfireTarget, Player.InstanceID) != null)?.Actor; + + // if autotarget enabled, force all weaponskills to hit wildfire'd target during effect to maximize potency + if (wildfireTarget != null && targeting == Targeting.Auto) + { + primaryTarget = wildfireTarget; + targeting = Targeting.AutoPrimary; + } + else + SelectPrimaryTarget(targeting, ref primaryTarget, range: 25); + + _state.UpdateCommon(primaryTarget); + + var gauge = GetGauge(); + + Heat = gauge.Heat; + Battery = gauge.Battery; + Overheated = (gauge.TimerActive & 1) != 0; + OverheatLeft = gauge.OverheatTimeRemaining / 1000f; + HasMinion = (gauge.TimerActive & 2) != 0; + + ReassembleLeft = StatusLeft(SID.Reassembled); + WildfireLeft = StatusLeft(SID.WildfirePlayer); + HyperchargedLeft = StatusLeft(SID.Hypercharged); + ExcavatorLeft = StatusLeft(SID.ExcavatorReady); + FMFLeft = StatusLeft(SID.FullMetalMachinist); + + Flamethrower = StatusLeft(SID.Flamethrower) > 0; + + (BestAOETarget, NumAOETargets) = strategy.Option(Track.AOE).As() switch + { + AOEStrategy.AOE => SelectTarget(targeting, primaryTarget, 12, IsConeAOETarget), + _ => (primaryTarget, 0) + }; + (BestRangedAOETarget, NumRangedAOETargets) = SelectTarget(targeting, primaryTarget, 25, IsSplashTarget); + (BestChainsawTarget, NumSawTargets) = SelectTarget(targeting, primaryTarget, 25, Is25yRectTarget); + NumFlamethrowerTargets = Hints.NumPriorityTargetsInAOECone(Player.Position, 8, Player.Rotation.ToDirection(), 45.Degrees()); + + CalcNextBestGCD(strategy, primaryTarget); + QueueOGCD(deadline => CalcNextBestOGCD(strategy, primaryTarget, deadline)); + } + + private PositionCheck IsConeAOETarget => (playerTarget, targetToTest) => Hints.TargetInAOECone(targetToTest, Player.Position, 12, Player.DirectionTo(playerTarget), 60.Degrees()); +} diff --git a/BossMod/Autorotation/xan/xbase.cs b/BossMod/Autorotation/xan/xbase.cs index 19abc9083..7526319be 100644 --- a/BossMod/Autorotation/xan/xbase.cs +++ b/BossMod/Autorotation/xan/xbase.cs @@ -1,5 +1,4 @@ using BossMod.Autorotation.Legacy; -using static BossMod.Autorotation.StrategyValues; namespace BossMod.Autorotation.xan; @@ -20,6 +19,7 @@ public class State(RotationModule module) : CommonState(module) { } protected float PelotonLeft { get; private set; } protected float SwiftcastLeft { get; private set; } protected float TrueNorthLeft { get; private set; } + protected float CombatTimer { get; private set; } protected xbase(RotationModuleManager manager, Actor player) : base(manager, player) { @@ -50,35 +50,32 @@ protected void PushAction(AID aid, Actor? target, float priority) Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, priority); } - protected void QueueOGCD(Action ogcdFun) + protected void QueueOGCD(Action ogcdFun) { var deadline = _state.GCD > 0 ? _state.GCD : float.MaxValue; if (_state.CanWeave(deadline - _state.OGCDSlotLength)) - ogcdFun(deadline - _state.OGCDSlotLength, deadline); + ogcdFun(deadline - _state.OGCDSlotLength); if (_state.CanWeave(deadline)) - ogcdFun(deadline, deadline); + ogcdFun(deadline); } /// - /// If the user's current target is more than yalms from the player, this function attempts to find a closer one. No prioritization is done; if any target is returned, it is simply the actor that was earliest in the object table.
+ /// Tries to select a suitable primary target.
/// - /// It is guaranteed that will be set to either null or an attackable enemy (not, for example, an ally or event object).
+ /// If the provided is null, an NPC, or non-enemy object; it will be reset to null.
/// - /// If the provided Targeting strategy is Manual, this function is otherwise a no-op. + /// Additionally, if is set to Targeting.Auto, and the user's current target is more than yalms from the player, this function attempts to find a closer one. No prioritization is done; if any target is returned, it is simply the actor that was earliest in the object table. ///
- /// Reference to the Targeting track of the active strategy + /// Targeting strategy /// Player's current target - may be null /// Maximum distance from the player to search for a candidate target - protected void SelectPrimaryTarget(OptionRef track, ref Actor? primaryTarget, float range) + protected void SelectPrimaryTarget(Targeting strategy, ref Actor? primaryTarget, float range) { if (!IsEnemy(primaryTarget)) primaryTarget = null; - var tars = track.As(); - if (tars == Targeting.Manual) - { + if (strategy != Targeting.Auto) return; - } if (Player.DistanceToHitbox(primaryTarget) > range) { @@ -87,6 +84,13 @@ protected void SelectPrimaryTarget(OptionRef track, ref Actor? primaryTarget, fl } } + /// + /// Get effective cast time for the provided action.
+ /// The default implementation returns the action's base cast time multiplied by the player's spell-speed factor, which accounts for haste buffs (like Leylines) and slow debuffs. It also accounts for Swiftcast.
+ /// Subclasses should handle job-specific cast speed adjustments, such as RDM's Dualcast or PCT's motifs. + ///
+ /// + /// protected virtual float GetCastTime(AID aid) => SwiftcastLeft > _state.GCD ? 0 : ActionDefinitions.Instance.Spell(aid)!.CastTime * _state.SpellGCDTime / 2.5f; protected bool CanCast(AID aid) => GetCastTime(aid) <= ForceMovementIn; @@ -98,24 +102,27 @@ protected void SelectPrimaryTarget(OptionRef track, ref Actor? primaryTarget, fl private static bool IsEnemy(Actor? actor) => actor != null && actor.Type is ActorType.Enemy or ActorType.Part && !actor.IsAlly; - protected (Actor? Best, int Priority) SelectTarget( - OptionRef track, + protected delegate bool PositionCheck(Actor playerTarget, Actor targetToTest); + protected delegate P PriorityFunc

(int totalTargets, Actor primaryTarget); + + protected (Actor? Best, int Targets) SelectTarget( + Targeting track, Actor? primaryTarget, float range, - Func isInAOE + PositionCheck isInAOE ) => SelectTarget(track, primaryTarget, range, isInAOE, (numTargets, _) => numTargets); protected (Actor? Best, P Priority) SelectTarget

( - OptionRef track, + Targeting track, Actor? primaryTarget, float range, - Func isInAOE, - Func prioFunc + PositionCheck isInAOE, + PriorityFunc

prioritize ) where P : struct, IComparable { - P targetPrio(Actor a) => prioFunc(Hints.NumPriorityTargetsInAOE(enemy => isInAOE(a, enemy.Actor)), a); + P targetPrio(Actor potentialTarget) => prioritize(Hints.NumPriorityTargetsInAOE(enemy => isInAOE(potentialTarget, enemy.Actor)), potentialTarget); - return track.As() switch + return track switch { Targeting.Auto => FindBetterTargetBy(primaryTarget, range, targetPrio), Targeting.AutoPrimary => primaryTarget == null ? (null, default) : FindBetterTargetBy( @@ -128,11 +135,10 @@ Func prioFunc }; } - protected bool IsSplashTarget(Actor primary, Actor other) => Hints.TargetInAOECircle(other, primary.Position, 5); - protected int NumMeleeAOETargets() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 5); - protected bool Is25yRectTarget(Actor primary, Actor other) => Hints.TargetInAOERect(other, Player.Position, (primary.Position - Player.Position).Normalized(), 25, 4); + protected PositionCheck IsSplashTarget => (Actor primary, Actor other) => Hints.TargetInAOECircle(other, primary.Position, 5); + protected PositionCheck Is25yRectTarget => (Actor primary, Actor other) => Hints.TargetInAOERect(other, Player.Position, Player.DirectionTo(primary), 25, 4); public sealed override void Execute(StrategyValues strategy, Actor? primaryTarget) { @@ -143,6 +149,8 @@ public sealed override void Execute(StrategyValues strategy, Actor? primaryTarge _state.AnimationLockDelay = MathF.Max(0.1f, _state.AnimationLockDelay); + CombatTimer = (float)(World.CurrentTime - Manager.CombatStart).TotalSeconds; + Exec(strategy, primaryTarget); } diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index ec57a2847..7b167e3fc 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -121,6 +121,8 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public ActorStatus? FindStatus(SID sid) where SID : Enum => FindStatus((uint)(object)sid); public ActorStatus? FindStatus(SID sid, ulong source) where SID : Enum => FindStatus((uint)(object)sid, source); + public WDir DirectionTo(Actor other) => (other.Position - Position).Normalized(); + public float DistanceToHitbox(Actor? other) => other == null ? float.MaxValue : (other.Position - Position).Length() - other.HitboxRadius - HitboxRadius; public override string ToString() => $"{OID:X} '{Name}' <{InstanceID:X}>"; From 0ef9c1dbdec7685c4182200dd1df9f33566f3e25 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:48:26 -0400 Subject: [PATCH 2/3] "fix formatting" ok bro --- BossMod/Autorotation/xan/MCH.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/BossMod/Autorotation/xan/MCH.cs b/BossMod/Autorotation/xan/MCH.cs index b7bbeb03a..fc8da7ab8 100644 --- a/BossMod/Autorotation/xan/MCH.cs +++ b/BossMod/Autorotation/xan/MCH.cs @@ -222,14 +222,7 @@ private bool ShouldMinion(StrategyValues strategy, Actor? primaryTarget) private bool ShouldHypercharge(StrategyValues strategy, float deadline) { - if (!Unlocked(AID.Hypercharge) - // no gauge - || HyperchargedLeft == 0 && Heat < 50 - // already active, can't use again - || Overheated - // reassemble would be wasted on heat blast or crossbow - || ReassembleLeft > _state.GCD - || !_state.CanWeave(AID.Hypercharge, 0.6f, deadline)) + if (!Unlocked(AID.Hypercharge) || HyperchargedLeft == 0 && Heat < 50 || Overheated || ReassembleLeft > _state.GCD || !_state.CanWeave(AID.Hypercharge, 0.6f, deadline)) return false; // hack for CD alignment in opener - wait for wildfire application From b9407a37c7e884bafac3c39271e705e4c25cc937 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:57:21 -0400 Subject: [PATCH 3/3] trait level --- BossMod/Autorotation/RotationModule.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BossMod/Autorotation/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index a6e6c8bf7..110e8ed96 100644 --- a/BossMod/Autorotation/RotationModule.cs +++ b/BossMod/Autorotation/RotationModule.cs @@ -102,8 +102,10 @@ public bool ActionUnlocked(ActionID action) public bool TraitUnlocked(uint id) { - var unlock = Service.LuminaRow(id)?.Quest.Row ?? 0; - return ActionDefinitions.Instance.UnlockCheck?.Invoke(unlock) ?? true; + var trait = Service.LuminaRow(id); + var unlock = trait?.Quest.Row ?? 0; + var level = trait?.Level ?? 0; + return Player.Level >= level && (ActionDefinitions.Instance.UnlockCheck?.Invoke(unlock) ?? true); } // utility to resolve the target overrides; returns null on failure - in this case module is expected to run smart-targeting logic