From 346061ff85bdc3bffefad39f8b536d88fee54fd7 Mon Sep 17 00:00:00 2001 From: ace Date: Thu, 20 Feb 2025 15:40:07 -0800 Subject: [PATCH 1/2] delete Legacy join me in pulling the trigger --- BossMod/Autorotation/Legacy/CommonState.cs | 118 --- BossMod/Autorotation/Legacy/LegacyBRD.cs | 599 ------------- BossMod/Autorotation/Legacy/LegacyDNC.cs | 513 ----------- BossMod/Autorotation/Legacy/LegacyDRG.cs | 483 ---------- BossMod/Autorotation/Legacy/LegacyGNB.cs | 841 ------------------ BossMod/Autorotation/Legacy/LegacyModule.cs | 17 - BossMod/Autorotation/Legacy/LegacyRPR.cs | 678 -------------- BossMod/Autorotation/Legacy/LegacyWAR.cs | 630 ------------- .../AutorotationLegacy/AutorotationLegacy.cs | 26 - BossMod/AutorotationLegacy/BLM/BLMActions.cs | 125 --- BossMod/AutorotationLegacy/BLM/BLMRotation.cs | 244 ----- BossMod/AutorotationLegacy/HealerActions.cs | 234 ----- BossMod/AutorotationLegacy/PLD/PLDActions.cs | 115 --- BossMod/AutorotationLegacy/PLD/PLDRotation.cs | 83 -- BossMod/AutorotationLegacy/SCH/SCHActions.cs | 186 ---- BossMod/AutorotationLegacy/SCH/SCHRotation.cs | 86 -- BossMod/AutorotationLegacy/SMN/SMNActions.cs | 125 --- BossMod/AutorotationLegacy/SMN/SMNRotation.cs | 105 --- BossMod/AutorotationLegacy/TankActions.cs | 30 - BossMod/AutorotationLegacy/WHM/WHMActions.cs | 250 ------ BossMod/AutorotationLegacy/WHM/WHMConfig.cs | 12 - BossMod/AutorotationLegacy/WHM/WHMRotation.cs | 184 ---- 22 files changed, 5684 deletions(-) delete mode 100644 BossMod/Autorotation/Legacy/CommonState.cs delete mode 100644 BossMod/Autorotation/Legacy/LegacyBRD.cs delete mode 100644 BossMod/Autorotation/Legacy/LegacyDNC.cs delete mode 100644 BossMod/Autorotation/Legacy/LegacyDRG.cs delete mode 100644 BossMod/Autorotation/Legacy/LegacyGNB.cs delete mode 100644 BossMod/Autorotation/Legacy/LegacyModule.cs delete mode 100644 BossMod/Autorotation/Legacy/LegacyRPR.cs delete mode 100644 BossMod/Autorotation/Legacy/LegacyWAR.cs delete mode 100644 BossMod/AutorotationLegacy/AutorotationLegacy.cs delete mode 100644 BossMod/AutorotationLegacy/BLM/BLMActions.cs delete mode 100644 BossMod/AutorotationLegacy/BLM/BLMRotation.cs delete mode 100644 BossMod/AutorotationLegacy/HealerActions.cs delete mode 100644 BossMod/AutorotationLegacy/PLD/PLDActions.cs delete mode 100644 BossMod/AutorotationLegacy/PLD/PLDRotation.cs delete mode 100644 BossMod/AutorotationLegacy/SCH/SCHActions.cs delete mode 100644 BossMod/AutorotationLegacy/SCH/SCHRotation.cs delete mode 100644 BossMod/AutorotationLegacy/SMN/SMNActions.cs delete mode 100644 BossMod/AutorotationLegacy/SMN/SMNRotation.cs delete mode 100644 BossMod/AutorotationLegacy/TankActions.cs delete mode 100644 BossMod/AutorotationLegacy/WHM/WHMActions.cs delete mode 100644 BossMod/AutorotationLegacy/WHM/WHMConfig.cs delete mode 100644 BossMod/AutorotationLegacy/WHM/WHMRotation.cs diff --git a/BossMod/Autorotation/Legacy/CommonState.cs b/BossMod/Autorotation/Legacy/CommonState.cs deleted file mode 100644 index f64b219786..0000000000 --- a/BossMod/Autorotation/Legacy/CommonState.cs +++ /dev/null @@ -1,118 +0,0 @@ -namespace BossMod.Autorotation.Legacy; - -// TODO: a _lot_ of this stuff should be reworked... -public abstract class CommonState(RotationModule module) -{ - public RotationModule Module = module; - public int Level => Module.Player.Level; - public uint CurMP => Module.Player.HPMP.CurMP; // 10000 max - public bool TargetingEnemy; - public bool HaveTankStance; - public float RangeToTarget; // minus both hitboxes; <= 0 means inside hitbox, <= 3 means in melee range, maxvalue if there is no target - public float AnimationLock; // typical actions have 0.6 delay, but some (notably primal rend and potion) are >1 - public float AnimationLockDelay; // average time between action request and confirmation; this is added to effective animation lock for actions - public float ComboTimeLeft => Module.World.Client.ComboState.Remaining; // 0 if not in combo, max 30 - public uint ComboLastAction => Module.World.Client.ComboState.Action; - public float RaidBuffsLeft; // 0 if no damage-up status is up, otherwise it is time left on longest - - public float? CountdownRemaining => Module.World.Client.CountdownRemaining; - public bool ForbidDOTs; - public float FightEndIn; // how long fight will last (we try to spend all resources before this happens) - public float RaidBuffsIn; // estimate time when new raidbuff window starts (if it is smaller than FightEndIn, we try to conserve resources) - public float PositionLockIn; // time left to use moving abilities (Primal Rend and Onslaught) - we won't use them if it is ==0; setting this to 2.5f will make us use PR asap - public Positional NextPositional; - public bool NextPositionalImminent; // true if next positional will happen on next gcd - public bool NextPositionalCorrect; // true if correctly positioned for next positional - - // both 2.5 max (unless slowed), reduced by gear attributes and certain status effects - public float AttackGCDTime; - public float SpellGCDTime; - - // find a slot containing specified duty action; returns -1 if not found - public int FindDutyActionSlot(ActionID action) => Array.FindIndex(Module.World.Client.DutyActions, d => d.Action == action); - // find a slot containing specified duty action, if other duty action is the specified one; returns -1 if not found, or other action is different - public int FindDutyActionSlot(ActionID action, ActionID other) - { - var slot = FindDutyActionSlot(action); - return slot >= 0 && Module.World.Client.DutyActions[1 - slot].Action == other ? slot : -1; - } - - public float GCD => Module.World.Client.Cooldowns[ActionDefinitions.GCDGroup].Remaining; // 2.5 max (decreased by SkS), 0 if not on gcd - public float PotionCD => Module.World.Client.Cooldowns[ActionDefinitions.PotionCDGroup].Remaining; // variable max - public float CD(AID aid) where AID : Enum => Module.World.Client.Cooldowns[ActionDefinitions.Instance.Spell(aid)!.MainCooldownGroup].Remaining; - - public float DutyActionCD(int slot) => slot is >= 0 and < 2 ? Module.World.Client.Cooldowns[ActionDefinitions.DutyAction0CDGroup + slot].Remaining : float.MaxValue; - public float DutyActionCD(ActionID action) => DutyActionCD(FindDutyActionSlot(action)); - - // check whether weaving typical ogcd off cooldown would end its animation lock by the specified deadline - public float OGCDSlotLength => 0.6f + AnimationLockDelay; // most actions have 0.6 anim lock delay, which allows double-weaving oGCDs between GCDs - public bool CanWeave(float deadline) => AnimationLock + OGCDSlotLength <= deadline; // is it still possible to weave typical oGCD without missing deadline? - // check whether weaving ogcd with specified remaining cooldown and lock time would end its animation lock by the specified deadline - // deadline is typically either infinity (if we don't care about GCDs) or GCD (for second/only ogcd slot) or GCD-OGCDSlotLength (for first ogcd slot) - public bool CanWeave(float cooldown, float actionLock, float deadline) => deadline < 10000 ? MathF.Max(cooldown, AnimationLock) + actionLock + AnimationLockDelay <= deadline : cooldown <= AnimationLock; - public bool CanWeave(AID aid, float actionLock, float deadline) where AID : Enum => CanWeave(CD(aid), actionLock, deadline); - - public void UpdateCommon(Actor? target, float estimatedAnimLockDelay) - { - var vuln = Module.Manager.Planner?.EstimateTimeToNextVulnerable() ?? (false, 10000); - var downtime = Module.Manager.Planner?.EstimateTimeToNextDowntime() ?? (false, 0); - var poslock = Module.Manager.Planner?.EstimateTimeToNextPositioning() ?? (false, 10000); - - TargetingEnemy = target != null && target.Type is ActorType.Enemy or ActorType.Part && !target.IsAlly; - RangeToTarget = Module.Player.DistanceToHitbox(target); - AnimationLock = (Module.Player.CastInfo?.RemainingTime ?? 0) + Module.World.Client.AnimationLock; - AnimationLockDelay = estimatedAnimLockDelay; - - RaidBuffsLeft = vuln.Item1 ? vuln.Item2 : 0; - foreach (var status in Module.Player.Statuses.Where(s => IsDamageBuff(s.ID))) - { - RaidBuffsLeft = MathF.Max(RaidBuffsLeft, StatusDuration(status.ExpireAt)); - } - // TODO: also check damage-taken debuffs on target - - var targetEnemy = Module.Hints.FindEnemy(target); - ForbidDOTs = targetEnemy?.ForbidDOTs ?? false; - FightEndIn = downtime.Item1 ? 0 : downtime.Item2; - RaidBuffsIn = vuln.Item1 ? 0 : vuln.Item2; - if (Module.Bossmods.ActiveModule?.Info?.PlanLevel > 0) // assumption: if there is no planning support for encounter (meaning it's something trivial, like outdoor boss), don't expect any cooldowns - RaidBuffsIn = Math.Min(RaidBuffsIn, Module.Bossmods.RaidCooldowns.NextDamageBuffIn()); - PositionLockIn = !poslock.Item1 ? poslock.Item2 : 0; - NextPositional = Positional.Any; - NextPositionalImminent = false; - NextPositionalCorrect = true; - - // all GCD skills share the same base recast time (with some exceptions that aren't relevant here) - AttackGCDTime = ActionSpeed.GCDRounded(Module.World.Client.PlayerStats.SkillSpeed, Module.World.Client.PlayerStats.Haste, Module.Player.Level); - SpellGCDTime = ActionSpeed.GCDRounded(Module.World.Client.PlayerStats.SpellSpeed, Module.World.Client.PlayerStats.Haste, Module.Player.Level); - } - - public void UpdatePositionals(Actor? target, (Positional pos, bool imm) positional, bool trueNorth) - { - var ignore = trueNorth || (target?.Omnidirectional ?? true); - NextPositional = positional.pos; - NextPositionalImminent = !ignore && positional.imm; - NextPositionalCorrect = ignore || target == null || positional.pos switch - { - Positional.Flank => MathF.Abs(target.Rotation.ToDirection().Dot((Module.Player.Position - target.Position).Normalized())) < 0.7071067f, - Positional.Rear => target.Rotation.ToDirection().Dot((Module.Player.Position - target.Position).Normalized()) < -0.7071068f, - _ => true - }; - Module.Manager.Hints.RecommendedPositional = (target, NextPositional, NextPositionalImminent, NextPositionalCorrect); - } - - public float StatusDuration(DateTime expireAt) => Math.Max((float)(expireAt - Module.World.CurrentTime).TotalSeconds, 0.0f); - - // this also checks pending statuses - // note that we check pending statuses first - otherwise we get the same problem with double refresh if we try to refresh early (we find old status even though we have pending one) - public (float Left, int Stacks) StatusDetails(Actor? actor, uint sid, ulong sourceID, float pendingDuration = 1000) - { - var status = actor?.FindStatus(sid, sourceID, Module.World.FutureTime(pendingDuration)); - return status != null ? (StatusDuration(status.Value.ExpireAt), status.Value.Extra & 0xFF) : (0, 0); - } - public (float Left, int Stacks) StatusDetails(Actor? actor, SID sid, ulong sourceID, float pendingDuration = 1000) where SID : Enum => StatusDetails(actor, (uint)(object)sid, sourceID, pendingDuration); - - // check whether specified status is a damage buff - // see https://i.redd.it/xrtgpras94881.png - // TODO: AST card buffs?, enemy debuffs?, single-target buffs (DRG dragon sight, DNC devilment) - public bool IsDamageBuff(uint statusID) => statusID == 49 || RaidCooldowns.IsDamageBuff(statusID); // medicated or raidbuff -} diff --git a/BossMod/Autorotation/Legacy/LegacyBRD.cs b/BossMod/Autorotation/Legacy/LegacyBRD.cs deleted file mode 100644 index c0a22058f7..0000000000 --- a/BossMod/Autorotation/Legacy/LegacyBRD.cs +++ /dev/null @@ -1,599 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; - -namespace BossMod.Autorotation.Legacy; - -public sealed class LegacyBRD : LegacyModule -{ - public enum Track { AOE, Songs, Potion, DOTs, ApexArrow, BlastArrow, RagingStrikes, Bloodletter, EmpyrealArrow, Barrage, Sidewinder } - public enum AOEStrategy { SingleTarget, AutoTargetHitPrimary, AutoTargetHitMost, AutoOnPrimary, ForceAOE } - public enum SongStrategy { Automatic, Extend, Overextend, ForceWM, ForceMB, ForceAP, ForcePP, Delay } - public enum PotionStrategy { Manual, Burst, Force } - public enum DotStrategy { Automatic, AutomaticExtendOnly, Forbid, ForceExtend, ExtendIgnoreBuffs, ExtendDelayed } - public enum ApexArrowStrategy { Automatic, Delay, ForceAnyGauge, ForceHighGauge, ForceCapGauge } - public enum OffensiveStrategy { Automatic, Delay, Force } - public enum BloodletterStrategy { Automatic, Delay, Force, KeepOneCharge, KeepTwoCharges } - - public static RotationModuleDefinition Definition() - { - // TODO: think about target overrides where they make sense - var res = new RotationModuleDefinition("Legacy BRD", "Old pre-refactoring module", "Legacy (pre-DT)", "veyn", RotationModuleQuality.WIP, BitMask.Build((int)Class.BRD), 100); - - res.Define(Track.AOE).As("AOE", uiPriority: 110) - .AddOption(AOEStrategy.SingleTarget, "ST", "Use single-target actions") - .AddOption(AOEStrategy.AutoTargetHitPrimary, "AutoTargetHitPrimary", "Use aoe actions if profitable select best target that ensures primary target is hit") - .AddOption(AOEStrategy.AutoTargetHitMost, "AutoTargetHitMost", "Use aoe actions if profitable select a target that ensures maximal number of targets are hit") - .AddOption(AOEStrategy.AutoOnPrimary, "AutoOnPrimary", "Use aoe actions on primary target if profitable") - .AddOption(AOEStrategy.ForceAOE, "AOE", "Use aoe rotation on primary target even if it's less total damage than single-target") - .AddAssociatedActions(BRD.AID.QuickNock, BRD.AID.Ladonsbite, BRD.AID.RainOfDeath, BRD.AID.Shadowbite); - - res.Define(Track.Songs).As("Songs", uiPriority: 100) - .AddOption(SongStrategy.Automatic, "Automatic") - .AddOption(SongStrategy.Extend, "Extend", "Extend until last tick") - .AddOption(SongStrategy.Overextend, "Overextend", "Extend until last possible moment") - .AddOption(SongStrategy.ForceWM, "ForceWM", "Force switch to Wanderer's Minuet") - .AddOption(SongStrategy.ForceMB, "ForceMB", "Force switch to Mage's Ballad") - .AddOption(SongStrategy.ForceAP, "ForceAP", "Force switch to Army's Paeon") - .AddOption(SongStrategy.ForcePP, "ForcePP", "Force Pitch Perfect (assuming WM is up)") - .AddOption(SongStrategy.Delay, "Delay", "Do not use any songs; stay songless if needed") - .AddAssociatedActions(BRD.AID.WanderersMinuet, BRD.AID.MagesBallad, BRD.AID.ArmysPaeon, BRD.AID.PitchPerfect); - - res.Define(Track.Potion).As("Potion", uiPriority: 90) - .AddOption(PotionStrategy.Manual, "Manual", "Do not use automatically") - .AddOption(PotionStrategy.Burst, "Burst", "Use right before burst", 270, 30) - .AddOption(PotionStrategy.Force, "Force", "Use ASAP", 270, 30) - .AddAssociatedAction(ActionDefinitions.IDPotionDex); - - // TODO: think about multidotting, probably should be a separate track (default is primary only, think about interactions with ij etc) - res.Define(Track.DOTs).As("DOTs", uiPriority: 80) - .AddOption(DotStrategy.Automatic, "Automatic", "Apply dots asap, reapply when either dots about to expire or in buff window") - .AddOption(DotStrategy.AutomaticExtendOnly, "AutomaticExtendOnly", "Do not apply new dots, extend existing normally with IJ") - .AddOption(DotStrategy.Forbid, "Forbid", "Do not apply new or extend existing dots") - .AddOption(DotStrategy.ForceExtend, "ForceExtend", "Force extend dots via IJ ASAP") - .AddOption(DotStrategy.ExtendIgnoreBuffs, "ExtendIgnoreBuffs", "Extend dots via IJ only if they are about to fall off (but don't risk proc overwrites), don't extend early under buffs") - .AddOption(DotStrategy.ExtendDelayed, "ExtendDelayed", "Extend dots via IJ at last possible moment, even if it might overwrite proc") - .AddAssociatedActions(BRD.AID.VenomousBite, BRD.AID.Windbite, BRD.AID.IronJaws, BRD.AID.CausticBite, BRD.AID.Stormbite); - - res.Define(Track.ApexArrow).As("Apex", uiPriority: 70) - .AddOption(ApexArrowStrategy.Automatic, "Automatic", "Use at 80+ if buffs are about to run off, use at 100 asap unless raid buffs are imminent") - .AddOption(ApexArrowStrategy.Delay, "Delay", "Delay") - .AddOption(ApexArrowStrategy.ForceAnyGauge, "ForceAnyGauge", "Force at any gauge (even if it means no BA)") - .AddOption(ApexArrowStrategy.ForceHighGauge, "ForceHighGauge", "Force at 80+ gauge") - .AddOption(ApexArrowStrategy.ForceCapGauge, "ForceCapGauge", "Force at 100 gauge (don't delay until raidbuffs)") - .AddAssociatedActions(BRD.AID.ApexArrow); - - res.Define(Track.BlastArrow).As("Blast", uiPriority: 60) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "Use normally") - .AddOption(OffensiveStrategy.Delay, "Delay", "Delay") - .AddOption(OffensiveStrategy.Force, "Force", "Force use ASAP") - .AddAssociatedActions(BRD.AID.BlastArrow); - - res.Define(Track.RagingStrikes).As("RS", uiPriority: 50) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "Use normally") - .AddOption(OffensiveStrategy.Delay, "Delay", "Delay") - .AddOption(OffensiveStrategy.Force, "Force", "Force use ASAP") - .AddAssociatedActions(BRD.AID.RagingStrikes, BRD.AID.BattleVoice, BRD.AID.RadiantFinale); - - res.Define(Track.Bloodletter).As("BL", uiPriority: 40) - .AddOption(BloodletterStrategy.Automatic, "Automatic", "Pool for raid buffs, otherwise use freely") - .AddOption(BloodletterStrategy.Delay, "Delay", "Do not use, allowing overcap") - .AddOption(BloodletterStrategy.Force, "Force", "Force use all charges") - .AddOption(BloodletterStrategy.KeepOneCharge, "KeepOneCharge", "Keep 1 charge, use if 2+ charges available") - .AddOption(BloodletterStrategy.KeepTwoCharges, "KeepTwoCharges", "Keep 2 charges, use if overcap is imminent") - .AddAssociatedActions(BRD.AID.Bloodletter, BRD.AID.RainOfDeath); - - res.Define(Track.EmpyrealArrow).As("EA", uiPriority: 30) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "Use normally") - .AddOption(OffensiveStrategy.Delay, "Delay", "Delay") - .AddOption(OffensiveStrategy.Force, "Force", "Force use ASAP") - .AddAssociatedActions(BRD.AID.EmpyrealArrow); - - res.Define(Track.Barrage).As("Barrage", uiPriority: 20) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "Use normally") - .AddOption(OffensiveStrategy.Delay, "Delay", "Delay") - .AddOption(OffensiveStrategy.Force, "Force", "Force use ASAP") - .AddAssociatedActions(BRD.AID.Barrage); - - res.Define(Track.Sidewinder).As("SW", uiPriority: 10) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "Use normally") - .AddOption(OffensiveStrategy.Delay, "Delay", "Delay") - .AddOption(OffensiveStrategy.Force, "Force", "Force use ASAP") - .AddAssociatedActions(BRD.AID.Sidewinder); - - return res; - } - - public enum Song { None, MagesBallad, ArmysPaeon, WanderersMinuet } - - // full state needed for determining next action - public class State(RotationModule module) : CommonState(module) - { - public Song ActiveSong; - public float ActiveSongLeft; // 45 max - public int Repertoire; - public int SoulVoice; - public int NumCoda; - public float HawkEyeLeft; - public float BlastArrowLeft; - public float RagingStrikesLeft; - public float BattleVoiceLeft; - public float RadiantFinaleLeft; - public float ArmysMuseLeft; - public float BarrageLeft; - public float PelotonLeft; // 30 max - public float TargetCausticLeft; - public float TargetStormbiteLeft; - public Actor? BestLadonsbiteTarget; - public int NumLadonsbiteTargets; // range 12 90-degree cone - public Actor? BestRainOfDeathTarget; - public int NumRainOfDeathTargets; // range 8 circle around target - - // upgrade paths - public BRD.AID BestBurstShot => Unlocked(BRD.AID.BurstShot) ? BRD.AID.BurstShot : BRD.AID.HeavyShot; - public BRD.AID BestRefulgentArrow => Unlocked(BRD.AID.RefulgentArrow) ? BRD.AID.RefulgentArrow : BRD.AID.StraightShot; - public BRD.AID BestCausticBite => Unlocked(BRD.AID.CausticBite) ? BRD.AID.CausticBite : BRD.AID.VenomousBite; - public BRD.AID BestStormbite => Unlocked(BRD.AID.Stormbite) ? BRD.AID.Stormbite : BRD.AID.Windbite; - public BRD.AID BestLadonsbite => Unlocked(BRD.AID.Ladonsbite) ? BRD.AID.Ladonsbite : BRD.AID.QuickNock; - - // statuses - public BRD.SID ExpectedCaustic => Unlocked(BRD.AID.CausticBite) ? BRD.SID.CausticBite : BRD.SID.VenomousBite; - public BRD.SID ExpectedStormbite => Unlocked(BRD.AID.Stormbite) ? BRD.SID.Stormbite : BRD.SID.Windbite; - - public bool Unlocked(BRD.AID aid) => Module.ActionUnlocked(ActionID.MakeSpell(aid)); - public bool Unlocked(BRD.TraitID tid) => Module.TraitUnlocked((uint)tid); - - public override string ToString() - { - return $"g={ActiveSong}/{ActiveSongLeft:f3}/{Repertoire}/{SoulVoice}/{NumCoda}, AOE={NumRainOfDeathTargets}/{NumLadonsbiteTargets}, no-dots={ForbidDOTs}, RB={RaidBuffsLeft:f3}, SS={HawkEyeLeft:f3}, BA={BlastArrowLeft:f3}, Buffs={RagingStrikesLeft:f3}/{BattleVoiceLeft:f3}/{RadiantFinaleLeft:f3}, Muse={ArmysMuseLeft:f3}, Barr={BarrageLeft:f3}, Dots={TargetStormbiteLeft:f3}/{TargetCausticLeft:f3}, PotCD={PotionCD:f3}, BVCD={CD(BRD.AID.BattleVoice):f3}, BLCD={CD(BRD.AID.Bloodletter):f3}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}"; - } - } - - private readonly State _state; - - public LegacyBRD(RotationModuleManager manager, Actor player) : base(manager, player) - { - _state = new(this); - } - - public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) - { - _state.UpdateCommon(primaryTarget, estimatedAnimLockDelay); - if (_state.AnimationLockDelay < 0.1f) - _state.AnimationLockDelay = 0.1f; // TODO: reconsider; we generally don't want triple weaves or extra-late proc weaves - - var gauge = World.Client.GetGauge(); - _state.ActiveSong = (Song)((byte)gauge.SongFlags & 3); - _state.ActiveSongLeft = gauge.SongTimer * 0.001f; - _state.Repertoire = gauge.Repertoire; - _state.SoulVoice = gauge.SoulVoice; - _state.NumCoda = BitOperations.PopCount((uint)gauge.SongFlags & 0x70); - - _state.HawkEyeLeft = _state.StatusDetails(Player, BRD.SID.HawksEye, Player.InstanceID, 30).Left; - _state.BlastArrowLeft = _state.StatusDetails(Player, BRD.SID.BlastArrowReady, Player.InstanceID, 10).Left; - _state.RagingStrikesLeft = _state.StatusDetails(Player, BRD.SID.RagingStrikes, Player.InstanceID, 20).Left; - _state.BattleVoiceLeft = _state.StatusDetails(Player, BRD.SID.BattleVoice, Player.InstanceID, 15).Left; - _state.RadiantFinaleLeft = _state.StatusDetails(Player, BRD.SID.RadiantFinale, Player.InstanceID, 15).Left; - _state.ArmysMuseLeft = _state.StatusDetails(Player, BRD.SID.ArmysMuse, Player.InstanceID, 10).Left; - _state.BarrageLeft = _state.StatusDetails(Player, BRD.SID.Barrage, Player.InstanceID, 10).Left; - _state.PelotonLeft = _state.StatusDetails(Player, BRD.SID.Peloton, Player.InstanceID, 30).Left; - - // TODO: multidot support - _state.TargetCausticLeft = _state.StatusDetails(primaryTarget, _state.ExpectedCaustic, Player.InstanceID, 45).Left; - _state.TargetStormbiteLeft = _state.StatusDetails(primaryTarget, _state.ExpectedStormbite, Player.InstanceID, 45).Left; - - var aoeStrategy = strategy.Option(Track.AOE).As(); - (_state.BestLadonsbiteTarget, _state.NumLadonsbiteTargets) = _state.Unlocked(BRD.AID.QuickNock) ? CheckAOETargeting(aoeStrategy, primaryTarget, 12, NumTargetsHitByLadonsbite, IsHitByLadonsbite) : (null, 0); - (_state.BestRainOfDeathTarget, _state.NumRainOfDeathTargets) = _state.Unlocked(BRD.AID.RainOfDeath) ? CheckAOETargeting(aoeStrategy, primaryTarget, 8, NumTargetsHitByRainOfDeath, IsHitByRainOfDeath) : (null, 0); - - // TODO: refactor all that, it's kinda senseless now - BRD.AID gcd = GetNextBestGCD(strategy); - PushResult(gcd, gcd is BRD.AID.QuickNock or BRD.AID.Ladonsbite ? _state.BestLadonsbiteTarget : primaryTarget); - - ActionID ogcd = default; - var deadline = _state.GCD > 0 && gcd != default ? _state.GCD : float.MaxValue; - if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline - _state.OGCDSlotLength); - if (!ogcd && _state.CanWeave(deadline)) // second/only ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline); - PushResult(ogcd, ogcd == ActionID.MakeSpell(BRD.AID.RainOfDeath) ? _state.BestRainOfDeathTarget : primaryTarget); - } - - //protected override void QueueAIActions() - //{ - // if (_state.Unlocked(AID.HeadGraze)) - // { - // var interruptibleEnemy = Autorot.Hints.PotentialTargets.Find(e => e.ShouldBeInterrupted && (e.Actor.CastInfo?.Interruptible ?? false) && e.Actor.Position.InCircle(Player.Position, 25 + e.Actor.HitboxRadius + Player.HitboxRadius)); - // SimulateManualActionForAI(ActionID.MakeSpell(AID.HeadGraze), interruptibleEnemy?.Actor, interruptibleEnemy != null); - // } - // 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.WardensPaean)) - // { - // var esunableTarget = FindEsunableTarget(); - // SimulateManualActionForAI(ActionID.MakeSpell(AID.WardensPaean), esunableTarget, esunableTarget != null); - // } - // if (_state.Unlocked(AID.Peloton)) - // SimulateManualActionForAI(ActionID.MakeSpell(AID.Peloton), Player, !Player.InCombat && _state.PelotonLeft < 3 && _strategy.ForceMovementIn == 0); - //} - - public override string DescribeState() => _state.ToString(); - - private int NumTargetsHitByLadonsbite(Actor primary) => Hints.NumPriorityTargetsInAOECone(Player.Position, 12, (primary.Position - Player.Position).Normalized(), 45.Degrees()); - private int NumTargetsHitByRainOfDeath(Actor primary) => Hints.NumPriorityTargetsInAOECircle(primary.Position, 8); - private bool IsHitByLadonsbite(Actor primary, Actor check) => Hints.TargetInAOECone(check, Player.Position, 12, (primary.Position - Player.Position).Normalized(), 45.Degrees()); - private bool IsHitByRainOfDeath(Actor primary, Actor check) => Hints.TargetInAOECircle(check, primary.Position, 8); - - private (Actor?, int) CheckAOETargeting(AOEStrategy strategy, Actor? primaryTarget, float range, Func numTargets, Func check) => strategy switch - { - AOEStrategy.AutoTargetHitPrimary => FindBetterTargetBy(primaryTarget, range, t => primaryTarget == null || check(t, primaryTarget) ? numTargets(t) : 0), - AOEStrategy.AutoTargetHitMost => FindBetterTargetBy(primaryTarget, range, numTargets), - AOEStrategy.AutoOnPrimary => (primaryTarget, primaryTarget != null ? numTargets(primaryTarget) : 0), - AOEStrategy.ForceAOE => (primaryTarget, int.MaxValue), - _ => (null, 0) - }; - - // old BRDRotation - private bool CanRefreshDOTsIn(int numGCDs) - { - var minLeft = Math.Min(_state.TargetStormbiteLeft, _state.TargetCausticLeft); - return minLeft > _state.GCD && minLeft <= _state.GCD + 2.5f * numGCDs; - } - - // heuristic to determine whether currently active dots were applied under raidbuffs (assumes dots are actually active) - // it's not easy to directly determine active dot potency - private bool AreActiveDOTsBuffed() - { - // dots last for 45s => their time of application Td = t + dotsLeft - 45 - // assuming we're using BV as the main buff, its cd is 120 => it was last used at Ts = t + bvcd - 120, and lasted until Te = Ts + 15 - // so dots are buffed if Ts < Td < Te => t + bvcd - 120 < t + dotsLeft - 45 < t + bvcd - 105 => bvcd - 75 < dotsLeft < bvcd - 60 - // this works when BV is off cd (dotsLeft < -60 is always false) - // this doesn't really work if dots are not up (can return true if bvcd is between 75 and 60) - var dotsLeft = Math.Min(_state.TargetStormbiteLeft, _state.TargetCausticLeft); - var bvCD = _state.CD(BRD.AID.BattleVoice); - return dotsLeft > bvCD - 75 && dotsLeft < bvCD - 60; - } - - // IJ generally has to be used at last possible gcd before dots fall off -or- before major buffs fall off (to snapshot buffs to dots), but in some cases we want to use it earlier: - // - 1 gcd earlier if we don't have RA proc (otherwise we might use filler, it would proc RA, then on next gcd we'll have to use IJ to avoid dropping dots and potentially waste another RA) - // - 1/2 gcds earlier if we're waiting for more gauge for AA - private bool ShouldUseIronJawsAutomatic(ApexArrowStrategy aaStrategy, OffensiveStrategy baStrategy) - { - var refreshDotsDeadline = Math.Min(_state.TargetStormbiteLeft, _state.TargetCausticLeft); - if (refreshDotsDeadline <= _state.GCD) - return false; // don't bother, we won't make it... - if (refreshDotsDeadline <= _state.GCD + 2.5f) - return true; // last possible gcd to refresh dots - just use IJ now - if (AreActiveDOTsBuffed()) - return false; // never extend buffed dots early: we obviously don't want to use multiple IJs in a single buff window, and outside buff window we don't want to overwrite buffed ticks, even if that means risking losing a proc - - // ok, dots aren't falling off imminently, and they are not buffed - see if we want to ij early and overwrite last ticks - if (_state.HawkEyeLeft <= _state.GCD && refreshDotsDeadline <= _state.GCD + 5 && !ShouldUseApexArrow(aaStrategy) && (_state.BlastArrowLeft <= _state.GCD || baStrategy == OffensiveStrategy.Delay)) - return true; // refresh 1 gcd early, if we would be forced to cast BS otherwise - if so, we could proc RA and then overwrite it by IJ on next gcd (TODO: i don't really like these conditions...) - if (_state.BattleVoiceLeft <= _state.GCD) - return false; // outside buff window, so no more reasons to extend early - - // under buffs, we might want to do early IJ, so that AA can be slightly delayed, or so that we don't risk proc overwrites - int maxRemainingGCDs = 1; // by default, refresh on last possible GCD before we either drop dots or drop major buffs - if (_state.HawkEyeLeft <= _state.GCD) - ++maxRemainingGCDs; // 1 extra gcd if we don't have RA proc (if we don't refresh early, we might use filler, which could give us a proc; then on next gcd we'll be forced to IJ to avoid dropping dots, which might give another proc) - // if we're almost at the gauge cap, we want to delay AA/BA (but still fit them into buff window), so we want to IJ earlier - if (_state.SoulVoice is > 50 and < 100) // best we can hope for over 4 gcds is ~25 gauge (4 ticks + EA) - TODO: improve condition - maxRemainingGCDs += _state.Unlocked(BRD.AID.BlastArrow) ? 2 : 1; // 1/2 gcds for AA/BA; only under buffs - outside buffs it's simpler to delay AA - return _state.BattleVoiceLeft <= _state.GCD + 2.5f * maxRemainingGCDs; - } - - private bool ShouldUseIronJaws(DotStrategy dotStrategy, ApexArrowStrategy aaStrategy, OffensiveStrategy baStrategy) => dotStrategy switch - { - DotStrategy.Forbid => false, - DotStrategy.ForceExtend => true, - DotStrategy.ExtendIgnoreBuffs => CanRefreshDOTsIn(_state.HawkEyeLeft <= _state.GCD ? 2 : 1), - DotStrategy.ExtendDelayed => CanRefreshDOTsIn(1), - _ => ShouldUseIronJawsAutomatic(aaStrategy, baStrategy) - }; - - // you get 5 gauge for every repertoire tick, meaning every 15s you get 5 gauge from EA + up to 25 gauge (*80% = 20 average) from songs - // using AA at 80+ gauge procs BA, meaning AA at <80 gauge is rarely worth it - private bool ShouldUseApexArrow(ApexArrowStrategy strategy) => strategy switch - { - ApexArrowStrategy.Delay => false, - ApexArrowStrategy.ForceAnyGauge => _state.SoulVoice > 0, - ApexArrowStrategy.ForceHighGauge => _state.SoulVoice >= 80, - ApexArrowStrategy.ForceCapGauge => _state.SoulVoice >= 100, - _ => _state.SoulVoice switch - { - >= 100 => _state.CD(BRD.AID.BattleVoice) >= _state.GCD + 45, // use asap, unless we are unlikely to have 80+ gauge by the next buff window (TODO: reconsider time limit) - >= 80 => _state.BattleVoiceLeft > _state.GCD - ? _state.BattleVoiceLeft < _state.GCD + 5 // under buffs, don't delay AA if doing that will make BA miss buffs (TODO: also don't delay if it can drift barrage past third gcd...) - : _state.CD(BRD.AID.BattleVoice) - _state.GCD is >= 45 and < 55, // outside buffs, delay unless we risk entering a window where next buffs are imminent and we can't AA (TODO: reconsider window size) - _ => false // never use AA at <80 gauge automatically; assume manual planning for things like end-of-fight or downtimes - } - }; - - private float SwitchAtRemainingSongTimer(SongStrategy strategy) => strategy switch - { - SongStrategy.Automatic => _state.ActiveSong switch - { - Song.WanderersMinuet => 3, // WM->MB transition when no more repertoire ticks left - Song.MagesBallad => _state.NumRainOfDeathTargets < 3 ? 12 : 3, // MB->AP transition asap as long as we won't end up songless (active song condition 15 == 45 - (120 - 2*45); get extra MB tick at 12s to avoid being songless for a moment), unless we're doing aoe rotation - Song.ArmysPaeon => _state.Repertoire == 4 ? 15 : 3, // AP->WM transition asap as long as we'll have MB ready when WM ends, if we either have full repertoire or AP is about to run out anyway - _ => 3 - }, - SongStrategy.Extend => 3, - SongStrategy.Overextend => 0, // TODO: think more about it... - SongStrategy.ForcePP => _state.Repertoire > 0 ? 0 : 3, // if we still have PP charges, don't switch; otherwise switch after last tick (assuming we're under WM) - _ => 3 - }; - - private bool ShouldUsePotion(PotionStrategy strategy) => strategy switch - { - PotionStrategy.Manual => false, - PotionStrategy.Burst => !Player.InCombat ? _state.CountdownRemaining < 2 : _state.TargetingEnemy && _state.CD(BRD.AID.RagingStrikes) < _state.GCD + 3.5f, // pre-pull or RS ready in 2 gcds (assume pot -> late-weaved WM -> RS) - PotionStrategy.Force => true, - _ => false - }; - - // by default, we use RS asap as soon as WM is up - private bool ShouldUseRagingStrikes(OffensiveStrategy strategy) => strategy switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => _state.TargetingEnemy && (_state.ActiveSong == Song.WanderersMinuet || !_state.Unlocked(BRD.AID.WanderersMinuet)) - }; - - // by default, we pool bloodletter for burst - private bool ShouldUseBloodletter(BloodletterStrategy strategy) => strategy switch - { - BloodletterStrategy.Delay => false, - BloodletterStrategy.Force => true, - BloodletterStrategy.KeepOneCharge => _state.CD(BRD.AID.Bloodletter) <= 15 + _state.AnimationLock, - BloodletterStrategy.KeepTwoCharges => _state.Unlocked(BRD.TraitID.EnhancedBloodletter) && _state.CD(BRD.AID.Bloodletter) <= _state.AnimationLock, - _ => !_state.Unlocked(BRD.AID.WanderersMinuet) || // don't try to pool BLs at low level (reconsider) - _state.ActiveSong == Song.MagesBallad || // don't try to pool BLs during MB, it's risky - _state.BattleVoiceLeft > _state.AnimationLock || // don't pool BLs during buffs - _state.CD(BRD.AID.Bloodletter) - (_state.Unlocked(BRD.TraitID.EnhancedBloodletter) ? 0 : 15) <= Math.Min(_state.CD(BRD.AID.RagingStrikes), _state.CD(BRD.AID.BattleVoice)) // don't pool BLs if they will overcap before next buffs - }; - - // by default, we use EA asap if in combat - private bool ShouldUseEmpyrealArrow(OffensiveStrategy strategy) => strategy switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat - }; - - // by default, we use barrage under raid buffs, being careful not to overwrite RA proc - // TODO: reconsider barrage usage during aoe - private bool ShouldUseBarrage(OffensiveStrategy strategy) => strategy switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat // in combat - && (_state.Unlocked(BRD.AID.BattleVoice) ? _state.BattleVoiceLeft : _state.RagingStrikesLeft) > 0 // and under raid buffs - && (_state.NumLadonsbiteTargets < 2 - ? _state.HawkEyeLeft <= _state.GCD // in non-aoe situation - if there is no RA proc already - : _state.NumLadonsbiteTargets >= 4 && _state.HawkEyeLeft > _state.GCD) // in aoe situations - use on shadowbite on 4+ targets (TODO: verify!!!) - }; - - // by default, we use sidewinder asap, unless raid buffs are imminent - private bool ShouldUseSidewinder(OffensiveStrategy strategy) => strategy switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat && _state.CD(BRD.AID.BattleVoice) > 45 // TODO: consider exact delay condition - }; - - private BRD.AID GetNextBestGCD(StrategyValues strategy) - { - // prepull or no target - if (!_state.TargetingEnemy || _state.CountdownRemaining > 0.7f) - return BRD.AID.None; - - if (_state.NumLadonsbiteTargets >= 2) - { - // TODO: AA/BA targeting/condition (it might hit fewer targets) - if (_state.BlastArrowLeft > _state.GCD && strategy.Option(Track.BlastArrow).As() != OffensiveStrategy.Delay) - return BRD.AID.BlastArrow; - if (ShouldUseApexArrow(strategy.Option(Track.ApexArrow).As())) - return BRD.AID.ApexArrow; - - // TODO: barraged RA on 3 targets?.. - // TODO: better shadowbite targeting (it might hit fewer targets) - return _state.HawkEyeLeft > _state.GCD ? BRD.AID.Shadowbite : _state.BestLadonsbite; - } - else - { - var strategyDOTs = strategy.Option(Track.DOTs).As(); - var forbidApplyDOTs = _state.ForbidDOTs || strategyDOTs is DotStrategy.AutomaticExtendOnly or DotStrategy.Forbid; - if (_state.Unlocked(BRD.AID.IronJaws)) - { - // apply dots if not up and allowed by strategy - if (!forbidApplyDOTs && _state.TargetStormbiteLeft <= _state.GCD) - return _state.BestStormbite; - if (!forbidApplyDOTs && _state.TargetCausticLeft <= _state.GCD) - return _state.BestCausticBite; - - // at this point, we have to prioritize IJ, AA/BA and RA procs - var strategyAA = strategy.Option(Track.ApexArrow).As(); - var strategyBA = strategy.Option(Track.BlastArrow).As(); - if (!_state.ForbidDOTs && ShouldUseIronJaws(strategyDOTs, strategyAA, strategyBA)) - return BRD.AID.IronJaws; - - // there are cases where we want to prioritize RA over AA/BA: - // - if barrage is about to come off CD, we don't want to delay it needlessly - // - if delaying RA would force us to IJ on next gcd (potentially overwriting proc) - // we only do that if there are no explicit AA/BA force strategies (in that case we assume just doing AA/BA is more important than wasting a proc) - bool highPriorityRA = _state.HawkEyeLeft > _state.GCD // RA ready - && strategyAA is ApexArrowStrategy.Automatic or ApexArrowStrategy.Delay // no forced AA - && strategyBA != OffensiveStrategy.Force // no forced BA - && (_state.CD(BRD.AID.Barrage) < _state.GCD + 2.5f || CanRefreshDOTsIn(2)); // either barrage coming off cd or dots falling off imminent - if (highPriorityRA) - return _state.BestRefulgentArrow; - - // BA if possible and not forbidden - if (_state.BlastArrowLeft > _state.GCD && strategyBA != OffensiveStrategy.Delay) - return BRD.AID.BlastArrow; - - // AA depending on conditions - if (ShouldUseApexArrow(strategyAA)) - return BRD.AID.ApexArrow; - - // RA/BS - return _state.HawkEyeLeft > _state.GCD ? _state.BestRefulgentArrow : _state.BestBurstShot; - } - else - { - // pre IJ our gcds are extremely boring: keep dots up and use up straight shot procs asap - // only HS can proc straight shot, so we're not wasting potential procs here - // TODO: tweak threshold so that we don't overwrite or miss ticks... - // TODO: do we care about reapplying dots early under raidbuffs?.. - if (!forbidApplyDOTs && _state.Unlocked(BRD.AID.Windbite) && _state.TargetStormbiteLeft < _state.GCD + 3) - return BRD.AID.Windbite; - if (!forbidApplyDOTs && _state.Unlocked(BRD.AID.VenomousBite) && _state.TargetCausticLeft < _state.GCD + 3) - return BRD.AID.VenomousBite; - return _state.HawkEyeLeft > _state.GCD ? BRD.AID.StraightShot : BRD.AID.HeavyShot; - } - } - } - - private ActionID GetNextBestOGCD(StrategyValues strategy, float deadline) - { - // potion - var strategyPotion = strategy.Option(Track.Potion).As(); - if (ShouldUsePotion(strategyPotion) && _state.CanWeave(_state.PotionCD, 1.1f, deadline)) - return ActionDefinitions.IDPotionDex; - - // maintain songs - var strategySongs = strategy.Option(Track.Songs).As(); - if (_state.TargetingEnemy && Player.InCombat && strategySongs != SongStrategy.Delay) - { - if (strategySongs == SongStrategy.ForceWM && _state.Unlocked(BRD.AID.WanderersMinuet) && _state.CanWeave(BRD.AID.WanderersMinuet, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.WanderersMinuet); - if (strategySongs == SongStrategy.ForceMB && _state.Unlocked(BRD.AID.MagesBallad) && _state.CanWeave(BRD.AID.MagesBallad, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.MagesBallad); - if (strategySongs == SongStrategy.ForceAP && _state.Unlocked(BRD.AID.ArmysPaeon) && _state.CanWeave(BRD.AID.ArmysPaeon, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.ArmysPaeon); - - if (_state.ActiveSong == Song.None) - { - // if no song is up, use best available one - if (_state.Unlocked(BRD.AID.WanderersMinuet) && _state.CanWeave(BRD.AID.WanderersMinuet, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.WanderersMinuet); - if (_state.Unlocked(BRD.AID.MagesBallad) && _state.CanWeave(BRD.AID.MagesBallad, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.MagesBallad); - if (_state.Unlocked(BRD.AID.ArmysPaeon) && _state.CanWeave(BRD.AID.ArmysPaeon, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.ArmysPaeon); - } - else if (_state.Unlocked(BRD.AID.WanderersMinuet) && _state.ActiveSongLeft < SwitchAtRemainingSongTimer(strategySongs) - 0.1f) // TODO: rethink this extra leeway, we want to make sure we use up last tick's repertoire e.g. in WM - { - // once we have WM, we can have a proper song cycle - if (_state.ActiveSong == Song.WanderersMinuet) - { - if (_state.Repertoire > 0 && _state.CanWeave(BRD.AID.PitchPerfect, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.PitchPerfect); // spend remaining repertoire before leaving WM - if (_state.CanWeave(BRD.AID.MagesBallad, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.MagesBallad); - } - if (_state.ActiveSong == Song.MagesBallad && _state.CD(BRD.AID.WanderersMinuet) < 45 && _state.CanWeave(BRD.AID.ArmysPaeon, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.ArmysPaeon); - if (_state.ActiveSong == Song.ArmysPaeon && _state.CD(BRD.AID.MagesBallad) < 45 && _state.CanWeave(BRD.AID.WanderersMinuet, 0.6f, deadline) && _state.GCD < 0.9f) - return ActionID.MakeSpell(BRD.AID.WanderersMinuet); // late-weave - } - } - - // apply major buffs - // RS as soon as we enter WM (or just on CD, if we don't have it yet) - // in opener, it end up being late-weaved after WM (TODO: can we weave it extra-late to ensure 9th gcd is buffed?) - // in 2-minute bursts, it ends up being early-weaved after first WM gcd (TODO: can we weave it later to ensure 10th gcd is buffed?) - var strategyRS = strategy.Option(Track.RagingStrikes).As(); - if (_state.Unlocked(BRD.AID.RagingStrikes) && ShouldUseRagingStrikes(strategyRS) && _state.CanWeave(BRD.AID.RagingStrikes, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.RagingStrikes); - - // BV+RF 2 gcds after RS (RF first with 1 coda, ? with 2 coda, BV first with 3 coda) - // visualization: - // -GCD 0 GCD - // * -gcd---------- * -gcd---------- * -gcd---------- * -gcd---------- - // * ---- RS ------ * -------------- * ---- BV - RF - * -------------- - // ^^^^----------------^^^----------^^^ - // 20s 20s max min - // GCD is slightly smaller than 2.5 during opener, and slightly smaller than 2.1 during reopener (assuming 4-stack AP) - // RS should happen in second ogcd slot during opener (t in [-gcd + 1.2, -0.6] == [-1.3 + sksDelta, -0.6], or anywhere during burst (t in [-gcd + 0.6, -0.6] == [-1.5 + sksDelta, -0.6]) - // RS buff starts ticking down from 20 at t+erDelay, so we can imagine that RS has effective time-left == 20+erDelay when applied - // we want to enable BV/RF between [gcd-0.6, gcd+0.6] - // at T=gcd RS buff will have remaining 20+erDelay-(T-t) == [opener] 20+erDelay-(2.5-sksDelta)+[-1.3+sksDelta, -0.6] == 17.5+erDelay+sksDelta+[-1.3+sksDelta, -0.6] == [16.2+sksDelta, 16.9]+erDelay+sksDelta - // == [burst] 20+erDelay-(2.1-sksDelta)+[-1.5+sksDelta, -0.6] == 17.9+erDelay+sksDelta+[-1.5+sksDelta, -0.6] == [16.4+sksDelta, 17.3]+erDelay+sksDelta - // so condition is [opener] RSLeft <= 16.8 + [sksDelta, 0.7]+erDelay+sksDelta - // [burst] RSLeft <= 17.0 + [sksDelta, 0.9]+erDelay+sksDelta - // but note that if we select too small limit (e.g. 16.8), we might run into a problem: if 0.7+erDelay+sksDelta > 1.2, then we might not allow using buff at min, use something else instead, and push second buff to next GCD - if (_state.TargetingEnemy && _state.RagingStrikesLeft > _state.AnimationLock && _state.RagingStrikesLeft < 17) - { - if (_state.NumCoda == 1 && _state.CanWeave(BRD.AID.RadiantFinale, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.RadiantFinale); - if (_state.Unlocked(BRD.AID.BattleVoice) && _state.CanWeave(BRD.AID.BattleVoice, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.BattleVoice); - if (_state.NumCoda > 1 && _state.CanWeave(BRD.AID.RadiantFinale, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.RadiantFinale); - } - - // TODO: consider moving PP3 check here and delay EA if we actually fuck up - - // EA - important not to drift (TODO: is it actually better to delay it if we're capped on PP/BL?) - // we should not be at risk of capping BL (since we spend charges asap in WM/MB anyway) - // we might risk capping PP, but we should've dealt with that on previous slots by using PP2 - // TODO: consider clipping gcd to avoid ea drift... - var strategyEA = strategy.Option(Track.EmpyrealArrow).As(); - if (_state.TargetingEnemy && ShouldUseEmpyrealArrow(strategyEA) && _state.Unlocked(BRD.AID.EmpyrealArrow) && _state.CanWeave(BRD.AID.EmpyrealArrow, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.EmpyrealArrow); - - // PP here should not conflict with anything priority-wise - // note that we already handle PPx after last repertoire tick before switching to WM (see song cycle code above) - if (_state.TargetingEnemy && _state.ActiveSong == Song.WanderersMinuet && _state.Repertoire > 0 && _state.CanWeave(BRD.AID.PitchPerfect, 0.6f, deadline)) - { - if (_state.Repertoire == 3) - return ActionID.MakeSpell(BRD.AID.PitchPerfect); // PP3 is a no-brainer - - if (strategySongs == SongStrategy.ForcePP) - return ActionID.MakeSpell(BRD.AID.PitchPerfect); // PPx if strategy says so - - var nextProcIn = _state.ActiveSongLeft % 3.0f; - if (_state.BattleVoiceLeft > _state.AnimationLock && _state.BattleVoiceLeft <= nextProcIn + 1) - return ActionID.MakeSpell(BRD.AID.PitchPerfect); // PPx if we won't get any more stacks under buffs (TODO: better leeway) - - // if we're at PP2 and EA is about to come off cd, we might be in a situation where waiting for PP3 would have us choose between delaying EA or wasting its guaranteed proc; in such case we want to PP2 early - if (_state.Repertoire == 2) - { - bool usePP2 = false; - if (_state.CanWeave(BRD.AID.EmpyrealArrow, 0.6f, _state.GCD)) - { - // we're going to use EA in next ogcd slot before GCD => use PP2 if we won't be able to wait until tick and weave before EA - usePP2 = !_state.CanWeave(nextProcIn, 0.6f, _state.CD(BRD.AID.EmpyrealArrow)); - } - else if (_state.CD(BRD.AID.EmpyrealArrow) < _state.GCD + 1.2f) - { - // we're going to use EA just after next GCD => use PP2 if we won't be able to wait until tick and late-weave before next GCD - usePP2 = !_state.CanWeave(nextProcIn, 0.6f, _state.GCD); - } - - if (usePP2) - return ActionID.MakeSpell(BRD.AID.PitchPerfect); // PP2 if we might get conflict with EA - } - } - - // barrage, under buffs and if there is no proc already - // TODO: consider moving up to avoid drifting? seems risky... - var strategyBarrage = strategy.Option(Track.Barrage).As(); - if (_state.TargetingEnemy && ShouldUseBarrage(strategyBarrage) && _state.Unlocked(BRD.AID.Barrage) && _state.CanWeave(BRD.AID.Barrage, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.Barrage); - - // sidewinder, unless we're delaying it until buffs - var strategySW = strategy.Option(Track.Sidewinder).As(); - if (_state.TargetingEnemy && ShouldUseSidewinder(strategySW) && _state.Unlocked(BRD.AID.Sidewinder) && _state.CanWeave(BRD.AID.Sidewinder, 0.6f, deadline)) - return ActionID.MakeSpell(BRD.AID.Sidewinder); - - // bloodletter, unless we're pooling them for burst - var strategyBL = strategy.Option(Track.Bloodletter).As(); - if (_state.TargetingEnemy && Player.InCombat && _state.Unlocked(BRD.AID.Bloodletter) && ShouldUseBloodletter(strategyBL) && _state.CanWeave(_state.CD(BRD.AID.Bloodletter) - 30, 0.6f, deadline)) - return ActionID.MakeSpell(_state.NumRainOfDeathTargets >= 2 ? BRD.AID.RainOfDeath : BRD.AID.Bloodletter); - - // no suitable oGCDs... - return new(); - } -} diff --git a/BossMod/Autorotation/Legacy/LegacyDNC.cs b/BossMod/Autorotation/Legacy/LegacyDNC.cs deleted file mode 100644 index 95562543b4..0000000000 --- a/BossMod/Autorotation/Legacy/LegacyDNC.cs +++ /dev/null @@ -1,513 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; - -namespace BossMod.Autorotation.Legacy; - -public sealed class LegacyDNC : LegacyModule -{ - public enum Track { AOE, Gauge, Feather, TechStep, StdStep, PauseDuringImprov, AutoPartner } - public enum AOEStrategy { SingleTarget, AutoOnPrimary } - public enum OffensiveStrategy { Automatic, Delay, Force } - public enum ImprovStrategy { Normal, Pause } - public enum PartnerStrategy { Automatic, Manual } - - public static RotationModuleDefinition Definition() - { - // TODO: think about target overrides where they make sense - var res = new RotationModuleDefinition("Legacy DNC", "Old pre-refactoring module", "Legacy (pre-DT)", "xan", RotationModuleQuality.WIP, BitMask.Build((int)Class.DNC), 100); - - res.Define(Track.AOE).As("AOE", uiPriority: 90) - .AddOption(AOEStrategy.SingleTarget, "ST", "Use single-target actions") - .AddOption(AOEStrategy.AutoOnPrimary, "AutoOnPrimary", "Use aoe actions on primary target if profitable"); - - res.Define(Track.Gauge).As("Gauge", uiPriority: 80) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.Feather).As("Feather", uiPriority: 70) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.TechStep).As("TechStep", uiPriority: 60) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "On cooldown, if there are enemies") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.StdStep).As("StdStep", uiPriority: 50) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "On cooldown, if there are enemies") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.PauseDuringImprov).As("PauseDuringImprov", uiPriority: 40) - .AddOption(ImprovStrategy.Normal, "Normal") - .AddOption(ImprovStrategy.Pause, "Pause", "Pause autorotation while Improvisation is active"); - - res.Define(Track.AutoPartner).As("AutoPartner", uiPriority: 30) - .AddOption(PartnerStrategy.Automatic, "Automatic", "Automatically choose dance partner") - .AddOption(PartnerStrategy.Manual, "Manual"); - - return res; - } - - // full state needed for determining next action - public class State(RotationModule module) : CommonState(module) - { - public byte Feathers; - public bool IsDancing; - public byte CompletedSteps; - public uint NextStep; - public byte Esprit; - - public float StandardStepLeft; // 15s max - public float StandardFinishLeft; // 60s max - public float TechStepLeft; // 15s max - public float TechFinishLeft; // 20s max - public float FlourishingFinishLeft; // 30s max, granted by tech step - public float ImprovisationLeft; // 15s max - public float ImprovisedFinishLeft; // 30s max - public float DevilmentLeft; // 20s max - public float SymmetryLeft; // 30s max - public float FlowLeft; // 30s max - public float FlourishingStarfallLeft; // 20s max - public float ThreefoldLeft; // 30s max - public float FourfoldLeft; // 30s max - public float PelotonLeft; - - public bool PauseDuringImprov; - public bool AutoPartner; - - public int NumDanceTargets; // 15y around self - public int NumAOETargets; // 5y around self - public int NumRangedAOETargets; // 5y around target - Saber Dance, Fan3 - public int NumFan4Targets; // 15y/120deg cone - public int NumStarfallTargets; // 25/4 rect - - public DNC.AID ComboLastMove => (DNC.AID)ComboLastAction; - - public DNC.AID BestStandardStep - { - get - { - if (StandardStepLeft <= GCD) - return DNC.AID.StandardStep; - - return CompletedSteps switch - { - 0 => DNC.AID.StandardFinish, - 1 => DNC.AID.SingleStandardFinish, - _ => DNC.AID.DoubleStandardFinish, - }; - } - } - - public DNC.AID BestTechStep - { - get - { - if (FlourishingFinishLeft > GCD && Unlocked(DNC.AID.Tillana)) - return DNC.AID.Tillana; - if (TechStepLeft <= GCD) - return DNC.AID.TechnicalStep; - - return CompletedSteps switch - { - 0 => DNC.AID.TechnicalFinish, - 1 => DNC.AID.SingleTechnicalFinish, - 2 => DNC.AID.DoubleTechnicalFinish, - 3 => DNC.AID.TripleTechnicalFinish, - _ => DNC.AID.QuadrupleTechnicalFinish - }; - } - } - - public DNC.AID BestImprov => ImprovisationLeft > 0 ? DNC.AID.ImprovisedFinish : DNC.AID.Improvisation; - - public bool Unlocked(DNC.AID aid) => Module.ActionUnlocked(ActionID.MakeSpell(aid)); - public bool Unlocked(DNC.TraitID tid) => Module.TraitUnlocked((uint)tid); - - public override string ToString() - { - return $"AOE={NumAOETargets}/Fan3 {NumRangedAOETargets}/Fan4 {NumFan4Targets}/Star {NumStarfallTargets}, Dance={NumDanceTargets}, T={TechFinishLeft:f2}, S={StandardFinishLeft:f2}, C3={SymmetryLeft:f2}, C4={FlowLeft:f2}, Fan3={ThreefoldLeft:f2}, Fan4={FourfoldLeft:f2}, E={Esprit}, PotCD={PotionCD:f3}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}"; - } - } - - private readonly State _state; - private bool _predictedTechFinish; // TODO: find a way to remove that - - private const float FinishDanceWindow = 0.5f; - - public LegacyDNC(RotationModuleManager manager, Actor player) : base(manager, player) - { - _state = new(this); - } - - public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) - { - _state.UpdateCommon(primaryTarget, estimatedAnimLockDelay); - _state.AnimationLockDelay = MathF.Max(0.1f, _state.AnimationLockDelay); - - var gauge = World.Client.GetGauge(); - var curStep = (uint)gauge.CurrentStep; - - _state.Feathers = gauge.Feathers; - _state.IsDancing = gauge.DanceSteps[0] != 0; - _state.CompletedSteps = gauge.StepIndex; - _state.NextStep = curStep > 0 ? curStep + 15998 : curStep; - _state.Esprit = gauge.Esprit; - - _state.StandardStepLeft = StatusLeft(DNC.SID.StandardStep); - _state.StandardFinishLeft = StatusLeft(DNC.SID.StandardFinish); - _state.TechStepLeft = StatusLeft(DNC.SID.TechnicalStep); - _state.TechFinishLeft = StatusLeft(DNC.SID.TechnicalFinish); - _state.FlourishingFinishLeft = StatusLeft(DNC.SID.FlourishingFinish); - _state.ImprovisationLeft = StatusLeft(DNC.SID.Improvisation); - _state.ImprovisedFinishLeft = StatusLeft(DNC.SID.ImprovisedFinish); - _state.DevilmentLeft = StatusLeft(DNC.SID.Devilment); - _state.SymmetryLeft = MathF.Max(StatusLeft(DNC.SID.SilkenSymmetry), StatusLeft(DNC.SID.FlourishingSymmetry)); - _state.FlowLeft = MathF.Max(StatusLeft(DNC.SID.SilkenFlow), StatusLeft(DNC.SID.FlourishingFlow)); - _state.FlourishingStarfallLeft = StatusLeft(DNC.SID.FlourishingStarfall); - _state.ThreefoldLeft = StatusLeft(DNC.SID.ThreefoldFanDance); - _state.FourfoldLeft = StatusLeft(DNC.SID.FourfoldFanDance); - - var pelo = Player.FindStatus((uint)DNC.SID.Peloton); - if (pelo != null) - _state.PelotonLeft = _state.StatusDuration(pelo.Value.ExpireAt); - else - _state.PelotonLeft = 0; - - // there seems to be a delay between tech finish use and buff application in full parties - maybe it's a - // cascading buff that is applied to self last? anyway, the delay can cause the rotation to skip the - // devilment weave window that occurs right after tech finish since it doesn't think we have tech finish yet - // TODO: this is not very robust (eg player could die between action and buff application), investigate why StatusDetail doesn't pick it up from pending statuses... - if (_predictedTechFinish) - { - if (_state.TechFinishLeft == 0) - _state.TechFinishLeft = 1000f; - else - _predictedTechFinish = false; - } - - _state.PauseDuringImprov = strategy.Option(Track.PauseDuringImprov).As() == ImprovStrategy.Pause; - _state.AutoPartner = strategy.Option(Track.AutoPartner).As() == PartnerStrategy.Automatic; - - // TODO: aoe targeting; see how BRD/DRG do aoes - //if (_state.FlourishingStarfallLeft > _state.GCD && _state.Unlocked(AID.StarfallDance)) - // return SelectBestTarget(initial, 25, NumStarfallTargets); - //if (_state.CD(DNC.AID.Devilment) > 0 && _state.FourfoldLeft > _state.AnimationLock) - // return SelectBestTarget(initial, 15, NumFan4Targets); - //// default for saber dance and fan3 - //// TODO: look for enemies we can aoe and move closer? - //return SelectBestTarget(initial, 25, NumAOETargets); _state.NumDanceTargets = Hints.NumPriorityTargetsInAOECircle(Player.Position, 15); - var aoeStrategy = strategy.Option(Track.AOE).As(); - _state.NumAOETargets = aoeStrategy == AOEStrategy.SingleTarget ? 0 : NumAOETargets(Player); - _state.NumRangedAOETargets = primaryTarget == null ? 0 : NumAOETargets(primaryTarget); - _state.NumFan4Targets = primaryTarget == null ? 0 : NumFan4Targets(primaryTarget); - _state.NumStarfallTargets = primaryTarget == null ? 0 : NumStarfallTargets(primaryTarget); - - // TODO: refactor all that, it's kinda senseless now - DNC.AID gcd = GetNextBestGCD(strategy); - PushResult(gcd, primaryTarget); - - var deadline = _state.GCD > 0 && gcd != default ? _state.GCD : float.MaxValue; - if (_state.AutoPartner && _state.Unlocked(DNC.AID.ClosedPosition) && StatusLeft(DNC.SID.ClosedPosition) == 0 && _state.CanWeave(DNC.AID.ClosedPosition, 0.6f, deadline) && FindDancePartner() is var partner && partner != null) - { - PushResult(ActionID.MakeSpell(DNC.AID.ClosedPosition), partner); - } - else - { - ActionID ogcd = default; - if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline - _state.OGCDSlotLength); - if (!ogcd && _state.CanWeave(deadline)) // second/only ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline); - PushResult(ogcd, primaryTarget); - } - } - - //protected override void QueueAIActions() - //{ - // if (_state.Unlocked(DNC.AID.HeadGraze)) - // { - // var interruptibleEnemy = Hints.PotentialTargets.Find(e => e.ShouldBeInterrupted && (e.Actor.CastInfo?.Interruptible ?? false) && e.Actor.Position.InCircle(Player.Position, 25 + e.Actor.HitboxRadius + Player.HitboxRadius)); - // SimulateManualActionForAI(ActionID.MakeSpell(DNC.AID.HeadGraze), interruptibleEnemy?.Actor, interruptibleEnemy != null); - // } - // if (_state.Unlocked(DNC.AID.Peloton)) - // SimulateManualActionForAI(ActionID.MakeSpell(DNC.AID.Peloton), Player, !Player.InCombat && _state.PelotonLeft < 3 && _strategy.ForceMovementIn == 0); - // if (_state.Unlocked(DNC.AID.CuringWaltz)) - // { - // SimulateManualActionForAI(ActionID.MakeSpell(DNC.AID.CuringWaltz), Player, Player.InCombat && Player.HPMP.CurHP < Player.HPMP.MaxHP * 0.75f); - // } - // if (_state.Unlocked(DNC.AID.SecondWind)) - // { - // SimulateManualActionForAI(ActionID.MakeSpell(DNC.AID.SecondWind), Player, Player.InCombat && Player.HPMP.CurHP < Player.HPMP.MaxHP * 0.5f); - // } - //} - - // TODO: this won't work reliably, since modules can be destroyed and recreated at any time - //protected override void OnActionSucceeded(ActorCastEvent ev) - //{ - // if ((DNC.AID)ev.Action.ID is DNC.AID.TechnicalFinish or DNC.AID.SingleTechnicalFinish or DNC.AID.DoubleTechnicalFinish or DNC.AID.TripleTechnicalFinish or DNC.AID.QuadrupleTechnicalFinish) - // _predictedTechFinish = true; - //} - - public override string DescribeState() => _state.ToString(); - - //private Targeting SelectBestTarget(AIHints.Enemy initial, float maxDistanceFromPlayer, Func prio) - //{ - // var newBest = FindBetterTargetBy(initial, maxDistanceFromPlayer, x => prio(x.Actor)).Target; - // return new(newBest, newBest.StayAtLongRange ? 25 : 15); - //} - - private float StatusLeft(DNC.SID status) => _state.StatusDetails(Player, status, Player.InstanceID).Left; - - private Actor? FindDancePartner() => World.Party.WithoutSlot().Exclude(Player).MaxBy(p => p.Class switch - { - Class.SAM => 100, - Class.NIN => 99, - Class.MNK => 88, - Class.RPR => 87, - Class.DRG => 86, - Class.BLM => 79, - Class.SMN => 78, - Class.RDM => 77, - Class.MCH => 69, - Class.BRD => 68, - Class.DNC => 67, - _ => 1 - }); - - private int NumAOETargets(Actor origin) => Hints.NumPriorityTargetsInAOECircle(origin.Position, 5); - private int NumFan4Targets(Actor primary) => Hints.NumPriorityTargetsInAOECone(Player.Position, 15, (primary.Position - Player.Position).Normalized(), 60.Degrees()); - private int NumStarfallTargets(Actor primary) => Hints.NumPriorityTargetsInAOERect(Player.Position, (primary.Position - Player.Position).Normalized(), 25, 4); - - // old DNCRotation - private DNC.AID GetNextBestGCD(StrategyValues strategy) - { - if (ShouldDoNothing()) - return DNC.AID.None; - - if (_state.IsDancing) - { - if (_state.NextStep != 0) - return (DNC.AID)_state.NextStep; - - if (ShouldFinishDance(_state.StandardStepLeft)) - return _state.BestStandardStep; - if (ShouldFinishDance(_state.TechStepLeft)) - return _state.BestTechStep; - - return DNC.AID.None; - } - - if (_state.CountdownRemaining > 0) - { - if (_state.CountdownRemaining is < 15.5f and > 3.5f && !_state.IsDancing && _state.Unlocked(DNC.AID.StandardStep)) - return DNC.AID.StandardStep; - - return DNC.AID.None; - } - - if (ShouldTechStep(strategy)) - return DNC.AID.TechnicalStep; - - var shouldStdStep = ShouldStdStep(strategy); - - // priority for cdplan - if (strategy.Option(Track.StdStep).As() == OffensiveStrategy.Force && shouldStdStep) - return DNC.AID.StandardStep; - - var canStarfall = _state.FlourishingStarfallLeft > _state.GCD && _state.NumStarfallTargets > 0; - var canFlow = CanFlow(out var flowCombo); - var canSymmetry = CanSymmetry(out var symmetryCombo); - var combo2 = _state.NumAOETargets > 1 ? DNC.AID.Bladeshower : DNC.AID.Fountain; - var haveCombo2 = _state.Unlocked(combo2) && _state.ComboLastMove == (_state.NumAOETargets > 1 ? DNC.AID.Windmill : DNC.AID.Cascade); - - // prevent starfall expiration - if (canStarfall && _state.FlourishingStarfallLeft <= _state.AttackGCDTime) - return DNC.AID.StarfallDance; - - // prevent flow expiration - if (canFlow && _state.FlowLeft <= _state.AttackGCDTime) - return flowCombo; - - // prevent symmetry expiration - if (canSymmetry && _state.SymmetryLeft <= _state.AttackGCDTime) - return symmetryCombo; - - // prevent saber overcap - if (ShouldSaberDance(strategy, 85)) - return DNC.AID.SaberDance; - - // starfall dance - if (canStarfall) - return DNC.AID.StarfallDance; - - // prevent combo2 expiration - if (haveCombo2 && _state.ComboTimeLeft < _state.AttackGCDTime * 2) - { - // use flow first if we have it so combo2 doesn't overwrite proc - if (canFlow) - return flowCombo; - - if (_state.ComboTimeLeft < _state.AttackGCDTime) - return combo2; - } - - // tillana - if (_state.FlourishingFinishLeft > _state.GCD && _state.CD(DNC.AID.Devilment) > 0 && _state.NumDanceTargets > 0) - return DNC.AID.Tillana; - - // buffed saber dance - if (_state.TechFinishLeft > _state.GCD && ShouldSaberDance(strategy, 50)) - return DNC.AID.SaberDance; - - // unbuffed standard step - combos 3 and 4 are higher priority in raid buff window - // skip if tech step is around 5s cooldown or lower since std step would delay it - if (_state.TechFinishLeft == 0 && shouldStdStep && (_state.CD(DNC.AID.TechnicalStep) > _state.GCD + 5 || !_state.Unlocked(DNC.AID.TechnicalStep))) - return DNC.AID.StandardStep; - - // combo 3 - if (canFlow) - return flowCombo; - // combo 4 - if (canSymmetry) - return symmetryCombo; - - // (possibly buffed) standard step - if (shouldStdStep) - return DNC.AID.StandardStep; - - if (haveCombo2) - return combo2; - - return _state.NumAOETargets > 1 && _state.Unlocked(DNC.AID.Windmill) ? DNC.AID.Windmill - : _state.TargetingEnemy ? DNC.AID.Cascade - : DNC.AID.None; - } - - private ActionID GetNextBestOGCD(StrategyValues strategy, float deadline) - { - if (ShouldDoNothing()) - return new(); - - if (_state.CountdownRemaining is < 10 and > 2 && _state.NextStep == 0 && _state.PelotonLeft == 0 && _state.Unlocked(DNC.AID.Peloton)) - return ActionID.MakeSpell(DNC.AID.Peloton); - - // only permitted OGCDs while dancing are role actions, shield samba, and curing waltz - if (_state.IsDancing) - return new(); - - if (_state.TechFinishLeft > _state.GCD && _state.Unlocked(DNC.AID.Devilment) && _state.CanWeave(DNC.AID.Devilment, 0.6f, deadline)) - return ActionID.MakeSpell(DNC.AID.Devilment); - - if (_state.CD(DNC.AID.Devilment) > 55 && _state.CanWeave(DNC.AID.Flourish, 0.6f, deadline)) - return ActionID.MakeSpell(DNC.AID.Flourish); - - if ((_state.TechFinishLeft == 0 || _state.CD(DNC.AID.Devilment) > 0) && _state.ThreefoldLeft > _state.AnimationLock && _state.NumRangedAOETargets > 0) - return ActionID.MakeSpell(DNC.AID.FanDanceIII); - - var canF1 = ShouldSpendFeathers(strategy); - var f1ToUse = _state.NumAOETargets > 1 && _state.Unlocked(DNC.AID.FanDanceII) ? ActionID.MakeSpell(DNC.AID.FanDanceII) : ActionID.MakeSpell(DNC.AID.FanDance); - - if (_state.Feathers == 4 && canF1) - return f1ToUse; - - if (_state.CD(DNC.AID.Devilment) > 0 && _state.FourfoldLeft > _state.AnimationLock && _state.NumFan4Targets > 0) - return ActionID.MakeSpell(DNC.AID.FanDanceIV); - - if (canF1) - return f1ToUse; - - return new(); - } - - private bool ShouldTechStep(StrategyValues strategy) - { - var techStepStrategy = strategy.Option(Track.TechStep).As(); - if (!_state.Unlocked(DNC.AID.TechnicalStep) || _state.CD(DNC.AID.TechnicalStep) > _state.GCD || techStepStrategy == OffensiveStrategy.Delay) - return false; - - if (techStepStrategy == OffensiveStrategy.Force) - return true; - - return _state.NumDanceTargets > 0 && _state.StandardFinishLeft > _state.GCD + 5.5; - } - - private bool ShouldStdStep(StrategyValues strategy) - { - var stdStepStrategy = strategy.Option(Track.StdStep).As(); - if (!_state.Unlocked(DNC.AID.StandardStep) || _state.CD(DNC.AID.StandardStep) > _state.GCD || stdStepStrategy == OffensiveStrategy.Delay) - return false; - - if (stdStepStrategy == OffensiveStrategy.Force) - return true; - - // skip if tech finish would expire before we can cast std finish - // standard step = 1.5s, step = 2x1s -> 3.5s - return _state.NumDanceTargets > 0 && (_state.TechFinishLeft == 0 || _state.TechFinishLeft > _state.GCD + 3.5 || !_state.Unlocked(DNC.AID.TechnicalStep)); - } - - private bool ShouldFinishDance(float danceTimeLeft) - { - if (_state.NextStep != 0) - return false; - if (danceTimeLeft is > 0 and < FinishDanceWindow) - return true; - - return danceTimeLeft > _state.GCD && _state.NumDanceTargets > 0; - } - - private bool ShouldSpendFeathers(StrategyValues strategy) - { - var featherStrategy = strategy.Option(Track.Feather).As(); - if (_state.Feathers == 0 || featherStrategy == OffensiveStrategy.Delay) - return false; - - if (_state.Feathers == 4 || featherStrategy == OffensiveStrategy.Force) - return true; - - return _state.TechFinishLeft > _state.AnimationLock; - } - - private bool ShouldSaberDance(StrategyValues strategy, int minimumEsprit) - { - var gaugeStrategy = strategy.Option(Track.Gauge).As(); - if (_state.Esprit < 50 || gaugeStrategy == OffensiveStrategy.Delay || !_state.Unlocked(DNC.AID.SaberDance)) - return false; - - if (gaugeStrategy == OffensiveStrategy.Force) - return true; - - return _state.Esprit >= minimumEsprit && _state.NumRangedAOETargets > 0; - } - - private bool CanFlow(out DNC.AID action) - { - var act = _state.NumAOETargets > 1 ? DNC.AID.Bloodshower : DNC.AID.Fountainfall; - if (_state.Unlocked(act) && _state.FlowLeft > _state.GCD && HaveTarget()) - { - action = act; - return true; - } - - action = DNC.AID.None; - return false; - } - - private bool CanSymmetry(out DNC.AID action) - { - var act = _state.NumAOETargets > 1 ? DNC.AID.RisingWindmill : DNC.AID.ReverseCascade; - if (_state.Unlocked(act) && _state.SymmetryLeft > _state.GCD && HaveTarget()) - { - action = act; - return true; - } - - action = DNC.AID.None; - return false; - } - - private bool HaveTarget() => _state.NumAOETargets > 1 || _state.TargetingEnemy; - private bool ShouldDoNothing() => _state.PauseDuringImprov && _state.ImprovisationLeft > 0; -} diff --git a/BossMod/Autorotation/Legacy/LegacyDRG.cs b/BossMod/Autorotation/Legacy/LegacyDRG.cs deleted file mode 100644 index d43c003374..0000000000 --- a/BossMod/Autorotation/Legacy/LegacyDRG.cs +++ /dev/null @@ -1,483 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; - -namespace BossMod.Autorotation.Legacy; - -public sealed class LegacyDRG : LegacyModule -{ - public enum Track { AOE, TrueNorth, SpineshatterDive } - public enum AOEStrategy { SingleTarget, AutoTargetHitPrimary, AutoTargetHitMost, AutoOnPrimary, ForceAOE } - public enum TrueNorthStrategy { Automatic, Delay, Force } - public enum SpineshatterStrategy { Automatic, Forbid, Force, ForceReserve, UseOutsideMelee } - - public static RotationModuleDefinition Definition() - { - // TODO: think about target overrides where they make sense - var res = new RotationModuleDefinition("Legacy DRG", "Old pre-refactoring module", "Legacy (pre-DT)", "veyn, lazylemo", RotationModuleQuality.WIP, BitMask.Build((int)Class.DRG), 100); - - // TODO: 'force' flavour for continuing vs breaking combo? - res.Define(Track.AOE).As("AOE", uiPriority: 90) - .AddOption(AOEStrategy.SingleTarget, "ST", "Use single-target actions") - .AddOption(AOEStrategy.AutoTargetHitPrimary, "AutoTargetHitPrimary", "Use aoe actions if profitable select best target that ensures primary target is hit") - .AddOption(AOEStrategy.AutoTargetHitMost, "AutoTargetHitMost", "Use aoe actions if profitable select a target that ensures maximal number of targets are hit") - .AddOption(AOEStrategy.AutoOnPrimary, "AutoOnPrimary", "Use aoe actions on primary target if profitable") - .AddOption(AOEStrategy.ForceAOE, "AOE", "Use aoe rotation on primary target even if it's less total damage than single-target"); - - res.Define(Track.TrueNorth).As("TrueN", uiPriority: 80) - .AddOption(TrueNorthStrategy.Automatic, "Automatic") - .AddOption(TrueNorthStrategy.Delay, "Delay") - .AddOption(TrueNorthStrategy.Force, "Force") - .AddAssociatedActions(DRG.AID.TrueNorth); - - //res.Define(Track.SpineshatterDive).As("SpineShatter", "SSDive", uiPriority: 70) - // .AddOption(SpineshatterStrategy.Automatic, "Automatic", "Always keep one charge reserved, use other charges under raidbuffs or prevent overcapping") - // .AddOption(SpineshatterStrategy.Forbid, "Forbid", "Forbid automatic use") - // .AddOption(SpineshatterStrategy.Force, "Force", "Use all charges ASAP") - // .AddOption(SpineshatterStrategy.ForceReserve, "ForceReserve", "Use all charges except one ASAP") - // .AddOption(SpineshatterStrategy.UseOutsideMelee, "UseOutsideMelee", "Use as gapcloser if outside melee range") - // .AddAssociatedActions(DRG.AID.SpineshatterDive); - - return res; - } - - // full state needed for determining next action - public class State(RotationModule module) : CommonState(module) - { - public int FirstmindFocusCount; // 2 max - public int EyeCount; // 2 max - public float LifeOfTheDragonLeft; // 20 max - public float FangAndClawBaredLeft; // 30 max - public float WheelInMotionLeft; // 30 max - public float DraconianFireLeft; // 30 max - public float DiveReadyLeft; // 15 max - public float PowerSurgeLeft; // 30 max - public float LanceChargeLeft; // 20 max - public float RightEyeLeft; // 20 max - public float LifeSurgeLeft; // 5 max - public float TrueNorthLeft; // 10 max - public float TargetChaosThrustLeft; // 24 max - public Actor? BestAOEGCDTarget; - public int NumAOEGCDTargets; // range 10 width 4 rect - public bool UseAOERotation; - - // upgrade paths - public DRG.AID BestHeavensThrust => Unlocked(DRG.AID.HeavensThrust) ? DRG.AID.HeavensThrust : DRG.AID.FullThrust; - public DRG.AID BestChaoticSpring => Unlocked(DRG.AID.ChaoticSpring) ? DRG.AID.ChaoticSpring : DRG.AID.ChaosThrust; - public DRG.AID BestJump => Unlocked(DRG.AID.HighJump) ? DRG.AID.HighJump : DRG.AID.Jump; - // proc replacements - public DRG.AID BestGeirskogul => LifeOfTheDragonLeft > AnimationLock ? DRG.AID.Nastrond : DRG.AID.Geirskogul; - public DRG.AID BestTrueThrust => DraconianFireLeft > GCD ? DRG.AID.RaidenThrust : DRG.AID.TrueThrust; - public DRG.AID BestDoomSpike => DraconianFireLeft > GCD && Unlocked(DRG.AID.DraconianFury) ? DRG.AID.DraconianFury : DRG.AID.DoomSpike; - - // statuses - public DRG.SID ExpectedChaoticSpring => Unlocked(DRG.AID.ChaoticSpring) ? DRG.SID.ChaoticSpring : DRG.SID.ChaosThrust; - - public DRG.AID ComboLastMove => (DRG.AID)ComboLastAction; - - public bool Unlocked(DRG.AID aid) => Module.ActionUnlocked(ActionID.MakeSpell(aid)); - public bool Unlocked(DRG.TraitID tid) => Module.TraitUnlocked((uint)tid); - - public override string ToString() - { - return $"FF={FirstmindFocusCount}, LotD={EyeCount}/{LifeOfTheDragonLeft:f3}, ComboEx={FangAndClawBaredLeft:f3}/{WheelInMotionLeft:f3}, DFire={DraconianFireLeft:f3}, Dive={DiveReadyLeft:f3}, RB={RaidBuffsLeft:f3}, PS={PowerSurgeLeft:f3}, LC={LanceChargeLeft:f3}, Eye={RightEyeLeft:f3}, TN={TrueNorthLeft:f3}, CT={TargetChaosThrustLeft:f3}, PotCD={PotionCD:f3}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}, SCD={CD(DRG.AID.Stardiver)}"; - } - } - - private readonly State _state; - - public LegacyDRG(RotationModuleManager manager, Actor player) : base(manager, player) - { - _state = new(this); - } - - public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) - { - _state.UpdateCommon(primaryTarget, estimatedAnimLockDelay); - - var gauge = World.Client.GetGauge(); - _state.FirstmindFocusCount = gauge.FirstmindsFocusCount; - _state.EyeCount = gauge.EyeCount; - _state.LifeOfTheDragonLeft = gauge.LotdState != 0 ? gauge.LotdTimer * 0.001f : 0; - - _state.FangAndClawBaredLeft = _state.StatusDetails(Player, DRG.SID.FangAndClawBared, Player.InstanceID).Left; - _state.WheelInMotionLeft = _state.StatusDetails(Player, DRG.SID.WheelInMotion, Player.InstanceID).Left; - _state.DraconianFireLeft = _state.StatusDetails(Player, DRG.SID.DraconianFire, Player.InstanceID).Left; - _state.DiveReadyLeft = _state.StatusDetails(Player, DRG.SID.DiveReady, Player.InstanceID).Left; - _state.PowerSurgeLeft = _state.StatusDetails(Player, DRG.SID.PowerSurge, Player.InstanceID).Left; - _state.LanceChargeLeft = _state.StatusDetails(Player, DRG.SID.LanceCharge, Player.InstanceID).Left; - _state.RightEyeLeft = _state.StatusDetails(Player, DRG.SID.RightEye, Player.InstanceID).Left; - _state.TrueNorthLeft = _state.StatusDetails(Player, DRG.SID.TrueNorth, Player.InstanceID).Left; - _state.LifeSurgeLeft = _state.StatusDetails(Player, DRG.SID.LifeSurge, Player.InstanceID).Left; - - // TODO: multidot support - //var adjTarget = initial; - //if (_state.Unlocked(AID.ChaosThrust) && !WithoutDOT(initial.Actor)) - //{ - // var multidotTarget = Autorot.Hints.PriorityTargets.FirstOrDefault(e => e != initial && !e.ForbidDOTs && e.Actor.Position.InCircle(Player.Position, 5) && WithoutDOT(e.Actor)); - // if (multidotTarget != null) - // adjTarget = multidotTarget; - //} - _state.TargetChaosThrustLeft = _state.StatusDetails(primaryTarget, _state.ExpectedChaoticSpring, Player.InstanceID).Left; - - // TODO: auto select aoe target for ogcds - var aoeStrategy = strategy.Option(Track.AOE).As(); - (_state.BestAOEGCDTarget, _state.NumAOEGCDTargets) = _state.Unlocked(DRG.AID.DoomSpike) ? CheckAOETargeting(aoeStrategy, primaryTarget, 10, NumTargetsHitByAOEGCD, IsHitByAOEGCD) : (null, 0); - _state.UseAOERotation = _state.NumAOEGCDTargets >= 3 && (_state.Unlocked(DRG.AID.SonicThrust) || _state.PowerSurgeLeft > _state.GCD); // TODO: better AOE condition - - _state.UpdatePositionals(primaryTarget, GetNextPositional(), _state.TrueNorthLeft > _state.GCD || _state.RightEyeLeft > _state.GCD); - - // TODO: refactor all that, it's kinda senseless now - DRG.AID gcd = GetNextBestGCD(strategy); - PushResult(gcd, gcd is DRG.AID.DoomSpike or DRG.AID.DraconianFury or DRG.AID.SonicThrust or DRG.AID.CoerthanTorment ? _state.BestAOEGCDTarget : primaryTarget); - - ActionID ogcd = default; - var deadline = _state.GCD > 0 && gcd != default ? _state.GCD : float.MaxValue; - //if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot - // ogcd = GetNextBestOGCD(strategy, deadline - _state.OGCDSlotLength); - if (!ogcd && _state.CanWeave(deadline)) // second/only ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline, gcd); - PushResult(ogcd, /*ogcd == ActionID.MakeSpell(DRG.AID.DragonSight) ? DRG.Definitions.FindBestDragonSightTarget(World, Player) :*/ primaryTarget); - } - - //protected override void QueueAIActions() - //{ - // 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); - //} - - public override string DescribeState() => _state.ToString(); - - private int NumTargetsHitByAOEGCD(Actor primary) => Hints.NumPriorityTargetsInAOERect(Player.Position, (primary.Position - Player.Position).Normalized(), 10, 2); - private bool IsHitByAOEGCD(Actor primary, Actor check) => Hints.TargetInAOERect(check, Player.Position, (primary.Position - Player.Position).Normalized(), 10, 2); - - private (Actor?, int) CheckAOETargeting(AOEStrategy strategy, Actor? primaryTarget, float range, Func numTargets, Func check) => strategy switch - { - AOEStrategy.AutoTargetHitPrimary => FindBetterTargetBy(primaryTarget, range, t => primaryTarget == null || check(t, primaryTarget) ? numTargets(t) : 0), - AOEStrategy.AutoTargetHitMost => FindBetterTargetBy(primaryTarget, range, numTargets), - AOEStrategy.AutoOnPrimary => (primaryTarget, primaryTarget != null ? numTargets(primaryTarget) : 0), - AOEStrategy.ForceAOE => (primaryTarget, int.MaxValue), - _ => (null, 0) - }; - - // old DRGDRotation - //private bool WithoutDOT(Actor a) => Rotation.RefreshDOT(_state, StatusDetails(a, SID.ChaosThrust, Player.InstanceID).Left); - //private bool RefreshDOT(float timeLeft) => timeLeft < _state.GCD; // TODO: tweak threshold so that we don't overwrite or miss ticks... - - private bool UseBuffingCombo(bool predict) - { - // the selected action will happen in GCD, and it will be the *second* action in the combo - // note if we're in 'predict' mode (that is, our next action is TT, but we try to predict which branch we'll take), the selected action will happen in second GCD instead - var secondActionIn = _state.GCD + (predict ? 2.5f : 0); - if (_state.Unlocked(DRG.AID.ChaosThrust)) - { - // L50-55: at this point, we have 3-action buff and pure damage combos - // damage combo (vorpal + full) is 170+250+400 = 820 potency - // buff combo (disembowel + chaos) is 170+210+260+40*N = 640+40*N potency (assuming positional, -40 otherwise), where N is num ticks - // dot is 8 ticks (24 sec), assuming 2.5 gcd, we can either do 1:2 rotation (9 gcds = 22.5s => reapplying dot at ~1.5 left) or 1:3 rotation (12 gcds = 30s => dropping a dot for 6 seconds) - // these seem to be really close (285 vs 284.44 p/gcd); balance recommends 1:3 rotation however - // we use dot duration rather than buff duration, because it works for two-target scenario - - // L56-57: at this point, we have 3-action buff and 4-action damage combos - // damage combo (vorpal + full + f&c) is 170+250+400+300 = 1120 potency (asssuming positional) - // buff combo (disembowel + chaos) is same 640+40*N potency as above - // most logical is 1:2 rotation (11 gcds = 27.5s => dropping a dot for 3.5s), since 1:1 would clip 3 ticks - // it also works nicely for 2 targets (25s rotation, meaning almost full dot uptime on both targets) - - // L58-63: at this point, we have 4-action buff and pure damage combos - // damage combo (vorpal + full + f&c) is same 1120 potency as above - // buff combo (disembowel + chaos + wheeling) is +300 = 940+40*N potency - // most logical is 1:2 rotation (12 gcds = 30s => dropping a dot for 6s), since 1:1 would clip 2 ticks - - // L64+: at this point, we have 5-action buff and pure damage combos - // most logical is 1:1 rotation (10 gcds = 25s => dropping a dot for 1s) - - // if we use buff combo, next dot refresh be second action - that's 2.5s + however many ticks we are ok with overwriting (0 in all cases, for now) - return !_state.ForbidDOTs && _state.TargetChaosThrustLeft < secondActionIn + 2.5f; - } - else if (_state.Unlocked(DRG.AID.Disembowel)) - { - // at this point we have 2-action buff combo and 2 or 3-action pure damage combo - // if we execute pure damage combo, next chance to disembowel will be in GCD-remaining (vorpal) + N (full, if unlocked, then true, then disembowel) gcds - // we want to avoid dropping power surge buff, so that disembowel is still buffed - var damageComboLength = _state.Unlocked(DRG.AID.FullThrust) ? 3 : 2; - return _state.PowerSurgeLeft < secondActionIn + 2.5f * damageComboLength; - } - else - { - return false; // there is no buff combo yet... - } - } - - private bool UseLifeSurge() - { - if (_state.LifeSurgeLeft > _state.GCD) - return false; - - if (_state.Unlocked(DRG.TraitID.EnhancedLifeSurge)) - { - float chargeCapIn = _state.CD(DRG.AID.LifeSurge) - (_state.Unlocked(DRG.TraitID.EnhancedLifeSurge) ? 0 : 40); - if (_state.RightEyeLeft > _state.GCD - && _state.LanceChargeLeft > _state.GCD - && ((_state.WheelInMotionLeft > _state.GCD) || (_state.FangAndClawBaredLeft > _state.GCD)) - && chargeCapIn < _state.GCD + 2.5 - && _state.CD(DRG.AID.BattleLitany) > 0 - && _state.CD(DRG.AID.Geirskogul) > 0) - return true; - - if (_state.ComboLastMove == DRG.AID.VorpalThrust - && chargeCapIn < _state.GCD + 2.5 - && _state.CD(DRG.AID.BattleLitany) < 100 - //&& _state.CD(DRG.AID.DragonSight) < 100 - && _state.CD(DRG.AID.LanceCharge) < 40 - && _state.CD(DRG.AID.Geirskogul) > 0) - return true; - } - - if (_state.UseAOERotation) - { - // for aoe rotation, just use LS on last unlocked combo action - return _state.Unlocked(DRG.AID.CoerthanTorment) ? _state.ComboLastMove == DRG.AID.SonicThrust - : !_state.Unlocked(DRG.AID.SonicThrust) || _state.ComboLastMove is DRG.AID.DoomSpike or DRG.AID.DraconianFury; - } - - if (_state.Unlocked(DRG.AID.FullThrust) && _state.Unlocked(DRG.TraitID.EnhancedLifeSurge) && _state.LanceChargeLeft > _state.GCD) - { - // L26+: our most damaging action is FT, which is the third action in damaging combo - return _state.ComboLastMove == DRG.AID.VorpalThrust; - } - - // TODO: L64+ - if (_state.Unlocked(DRG.AID.FullThrust) && !_state.Unlocked(DRG.TraitID.EnhancedLifeSurge)) - { - // L26+: our most damaging action is FT, which is the third action in damaging combo - return _state.ComboLastMove == DRG.AID.VorpalThrust; - } - else - { - // L6+: our most damaging action is VT, which is the second action in damaging combo (which is the only combo we have before L18) - return _state.ComboLastMove == DRG.AID.TrueThrust && !UseBuffingCombo(false); - } - } - - /* - private bool UseSpineShatterDive(SpineshatterStrategy strategy) - { - switch (strategy) - { - case SpineshatterStrategy.Forbid: - return false; - case SpineshatterStrategy.Force: - return true; - case SpineshatterStrategy.ForceReserve: - return _state.CD(DRG.AID.SpineshatterDive) <= 60 + _state.AnimationLock; - case SpineshatterStrategy.UseOutsideMelee: - return _state.RangeToTarget > 3; - default: - if (_state.CountdownRemaining > 0 || _state.PositionLockIn <= _state.AnimationLock) - return false; // Combat or Position restrictions - - if (_state.Unlocked(DRG.TraitID.EnhancedSpineshatterDive)) - { - // Enhanced Spineshatter Dive logic - if (_state.RightEyeLeft > _state.AnimationLock) - return true; // Use all charges under RightEyeLeft buff - } - else - { - // Regular Spineshatter Dive logic - if (_state.LanceChargeLeft > _state.AnimationLock) - return true; // Use when there is Lance Charge left - } - - return false; // Default: Don't use in other cases - } - } - */ - - private (Positional, bool) GetNextPositional() - { - if (_state.UseAOERotation) - return default; // AOE rotation has no positionals - - if (_state.Unlocked(DRG.AID.FangAndClaw)) - { - // we have flank positional (fang and claw, 4th in damaging combo) and rear positionals (chaos thrust & wheeling thrust, 3rd and 4th in buffing combo) - // if our next action is not a positional, then just use next action in the current combo; for True Thrust, predict what branch we'll take using UseBuffingCombo - if (_state.FangAndClawBaredLeft > _state.GCD) - return (Positional.Flank, true); - if (_state.WheelInMotionLeft > _state.GCD) - return (Positional.Rear, true); - var buffingCombo = _state.ComboLastMove switch - { - DRG.AID.TrueThrust or DRG.AID.RaidenThrust => UseBuffingCombo(false), - DRG.AID.VorpalThrust => false, - DRG.AID.Disembowel => true, - _ => UseBuffingCombo(true) - }; - return (buffingCombo ? Positional.Rear : Positional.Flank, _state.ComboLastMove == DRG.AID.Disembowel); - } - else if (_state.Unlocked(DRG.AID.ChaosThrust)) - { - // the only positional we have at this point is chaos thrust (rear) - return (Positional.Rear, _state.ComboLastMove == DRG.AID.Disembowel); - } - else - { - return default; - } - } - - private bool ShouldUseTrueNorth(TrueNorthStrategy strategy) - { - switch (strategy) - { - case TrueNorthStrategy.Delay: - return false; - case TrueNorthStrategy.Force: - return true; - - default: - if (!_state.TargetingEnemy) - return false; - if (_state.TrueNorthLeft > _state.AnimationLock || _state.RightEyeLeft > _state.AnimationLock) - return false; - if (GetNextPositional().Item2 && _state.NextPositionalCorrect) - return false; - if (GetNextPositional().Item2 && !_state.NextPositionalCorrect) - return true; - return false; - } - } - - private bool ShouldUseGeirskogul() - { - if (_state.EyeCount == 2 && _state.CD(DRG.AID.LanceCharge) < 40) - return false; - if (_state.EyeCount == 2 && _state.LanceChargeLeft > _state.AnimationLock) - return true; - if (_state.EyeCount == 1 && _state.LanceChargeLeft > _state.AnimationLock) - { - if (_state.DiveReadyLeft > _state.AnimationLock && _state.CD(DRG.AID.HighJump) > 10) - return false; - } - if (_state.EyeCount != 2 && _state.CD(DRG.AID.LanceCharge) < 40) - { - return true; - } - if (_state.EyeCount == 0) - { - return true; - } - return true; - } - - private bool ShouldUseWyrmWindThrust() - { - bool nextGCDisRaiden = _state.DraconianFireLeft > _state.AnimationLock && (_state.ComboLastMove == DRG.AID.WheelingThrust || _state.ComboLastMove == DRG.AID.FangAndClaw) && _state.WheelInMotionLeft < _state.AnimationLock && _state.FangAndClawBaredLeft < _state.AnimationLock; - if (_state.FirstmindFocusCount >= 2 && _state.CD(DRG.AID.LanceCharge) > 10) - return true; - if (_state.FirstmindFocusCount >= 2 && _state.LanceChargeLeft > _state.AnimationLock) - return true; - if (_state.FirstmindFocusCount >= 2 && _state.CD(DRG.AID.LanceCharge) < 10 && !nextGCDisRaiden) - return false; - return false; - } - - private DRG.AID GetNextBestGCD(StrategyValues strategy) - { - // prepull or no target - if (!_state.TargetingEnemy || _state.CountdownRemaining > 0.7f) - return DRG.AID.None; - - if (_state.UseAOERotation) - { - return _state.ComboLastMove switch - { - DRG.AID.DoomSpike or DRG.AID.DraconianFury => _state.Unlocked(DRG.AID.SonicThrust) ? DRG.AID.SonicThrust : DRG.AID.DoomSpike, - DRG.AID.SonicThrust => _state.Unlocked(DRG.AID.CoerthanTorment) ? DRG.AID.CoerthanTorment : DRG.AID.DoomSpike, - _ => _state.BestDoomSpike - }; - } - else - { - if (_state.FangAndClawBaredLeft > _state.GCD) - return DRG.AID.FangAndClaw; - if (_state.WheelInMotionLeft > _state.GCD) - return DRG.AID.WheelingThrust; - return _state.ComboLastMove switch - { - DRG.AID.TrueThrust or DRG.AID.RaidenThrust => UseBuffingCombo(false) ? DRG.AID.Disembowel : _state.Unlocked(DRG.AID.VorpalThrust) ? DRG.AID.VorpalThrust : DRG.AID.TrueThrust, - DRG.AID.VorpalThrust => _state.Unlocked(DRG.AID.FullThrust) ? _state.BestHeavensThrust : DRG.AID.TrueThrust, - DRG.AID.Disembowel => _state.Unlocked(DRG.AID.ChaosThrust) ? _state.BestChaoticSpring : DRG.AID.TrueThrust, - _ => _state.BestTrueThrust - }; - } - } - - private ActionID GetNextBestOGCD(StrategyValues strategy, float deadline, DRG.AID nextBestGCD) - { - if (!_state.TargetingEnemy) - return default; - - bool canJump = _state.PositionLockIn > _state.AnimationLock; - // bool wantSpineShatter = _state.Unlocked(DRG.AID.SpineshatterDive) && _state.TargetingEnemy && UseSpineShatterDive(strategy.Option(Track.SpineshatterDive).As()); - - if (_state.PowerSurgeLeft > _state.GCD) - { - if (_state.Unlocked(DRG.AID.LanceCharge) && _state.CanWeave(DRG.AID.LanceCharge, 0.6f, deadline - _state.OGCDSlotLength) /*&& ((_state.CD(DRG.AID.DragonSight) < _state.GCD) || (_state.CD(DRG.AID.DragonSight) < 65) && (_state.CD(DRG.AID.DragonSight) > 55))*/) - return ActionID.MakeSpell(DRG.AID.LanceCharge); - //if (_state.Unlocked(DRG.AID.DragonSight) && _state.CanWeave(DRG.AID.DragonSight, 0.6f, deadline) && _state.CD(DRG.AID.BattleLitany) < _state.GCD + 2.5) - // return ActionID.MakeSpell(DRG.AID.DragonSight); - if (_state.Unlocked(DRG.AID.BattleLitany) && _state.CanWeave(DRG.AID.BattleLitany, 0.6f, deadline)) - return ActionID.MakeSpell(DRG.AID.BattleLitany); - // life surge on most damaging gcd - if (_state.Unlocked(DRG.AID.LifeSurge) && _state.CanWeave(_state.CD(DRG.AID.LifeSurge) - 45, 0.6f, deadline) && UseLifeSurge()) - return ActionID.MakeSpell(DRG.AID.LifeSurge); - - // TODO: better buff conditions, reconsider priorities - if (_state.LifeOfTheDragonLeft > _state.AnimationLock && _state.CanWeave(DRG.AID.Nastrond, 0.6f, deadline)) - return ActionID.MakeSpell(DRG.AID.Nastrond); - - if (_state.CD(DRG.AID.LanceCharge) > 5 && /*_state.CD(DRG.AID.DragonSight) > 5 &&*/ _state.CD(DRG.AID.BattleLitany) > 5) - { - if (_state.CanWeave(DRG.AID.WyrmwindThrust, 0.6f, deadline) && ShouldUseWyrmWindThrust() && (nextBestGCD is DRG.AID.DraconianFury or DRG.AID.RaidenThrust)) - return ActionID.MakeSpell(DRG.AID.WyrmwindThrust); - if (_state.Unlocked(DRG.AID.Geirskogul) && _state.CanWeave(DRG.AID.Geirskogul, 0.6f, deadline) && ShouldUseGeirskogul()) - return ActionID.MakeSpell(DRG.AID.Geirskogul); - if (canJump && _state.Unlocked(DRG.AID.Jump) && _state.CanWeave(_state.Unlocked(DRG.AID.HighJump) ? DRG.AID.HighJump : DRG.AID.Jump, 0.8f, deadline)) - return ActionID.MakeSpell(_state.BestJump); - if (_state.DiveReadyLeft > _state.AnimationLock && _state.CanWeave(DRG.AID.MirageDive, 0.6f, deadline) && _state.EyeCount == 1 && _state.CD(DRG.AID.Geirskogul) < _state.AnimationLock && _state.LanceChargeLeft > _state.AnimationLock) - return ActionID.MakeSpell(DRG.AID.MirageDive); - if (canJump && _state.Unlocked(DRG.AID.DragonfireDive) && _state.CanWeave(DRG.AID.DragonfireDive, 0.8f, deadline)) - return ActionID.MakeSpell(DRG.AID.DragonfireDive); - //if (wantSpineShatter && _state.CanWeave(_state.CD(DRG.AID.SpineshatterDive), 0.8f, deadline)) - // return ActionID.MakeSpell(DRG.AID.SpineshatterDive); - if (canJump && _state.Unlocked(DRG.AID.Stardiver) && _state.LifeOfTheDragonLeft > _state.AnimationLock && _state.CanWeave(DRG.AID.Stardiver, 1.5f, deadline)) - return ActionID.MakeSpell(DRG.AID.Stardiver); - if (_state.CanWeave(DRG.AID.WyrmwindThrust, 0.6f, deadline) && ShouldUseWyrmWindThrust()) - return ActionID.MakeSpell(DRG.AID.WyrmwindThrust); - //if (wantSpineShatter && _state.RangeToTarget > 3) - // return ActionID.MakeSpell(DRG.AID.SpineshatterDive); - //if (wantSpineShatter && _state.LifeOfTheDragonLeft < _state.AnimationLock && _state.CanWeave(_state.CD(DRG.AID.SpineshatterDive) - 60, 0.8f, deadline)) - // return ActionID.MakeSpell(DRG.AID.SpineshatterDive); - //if (wantSpineShatter && _state.LifeOfTheDragonLeft > _state.AnimationLock && _state.CD(DRG.AID.Stardiver) > 0 && _state.CanWeave(_state.CD(DRG.AID.SpineshatterDive) - 60, 0.8f, deadline)) - // return ActionID.MakeSpell(DRG.AID.SpineshatterDive); - //if (wantSpineShatter && _state.CanWeave(_state.CD(DRG.AID.SpineshatterDive) - 60, 0.8f, deadline)) - // return ActionID.MakeSpell(DRG.AID.SpineshatterDive); - //if (_state.DiveReadyLeft > _state.AnimationLock && _state.CanWeave(DRG.AID.MirageDive, 0.6f, deadline) && _state.EyeCount != 2 && _state.LifeOfTheDragonLeft > _state.AnimationLock && _state.CD(DRG.AID.Stardiver) > 0) - // return ActionID.MakeSpell(DRG.AID.MirageDive); - //if (_state.DiveReadyLeft > _state.AnimationLock && _state.CanWeave(DRG.AID.MirageDive, 0.6f, deadline) && _state.EyeCount != 2 && _state.LifeOfTheDragonLeft < _state.AnimationLock) - // return ActionID.MakeSpell(DRG.AID.MirageDive); - if (_state.DiveReadyLeft > _state.AnimationLock && _state.CanWeave(DRG.AID.MirageDive, 0.6f, deadline) && (_state.EyeCount != 2 || _state.DiveReadyLeft < _state.GCD)) - return ActionID.MakeSpell(DRG.AID.MirageDive); - } - } - - if (ShouldUseTrueNorth(strategy.Option(Track.TrueNorth).As()) && _state.CanWeave(DRG.AID.TrueNorth - 45, 0.6f, deadline) && !_state.UseAOERotation && _state.GCD < 0.8) - return ActionID.MakeSpell(DRG.AID.TrueNorth); - - // no suitable oGCDs... - return new(); - } -} diff --git a/BossMod/Autorotation/Legacy/LegacyGNB.cs b/BossMod/Autorotation/Legacy/LegacyGNB.cs deleted file mode 100644 index 9e5276b548..0000000000 --- a/BossMod/Autorotation/Legacy/LegacyGNB.cs +++ /dev/null @@ -1,841 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; - -namespace BossMod.Autorotation.Legacy; - -public sealed class LegacyGNB : LegacyModule -{ - public enum Track { AOE, Gauge, Potion, NoMercy, BloodFest, Gnash, Zone, Bow, RoughDivide } - public enum AOEStrategy { SingleTarget, ForceAOE, Auto } - public enum GaugeStrategy { Automatic, Spend, Hold, ForceGF, LightningShotIfNotInMelee } - public enum PotionStrategy { Manual, Immediate, DelayUntilRaidBuffs, Special, Force } - public enum OffensiveStrategy { Automatic, Delay, Force } - - public static RotationModuleDefinition Definition() - { - // TODO: add in "Hold Double Down" option? - // TODO: think about target overrides where they make sense (ST stuff, esp things like onslaught?) - var res = new RotationModuleDefinition("Legacy GNB", "Old pre-refactoring module", "Legacy (pre-DT)", "LazyLemo, Akechi-kun", RotationModuleQuality.WIP, BitMask.Build((int)Class.GNB), 100); - - res.Define(Track.AOE).As("AOE", uiPriority: 90) - .AddOption(AOEStrategy.SingleTarget, "ST", "Use single-target rotation") - .AddOption(AOEStrategy.ForceAOE, "AOE", "Use aoe rotation") - .AddOption(AOEStrategy.Auto, "Auto", "Use aoe rotation if 3+ targets would be hit, otherwise use single-target rotation"); - - res.Define(Track.Gauge).As("Gauge", "Gauge", uiPriority: 80) - .AddOption(GaugeStrategy.Automatic, "Automatic") // optimal spend (for the most part) - .AddOption(GaugeStrategy.Spend, "Spend", "Spend all gauge ASAP") // burn all carts; Double Down > GF combo > Burst Strike > 123 combo - .AddOption(GaugeStrategy.Hold, "Hold", "Hold Carts") // Hold cartridges optimally; works for both ST/AOE - .AddOption(GaugeStrategy.ForceGF, "ForceGF", "Force Gnashing combo") // forces GF combo - .AddOption(GaugeStrategy.LightningShotIfNotInMelee, "LightningShotIfNotInMelee", "Use Lightning Shot if outside melee"); - - res.Define(Track.Potion).As("Potion", uiPriority: 70) - .AddOption(PotionStrategy.Manual, "Manual", "Do not use automatically") - .AddOption(PotionStrategy.Immediate, "Immediate", "Use ASAP, but delay slightly during opener", 270, 30) - .AddOption(PotionStrategy.DelayUntilRaidBuffs, "DelayUntilRaidBuffs", "Delay until raidbuffs", 270, 30) - .AddOption(PotionStrategy.Special, "Special", "2min / 8min Potion use", 270, 30) - .AddOption(PotionStrategy.Force, "Force", "Use ASAP", 270, 30) - .AddAssociatedAction(ActionDefinitions.IDPotionStr); - - res.Define(Track.NoMercy).As("NoM", uiPriority: 60) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.BloodFest).As("Fest", uiPriority: 50) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.Gnash).As("Gnash", uiPriority: 40) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.Zone).As("Zone", uiPriority: 30) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.Bow).As("Bow", uiPriority: 20) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - return res; - } - - // full state needed for determining next action - public class State(RotationModule module) : CommonState(module) - { - public int Ammo; // 0 to 3 - public int GunComboStep; // 0 to 4 - public float NoMercyLeft; // 0 if buff not up, max 20 - public bool ReadyToRip; // 0 if buff not up, max 10 - public bool ReadyToTear; // 0 if buff not up, max 10 - public bool ReadyToGouge; // 0 if buff not up, max 10 - public bool ReadyToBlast; // 0 if buff not up, max 10 - public float AuroraLeft; // 0 if buff not up, max 18 - public float ReadyToBreak; // 0 if buff not up, max 30 - public int NumTargetsHitByAOE; - public int MaxCartridges; - - // upgrade paths - public GNB.AID BestZone => Unlocked(GNB.AID.BlastingZone) ? GNB.AID.BlastingZone : GNB.AID.DangerZone; - public GNB.AID BestHeart => Unlocked(GNB.AID.HeartOfCorundum) ? GNB.AID.HeartOfCorundum : GNB.AID.HeartOfStone; - public GNB.AID BestContinuation => ReadyToRip ? GNB.AID.JugularRip : ReadyToTear ? GNB.AID.AbdomenTear : ReadyToGouge ? GNB.AID.EyeGouge : ReadyToBlast ? GNB.AID.Hypervelocity : GNB.AID.Continuation; - public GNB.AID BestGnash => GunComboStep == 1 ? GNB.AID.SavageClaw : GunComboStep == 2 ? GNB.AID.WickedTalon : GNB.AID.GnashingFang; - public GNB.AID ComboLastMove => (GNB.AID)ComboLastAction; - - public bool Unlocked(GNB.AID aid) => Module.ActionUnlocked(ActionID.MakeSpell(aid)); - public bool Unlocked(GNB.TraitID tid) => Module.TraitUnlocked((uint)tid); - - public override string ToString() - { - return $"ammo={Ammo}, ReadytoBlast={ReadyToBlast}, ReadytoGouge={ReadyToGouge}, ReadytoRip={ReadyToRip}, ReadytoTear={ReadyToTear}, CBT={ComboTimeLeft:f1}, RB={RaidBuffsLeft:f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}"; - } - } - - private readonly State _state; - - public LegacyGNB(RotationModuleManager manager, Actor player) : base(manager, player) - { - _state = new(this); - } - - public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) - { - _state.UpdateCommon(primaryTarget, estimatedAnimLockDelay); - _state.HaveTankStance = Player.FindStatus(GNB.SID.RoyalGuard) != null; - - var gauge = World.Client.GetGauge(); - _state.Ammo = gauge.Ammo; - _state.GunComboStep = gauge.AmmoComboStep; - _state.MaxCartridges = _state.Unlocked(GNB.TraitID.CartridgeChargeII) ? 3 : 2; - - _state.NoMercyLeft = _state.StatusDetails(Player, GNB.SID.NoMercy, Player.InstanceID).Left; - _state.ReadyToBreak = _state.StatusDetails(Player, GNB.SID.ReadyToBreak, Player.InstanceID).Left; - _state.ReadyToRip = Player.FindStatus(GNB.SID.ReadyToRip) != null; - _state.ReadyToTear = Player.FindStatus(GNB.SID.ReadyToTear) != null; - _state.ReadyToGouge = Player.FindStatus(GNB.SID.ReadyToGouge) != null; - _state.ReadyToBlast = Player.FindStatus(GNB.SID.ReadyToBlast) != null; - _state.AuroraLeft = _state.StatusDetails(Player, GNB.SID.Aurora, Player.InstanceID).Left; - _state.NumTargetsHitByAOE = NumTargetsHitByAOE(); - - var aoe = strategy.Option(Track.AOE).As() switch - { - AOEStrategy.ForceAOE => true, - AOEStrategy.Auto => NumTargetsHitByAOE() >= 3, - _ => false, - }; - - // TODO: refactor all that, it's kinda senseless now - GNB.AID gcd = GetNextBestGCD(strategy, aoe); - PushResult(gcd, primaryTarget); - - ActionID ogcd = default; - var deadline = _state.GCD > 0 && gcd != default ? _state.GCD : float.MaxValue; - if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline - _state.OGCDSlotLength, aoe); - if (!ogcd && _state.CanWeave(deadline)) // second/only ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline, aoe); - PushResult(ogcd, primaryTarget); - } - - public override string DescribeState() => _state.ToString(); - - private int NumTargetsHitByAOE() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 5); - - private GNB.AID GetNextUnlockedComboAction(StrategyValues strategy, bool aoe) - { - if (aoe) - { - return _state.ComboLastMove switch - { - GNB.AID.DemonSlice => _state.Unlocked(GNB.AID.DemonSlaughter) ? GNB.AID.DemonSlaughter : GNB.AID.DemonSlice, - GNB.AID.BrutalShell => _state.Unlocked(GNB.AID.SolidBarrel) ? GNB.AID.SolidBarrel : GNB.AID.KeenEdge, - GNB.AID.KeenEdge => _state.Unlocked(GNB.AID.BrutalShell) ? GNB.AID.BrutalShell : GNB.AID.KeenEdge, - _ => GNB.AID.DemonSlice - }; - } - else - { - return _state.ComboLastMove switch - { - GNB.AID.DemonSlice => _state.Unlocked(GNB.AID.DemonSlaughter) ? GNB.AID.DemonSlaughter : GNB.AID.DemonSlice, - GNB.AID.BrutalShell => _state.Unlocked(GNB.AID.SolidBarrel) ? GNB.AID.SolidBarrel : GNB.AID.KeenEdge, - GNB.AID.KeenEdge => _state.Unlocked(GNB.AID.BrutalShell) ? GNB.AID.BrutalShell : GNB.AID.KeenEdge, - _ => GNB.AID.KeenEdge - }; - } - } - - private GNB.AID GetNextAmmoAction(StrategyValues strategy, bool aoe) - { - var gaugeStrategy = strategy.Option(Track.Gauge).As(); - if (gaugeStrategy == GaugeStrategy.Spend) - { - if (_state.Ammo >= 1) - { - if (_state.CD(GNB.AID.DoubleDown) < 0.6f && _state.Ammo >= 2) - { - return GNB.AID.DoubleDown; - } - else if (_state.CD(GNB.AID.GnashingFang) < 0.6f && _state.Ammo <= 3) - { - if (_state.GunComboStep == 0) - return GNB.AID.GnashingFang; - if (_state.GunComboStep == 1) - return GNB.AID.SavageClaw; - if (_state.GunComboStep == 2) - return GNB.AID.WickedTalon; - } - return GNB.AID.BurstStrike; - } - - if (_state.Ammo == 0 && _state.GunComboStep <= 0) - { - return GNB.AID.KeenEdge; - } - } - - if (gaugeStrategy == GaugeStrategy.Hold && _state.Ammo >= 0 && _state.NoMercyLeft >= 0) - { - if (aoe) - { - if (_state.ComboLastMove == GNB.AID.DemonSlice && _state.Ammo < 3) - { - return GNB.AID.DemonSlaughter; - } - else if (_state.ComboLastMove == GNB.AID.DemonSlice && _state.Ammo == 3) - { - return GNB.AID.FatedCircle; - } - else if (_state.ComboLastMove == GNB.AID.KeenEdge) - { - return GNB.AID.BrutalShell; - } - - return GNB.AID.DemonSlice; - } - else if (_state.ComboLastMove == GNB.AID.BrutalShell) - { - if (_state.Ammo < 3) - { - return GNB.AID.SolidBarrel; - } - else if (_state.Ammo == 3) - { - return GNB.AID.BurstStrike; - } - } - else if (_state.ComboLastMove == GNB.AID.KeenEdge) - { - return GNB.AID.BrutalShell; - } - return GNB.AID.KeenEdge; - } - - if (gaugeStrategy == GaugeStrategy.ForceGF) - { - if (_state.Ammo >= 1) - { - if (_state.CD(GNB.AID.GnashingFang) < 0.6f && _state.CD(GNB.AID.DoubleDown) < 0.6f) - { - if (_state.GunComboStep == 0) - { - return GNB.AID.GnashingFang; - } - else if (_state.GunComboStep == 1) - { - return GNB.AID.SavageClaw; - } - else if (_state.GunComboStep == 2) - { - return GNB.AID.WickedTalon; - } - } - else if (_state.GunComboStep == 1) - { - return GNB.AID.SavageClaw; - } - else if (_state.GunComboStep == 2) - { - return GNB.AID.WickedTalon; - } - } - } - - // Lv30-53 NM proc ST - if (_state.Unlocked(GNB.AID.NoMercy)) - { - bool canUseBurstStrike = !_state.Unlocked(GNB.AID.FatedCircle) && - !_state.Unlocked(GNB.AID.DoubleDown) && - !_state.Unlocked(GNB.AID.Bloodfest) && - !_state.Unlocked(GNB.AID.Continuation) && - !_state.Unlocked(GNB.AID.GnashingFang) && - !_state.Unlocked(GNB.AID.SonicBreak); - - // ST - if (!aoe) - { - if (!_state.Unlocked(GNB.AID.FatedCircle) && - !_state.Unlocked(GNB.AID.DoubleDown) && - !_state.Unlocked(GNB.AID.Bloodfest) && - !_state.Unlocked(GNB.AID.Continuation) && - !_state.Unlocked(GNB.AID.GnashingFang) && - !_state.Unlocked(GNB.AID.SonicBreak) && - _state.Ammo >= 2) - { - return GNB.AID.NoMercy; - } - else if (canUseBurstStrike && _state.CD(GNB.AID.NoMercy) < 40 && _state.Ammo >= 2 && _state.ComboLastMove == GNB.AID.BrutalShell) - { - return GNB.AID.BurstStrike; - } - } - - // AOE - if (aoe) - { - if (!_state.Unlocked(GNB.AID.FatedCircle) && - !_state.Unlocked(GNB.AID.DoubleDown) && - !_state.Unlocked(GNB.AID.Bloodfest) && - !_state.Unlocked(GNB.AID.Continuation) && - !_state.Unlocked(GNB.AID.GnashingFang) && - !_state.Unlocked(GNB.AID.SonicBreak) && - _state.Ammo >= 2) - { - return GNB.AID.NoMercy; - } - else if (canUseBurstStrike && _state.CD(GNB.AID.NoMercy) < 40 && _state.Ammo >= 2 && _state.ComboLastMove == GNB.AID.DemonSlice) - { - return GNB.AID.BurstStrike; - } - } - } - - if (_state.CD(GNB.AID.NoMercy) > 17) - { - if (_state.GunComboStep == 0 && _state.Unlocked(GNB.AID.GnashingFang) && _state.CD(GNB.AID.GnashingFang) < 0.6f && _state.Ammo >= 1 && ShouldUseGnash(strategy) && _state.NumTargetsHitByAOE <= 3) - return GNB.AID.GnashingFang; - } - - if (_state.NoMercyLeft > _state.AnimationLock) - { - if (_state.Unlocked(GNB.AID.SonicBreak) && _state.ReadyToBreak > 27.5) - return GNB.AID.SonicBreak; - if (_state.CD(GNB.AID.DoubleDown) < 0.6f && _state.Unlocked(GNB.AID.DoubleDown) && _state.Ammo >= 2 && _state.RangeToTarget <= 5) - return GNB.AID.DoubleDown; - if (!aoe && _state.CD(GNB.AID.DoubleDown) < _state.GCD && _state.CD(GNB.AID.GnashingFang) > _state.GCD && _state.Unlocked(GNB.AID.DoubleDown) && _state.Ammo == 1 && _state.CD(GNB.AID.Bloodfest) < 1.9) - return GNB.AID.BurstStrike; - if (aoe && _state.CD(GNB.AID.DoubleDown) < _state.GCD && _state.CD(GNB.AID.GnashingFang) > _state.GCD && _state.Unlocked(GNB.AID.DoubleDown) && _state.Ammo == 1 && _state.CD(GNB.AID.Bloodfest) < 1.9) - return GNB.AID.FatedCircle; - if (!aoe && _state.Ammo >= 1 && _state.CD(GNB.AID.GnashingFang) > _state.GCD && _state.CD(GNB.AID.DoubleDown) > _state.GCD && _state.Unlocked(GNB.AID.DoubleDown) && _state.GunComboStep == 0) - return GNB.AID.BurstStrike; - if (!aoe && _state.Ammo >= 1 && _state.CD(GNB.AID.GnashingFang) > _state.GCD && !_state.Unlocked(GNB.AID.DoubleDown) && _state.GunComboStep == 0) - return GNB.AID.BurstStrike; - if (!aoe && _state.Ammo >= 1 && _state.CD(GNB.AID.GnashingFang) > _state.GCD && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.SonicBreak) && _state.GunComboStep == 0) - return GNB.AID.BurstStrike; - if (!_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.Bloodfest) && !_state.Unlocked(GNB.AID.Continuation) && !_state.Unlocked(GNB.AID.GnashingFang) && !_state.Unlocked(GNB.AID.SonicBreak) && _state.Ammo >= 2) - return GNB.AID.BurstStrike; - - if (!aoe) - { - if (_state.Ammo >= 1 && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.Bloodfest) && !_state.Unlocked(GNB.AID.Continuation) && !_state.Unlocked(GNB.AID.GnashingFang) && !_state.Unlocked(GNB.AID.SonicBreak)) - return GNB.AID.BurstStrike; - } - // AOE - else if (aoe) - { - if (_state.NoMercyLeft > 0) - { - if (_state.Ammo >= 1) - { - if (_state.Unlocked(GNB.AID.GnashingFang) && _state.CD(GNB.AID.GnashingFang) == 0) - { - return GNB.AID.GnashingFang; // Lv60+ AOE GF - } - if (!_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown)) - { - return GNB.AID.BurstStrike; // Lv 30-72 AOE BS - } - } - if (_state.Ammo >= 2 && !_state.Unlocked(GNB.AID.DoubleDown) && - !_state.Unlocked(GNB.AID.Bloodfest) && !_state.Unlocked(GNB.AID.Continuation) && !_state.Unlocked(GNB.AID.GnashingFang) && !_state.Unlocked(GNB.AID.SonicBreak)) - { - return GNB.AID.BurstStrike; // Lv30-53 AOE BS - } - if (_state.Ammo >= 2 && _state.Unlocked(GNB.AID.SonicBreak) && _state.Unlocked(GNB.AID.GnashingFang) && - !_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown)) - { - return GNB.AID.GnashingFang; // Lv60 AOE GF fix - } - } - if (_state.Ammo >= 1 && _state.GunComboStep == 0) - { - if (_state.NoMercyLeft > 0 && !_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.Bloodfest) && - _state.Unlocked(GNB.AID.Continuation)) - { - return GNB.AID.BurstStrike; // Lv70 AOE BS - } - if (_state.Ammo >= 1 && _state.NoMercyLeft > 0 && _state.Unlocked(GNB.AID.SonicBreak) && _state.Unlocked(GNB.AID.GnashingFang) && - !_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown)) - { - return GNB.AID.BurstStrike; // Lv60 AOE BS - } - if (_state.CD(GNB.AID.GnashingFang) > _state.GCD && _state.CD(GNB.AID.DoubleDown) > _state.GCD - && _state.Unlocked(GNB.AID.DoubleDown)) - { - return GNB.AID.FatedCircle; // Lv80 AOE - } - if (_state.CD(GNB.AID.GnashingFang) > _state.GCD && _state.Unlocked(GNB.AID.FatedCircle) && - !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.SonicBreak)) - { - return GNB.AID.FatedCircle; // Lv80 AOE - } - if (_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown) && - !_state.Unlocked(GNB.AID.SonicBreak) && !_state.Unlocked(GNB.AID.GnashingFang)) - { - return GNB.AID.FatedCircle; // Lv80 AOE - } - } - } - } - - if (_state.GunComboStep > 0) - { - if (_state.GunComboStep == 2) - return GNB.AID.WickedTalon; - if (_state.GunComboStep == 1) - return GNB.AID.SavageClaw; - } - - if (_state.ComboLastMove == GNB.AID.BrutalShell && _state.Ammo == _state.MaxCartridges && _state.Unlocked(GNB.AID.BurstStrike)) - { - return GNB.AID.BurstStrike; - } - - if (aoe && _state.ComboLastMove == GNB.AID.DemonSlice && _state.Ammo == _state.MaxCartridges && _state.Unlocked(GNB.AID.FatedCircle)) - { - return GNB.AID.FatedCircle; - } - - if (gaugeStrategy == GaugeStrategy.Spend && _state.Ammo >= 0) - { - if (_state.Ammo >= 2) - { - if (_state.CD(GNB.AID.DoubleDown) < 0.6f && _state.Ammo > 2) - return GNB.AID.DoubleDown; - - if (_state.CD(GNB.AID.GnashingFang) < 0.6f && _state.Ammo <= 2) - { - if (_state.GunComboStep == 0) - return GNB.AID.GnashingFang; - if (_state.GunComboStep == 1) - return GNB.AID.SavageClaw; - if (_state.GunComboStep == 2) - return GNB.AID.WickedTalon; - } - - return GNB.AID.BurstStrike; - } - - if (_state.Ammo == 0 && _state.GunComboStep <= 0) - { - return GNB.AID.KeenEdge; - } - } - - if (gaugeStrategy == GaugeStrategy.Hold && _state.Ammo >= 0 && _state.NoMercyLeft >= 0) - { - if (aoe) - { - if (_state.ComboLastMove == GNB.AID.DemonSlice) - { - return GNB.AID.DemonSlaughter; - } - else if (_state.ComboLastMove == GNB.AID.BrutalShell) - { - return GNB.AID.SolidBarrel; - } - else if (_state.ComboLastMove == GNB.AID.KeenEdge) - { - return GNB.AID.BrutalShell; - } - - return GNB.AID.DemonSlice; - } - else if (_state.ComboLastMove == GNB.AID.BrutalShell) - { - if (_state.Ammo < 3) - { - return GNB.AID.SolidBarrel; - } - else if (_state.Ammo == 3) - { - return GNB.AID.BurstStrike; - } - } - else if (_state.ComboLastMove == GNB.AID.KeenEdge) - { - return GNB.AID.BrutalShell; - } - return GNB.AID.KeenEdge; - } - - if (gaugeStrategy == GaugeStrategy.ForceGF) - { - if (_state.Ammo >= 1) - { - if (_state.CD(GNB.AID.GnashingFang) < 0.6f && _state.CD(GNB.AID.DoubleDown) < 0.6f) - { - if (_state.GunComboStep == 0) - { - return GNB.AID.GnashingFang; - } - else if (_state.GunComboStep == 1) - { - return GNB.AID.SavageClaw; - } - else if (_state.GunComboStep == 2) - { - return GNB.AID.WickedTalon; - } - } - else if (_state.GunComboStep == 1) - { - return GNB.AID.SavageClaw; - } - else if (_state.GunComboStep == 2) - { - return GNB.AID.WickedTalon; - } - } - } - - return GetNextUnlockedComboAction(strategy, aoe); - } - - private bool ShouldSpendGauge(StrategyValues strategy) => strategy.Option(Track.Gauge).As() switch - { - GaugeStrategy.Automatic or GaugeStrategy.LightningShotIfNotInMelee => (_state.RaidBuffsLeft > _state.GCD || _state.FightEndIn <= _state.RaidBuffsIn + 10), - GaugeStrategy.Spend => true, - GaugeStrategy.ForceGF => true, - GaugeStrategy.Hold => true, - _ => true - }; - - private bool ShouldUsePotion(StrategyValues strategy) => strategy.Option(Track.Potion).As() switch - { - PotionStrategy.Manual => false, - PotionStrategy.Immediate => true, - PotionStrategy.Special => _state.ComboLastMove == GNB.AID.BrutalShell && _state.Ammo == 3 && _state.CD(GNB.AID.NoMercy) < 3 && _state.CD(GNB.AID.Bloodfest) < 15, - PotionStrategy.Force => true, - _ => false - }; - - private bool ShouldUseNoMercy(StrategyValues strategy) => strategy.Option(Track.NoMercy).As() switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat && _state.TargetingEnemy && _state.Ammo == 3 - }; - - private bool ShouldUseGnash(StrategyValues strategy) => strategy.Option(Track.Gnash).As() switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat && _state.TargetingEnemy && _state.Unlocked(GNB.AID.GnashingFang) && _state.CD(GNB.AID.GnashingFang) < 0.6f && _state.Ammo >= 1 - }; - - private bool ShouldUseZone(StrategyValues strategy) => strategy.Option(Track.Zone).As() switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat && _state.TargetingEnemy && _state.Unlocked(GNB.AID.SonicBreak) && _state.CD(GNB.AID.NoMercy) <= 57.5 && _state.CD(GNB.AID.NoMercy) > 17 - }; - - private bool ShouldUseFest(StrategyValues strategy) - { - var festStrategy = strategy.Option(Track.Zone).As(); - if (festStrategy == OffensiveStrategy.Delay) - { - return false; - } - else if (festStrategy == OffensiveStrategy.Force) - { - return true; - } - else - { - bool inNoMercy = _state.NoMercyLeft > _state.AnimationLock && _state.Unlocked(GNB.AID.Bloodfest); - return inNoMercy && _state.Ammo == 0; - } - } - - private bool ShouldUseBow(StrategyValues strategy) => strategy.Option(Track.Bow).As() switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat && _state.TargetingEnemy && _state.Unlocked(GNB.AID.BowShock) && _state.CD(GNB.AID.NoMercy) > 40 - }; - - private GNB.AID GetNextBestGCD(StrategyValues strategy, bool aoe) - { - // prepull - if (!_state.TargetingEnemy || _state.CountdownRemaining > 0.7f) - return GNB.AID.None; - - if (strategy.Option(Track.Gauge).As() == GaugeStrategy.LightningShotIfNotInMelee && _state.RangeToTarget > 3) - return GNB.AID.LightningShot; - - if (ShouldSpendGauge(strategy)) - { - if (strategy.Option(Track.Gauge).As() == GaugeStrategy.Spend && _state.Ammo > 0) - { - if (_state.CD(GNB.AID.DoubleDown) < 0.6f && _state.Ammo > 2) - return GNB.AID.DoubleDown; - - if (_state.CD(GNB.AID.GnashingFang) < 0.6f && _state.Ammo <= 2) - { - if (_state.GunComboStep == 0) - return GNB.AID.GnashingFang; - if (_state.GunComboStep == 1) - return GNB.AID.SavageClaw; - if (_state.GunComboStep == 2) - return GNB.AID.WickedTalon; - } - - return GNB.AID.BurstStrike; - } - - if (_state.Ammo == 0 && _state.GunComboStep <= 0) - { - return GNB.AID.KeenEdge; - } - - if (strategy.Option(Track.Gauge).As() == GaugeStrategy.Hold && _state.Ammo >= 0 && _state.NoMercyLeft >= 0 && (aoe || !aoe)) - { - if (_state.ComboLastMove == GNB.AID.DemonSlice) - { - return GNB.AID.DemonSlaughter; - } - else if (_state.ComboLastMove == GNB.AID.BrutalShell) - { - return GNB.AID.SolidBarrel; - } - else if (_state.ComboLastMove == GNB.AID.KeenEdge) - { - return GNB.AID.BrutalShell; - } - - return GNB.AID.DemonSlice; - } - else if (_state.ComboLastMove == GNB.AID.BrutalShell) - { - if (_state.Ammo < 3) - { - return GNB.AID.SolidBarrel; - } - else if (_state.Ammo == 3) - { - return GNB.AID.BurstStrike; - } - } - else if (_state.ComboLastMove == GNB.AID.KeenEdge) - { - return GNB.AID.BrutalShell; - } - - if (strategy.Option(Track.Gauge).As() == GaugeStrategy.ForceGF && _state.Ammo >= 1) - { - if (_state.CD(GNB.AID.GnashingFang) < 0.6f && _state.CD(GNB.AID.DoubleDown) < 0.6f) - { - if (_state.GunComboStep == 0) - { - return GNB.AID.GnashingFang; - } - else if (_state.GunComboStep == 1) - { - return GNB.AID.SavageClaw; - } - else if (_state.GunComboStep == 2) - { - return GNB.AID.WickedTalon; - } - } - else if (_state.GunComboStep == 1) - { - return GNB.AID.SavageClaw; - } - else if (_state.GunComboStep == 2) - { - return GNB.AID.WickedTalon; - } - } - } - - if (!aoe) - { - if (_state.Ammo >= 2 && !_state.Unlocked(GNB.AID.DoubleDown) && - !_state.Unlocked(GNB.AID.Bloodfest) && !_state.Unlocked(GNB.AID.Continuation) && !_state.Unlocked(GNB.AID.GnashingFang) && - !_state.Unlocked(GNB.AID.SonicBreak)) - { - return GNB.AID.BurstStrike; - } - } - // AOE Logic - else if (aoe) - { - if (_state.Ammo >= 2) - { - if (_state.Ammo >= 2) - { - if (_state.Unlocked(GNB.AID.GnashingFang) && _state.CD(GNB.AID.GnashingFang) == 0) - { - return GNB.AID.GnashingFang; // Lv60+ AOE GF - } - if (!_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown)) - { - return GNB.AID.BurstStrike; // Lv30-72 AOE BS - } - } - if (_state.Ammo >= 2 && !_state.Unlocked(GNB.AID.DoubleDown) && - !_state.Unlocked(GNB.AID.Bloodfest) && !_state.Unlocked(GNB.AID.Continuation) && !_state.Unlocked(GNB.AID.GnashingFang) && !_state.Unlocked(GNB.AID.SonicBreak)) - { - return GNB.AID.BurstStrike; // Lv30-53 AOE BS - } - if (_state.Ammo >= 2 && _state.Unlocked(GNB.AID.SonicBreak) && _state.Unlocked(GNB.AID.GnashingFang) && - !_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown)) - { - return GNB.AID.GnashingFang; // Lv60 AOE GF fix - } - else if (_state.Ammo >= 2 && _state.Unlocked(GNB.AID.SonicBreak) && _state.Unlocked(GNB.AID.GnashingFang) && (_state.CD(GNB.AID.GnashingFang) > _state.AnimationLock && !_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown))) - { - return GNB.AID.BurstStrike; // Lv60 AOE BS - } - } - if (_state.Ammo >= 2 && _state.GunComboStep == 0) - { - if (!_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.Bloodfest) && - _state.Unlocked(GNB.AID.Continuation)) - { - return GNB.AID.BurstStrike; // Lv70 AOE BS - } - if (_state.CD(GNB.AID.GnashingFang) > _state.GCD && _state.CD(GNB.AID.DoubleDown) > _state.GCD && - _state.Unlocked(GNB.AID.DoubleDown)) - { - return GNB.AID.FatedCircle; // Lv80 AOE - } - if (_state.CD(GNB.AID.GnashingFang) > _state.GCD && _state.Unlocked(GNB.AID.FatedCircle) && - !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.SonicBreak)) - { - return GNB.AID.FatedCircle; // Lv80 AOE - } - if (_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown) && - !_state.Unlocked(GNB.AID.SonicBreak) && !_state.Unlocked(GNB.AID.GnashingFang)) - { - return GNB.AID.FatedCircle; // Lv80 AOE - } - } - } - - if (_state.ReadyToBlast) - return _state.BestContinuation; - if (_state.ReadyToGouge) - return _state.BestContinuation; - if (_state.ReadyToTear) - return _state.BestContinuation; - if (_state.ReadyToRip) - return _state.BestContinuation; - - if (_state.Ammo >= _state.MaxCartridges && _state.ComboLastMove == GNB.AID.BrutalShell) - return GetNextAmmoAction(strategy, aoe); - - if (_state.Ammo >= _state.MaxCartridges && _state.ComboLastMove == GNB.AID.DemonSlice) - return GetNextAmmoAction(strategy, aoe); - - if (_state.NoMercyLeft > _state.AnimationLock) - return GetNextAmmoAction(strategy, aoe); - - if (_state.CD(GNB.AID.GnashingFang) < 0.6f) - return GetNextAmmoAction(strategy, aoe); - - if (_state.GunComboStep > 0) - { - if (_state.GunComboStep == 2) - return GNB.AID.WickedTalon; - if (_state.GunComboStep == 1) - return GNB.AID.SavageClaw; - } - - if (strategy.Option(Track.Gauge).As() == GaugeStrategy.Spend) - return GetNextAmmoAction(strategy, aoe); - - if (strategy.Option(Track.Gauge).As() == GaugeStrategy.Hold) - return GetNextUnlockedComboAction(strategy, aoe); - - if (strategy.Option(Track.Gauge).As() == GaugeStrategy.ForceGF) - return GetNextAmmoAction(strategy, aoe); - - return GetNextUnlockedComboAction(strategy, aoe); - } - - private ActionID GetNextBestOGCD(StrategyValues strategy, float deadline, bool aoe) - { - if (ShouldUsePotion(strategy) && _state.CanWeave(_state.PotionCD, 1.1f, deadline)) - return ActionDefinitions.IDPotionStr; - - if (_state.Unlocked(GNB.AID.NoMercy)) - { - if (ShouldUseNoMercy(strategy) && _state.CanWeave(GNB.AID.NoMercy, 0.6f, deadline)) - return ActionID.MakeSpell(GNB.AID.NoMercy); - } - - if (_state.Unlocked(GNB.AID.DangerZone) && ShouldUseZone(strategy) && _state.CanWeave(GNB.AID.DangerZone, 0.6f, deadline)) - return ActionID.MakeSpell(_state.BestZone); - - if (_state.Unlocked(GNB.AID.BowShock) && ShouldUseBow(strategy) && _state.CanWeave(GNB.AID.BowShock, 0.6f, deadline)) - return ActionID.MakeSpell(GNB.AID.BowShock); - - if (_state.ReadyToBlast && _state.Unlocked(GNB.AID.Hypervelocity)) - return ActionID.MakeSpell(_state.BestContinuation); - if (_state.ReadyToGouge && _state.Unlocked(GNB.AID.Continuation)) - return ActionID.MakeSpell(_state.BestContinuation); - if (_state.ReadyToTear && _state.Unlocked(GNB.AID.Continuation)) - return ActionID.MakeSpell(_state.BestContinuation); - if (_state.ReadyToRip && _state.Unlocked(GNB.AID.Continuation)) - return ActionID.MakeSpell(_state.BestContinuation); - - if (_state.Unlocked(GNB.AID.Bloodfest) && _state.CanWeave(GNB.AID.Bloodfest, 0.6f, deadline) && ShouldUseFest(strategy)) - return ActionID.MakeSpell(GNB.AID.Bloodfest); - - // Lv30-53 NM proc ST - if (_state.Unlocked(GNB.AID.NoMercy)) - { - if (!_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.Bloodfest) && !_state.Unlocked(GNB.AID.Continuation) && !_state.Unlocked(GNB.AID.GnashingFang) && !_state.Unlocked(GNB.AID.SonicBreak) && _state.Ammo == 2 && _state.CanWeave(GNB.AID.NoMercy, 0.6f, deadline)) - return ActionID.MakeSpell(GNB.AID.NoMercy); - } - - // Lv30-53 NM proc AOE - if (_state.Unlocked(GNB.AID.NoMercy)) - { - if (aoe && !_state.Unlocked(GNB.AID.FatedCircle) && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.Bloodfest) && !_state.Unlocked(GNB.AID.Continuation) && !_state.Unlocked(GNB.AID.GnashingFang) && _state.Ammo == 2 && !_state.Unlocked(GNB.AID.SonicBreak) && _state.Ammo == 2 && _state.CanWeave(GNB.AID.NoMercy, 0.6f, deadline)) - return ActionID.MakeSpell(GNB.AID.NoMercy); - } - - if (_state.CanWeave(_state.CD(GNB.AID.Aurora) - 60, 0.6f, deadline) && _state.Unlocked(GNB.AID.Aurora) && _state.AuroraLeft < _state.GCD && _state.CD(GNB.AID.NoMercy) > 1 && _state.CD(GNB.AID.GnashingFang) > 1 && _state.CD(GNB.AID.DoubleDown) > 1) - return ActionID.MakeSpell(GNB.AID.Aurora); - - if (_state.CanWeave(_state.CD(GNB.AID.Aurora) - 60, 0.6f, deadline) && _state.Unlocked(GNB.AID.Aurora) && !_state.Unlocked(GNB.AID.DoubleDown) && _state.AuroraLeft < _state.GCD && _state.CD(GNB.AID.NoMercy) > 1 && _state.CD(GNB.AID.GnashingFang) > 1) - return ActionID.MakeSpell(GNB.AID.Aurora); - - if (_state.CanWeave(_state.CD(GNB.AID.Aurora) - 60, 0.6f, deadline) && _state.Unlocked(GNB.AID.Aurora) && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.SonicBreak) && _state.AuroraLeft < _state.GCD && _state.CD(GNB.AID.NoMercy) > 1 && _state.CD(GNB.AID.GnashingFang) > 1) - return ActionID.MakeSpell(GNB.AID.Aurora); - - if (_state.CanWeave(_state.CD(GNB.AID.Aurora) - 60, 0.6f, deadline) && _state.Unlocked(GNB.AID.Aurora) && !_state.Unlocked(GNB.AID.DoubleDown) && !_state.Unlocked(GNB.AID.SonicBreak) && !_state.Unlocked(GNB.AID.GnashingFang) && _state.AuroraLeft < _state.GCD && _state.CD(GNB.AID.NoMercy) > 1) - return ActionID.MakeSpell(GNB.AID.Aurora); - - return new(); - } -} diff --git a/BossMod/Autorotation/Legacy/LegacyModule.cs b/BossMod/Autorotation/Legacy/LegacyModule.cs deleted file mode 100644 index 0f714a31bb..0000000000 --- a/BossMod/Autorotation/Legacy/LegacyModule.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace BossMod.Autorotation.Legacy; - -public abstract class LegacyModule(RotationModuleManager manager, Actor player) : RotationModule(manager, player) -{ - protected void PushResult(ActionID action, Actor? target) - { - var data = action ? ActionDefinitions.Instance[action] : null; - if (data == null) - return; - if (data.Range == 0) - target = Player; // override range-0 actions to always target player - if (target == null || Hints.FindEnemy(target)?.Priority == AIHints.Enemy.PriorityForbidden) - return; // forbidden - Hints.ActionsToExecute.Push(action, target, (data.IsGCD ? ActionQueue.Priority.High : ActionQueue.Priority.Low) + 500); - } - protected void PushResult(AID aid, Actor? target) where AID : Enum => PushResult(ActionID.MakeSpell(aid), target); -} diff --git a/BossMod/Autorotation/Legacy/LegacyRPR.cs b/BossMod/Autorotation/Legacy/LegacyRPR.cs deleted file mode 100644 index 660ae61e3c..0000000000 --- a/BossMod/Autorotation/Legacy/LegacyRPR.cs +++ /dev/null @@ -1,678 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; - -namespace BossMod.Autorotation.Legacy; - -public sealed class LegacyRPR : LegacyModule -{ - public enum Track { AOE, Gauge, Bloodstalk, SoulSlice, TrueNorth, Enshroud, ArcaneCircle, Gluttony, Potion } - public enum AOEStrategy { SingleTarget, AutoOnPrimary, ForceAOE } - public enum GaugeStrategy { Automatic, ForceExtendDD, HarpeorHarvestMoonIfNotInMelee, HarvestMoonIfNotInMelee, ForceHarvestMoon, ComboFitBeforeDowntime } - public enum OffensiveStrategy { Automatic, Delay, Force } - public enum PotionStrategy { Manual, Opener, Burst, Force, Special } - - public static RotationModuleDefinition Definition() - { - // TODO: think about target overrides where they make sense - var res = new RotationModuleDefinition("Legacy RPR", "Old pre-refactoring module", "Legacy (pre-DT)", "lazylemo", RotationModuleQuality.WIP, BitMask.Build((int)Class.RPR), 100); - - res.Define(Track.AOE).As("AOE", uiPriority: 100) - .AddOption(AOEStrategy.SingleTarget, "ST", "Use single-target actions") - .AddOption(AOEStrategy.AutoOnPrimary, "AutoOnPrimary", "Use aoe actions on primary target if profitable") - .AddOption(AOEStrategy.ForceAOE, "AOE", "Use aoe rotation on primary target even if it's less total damage than single-target"); - - res.Define(Track.Gauge).As("Gauge", uiPriority: 90) - .AddOption(GaugeStrategy.Automatic, "Automatic") - .AddOption(GaugeStrategy.ForceExtendDD, "ForceExtendDD", "Force extend DD Target Debuff, potentially overcapping DD") - .AddOption(GaugeStrategy.HarpeorHarvestMoonIfNotInMelee, "HarpeorHarvestMoonIfNotInMelee", "Use Harpe or HarvestMoon if outside melee") - .AddOption(GaugeStrategy.HarvestMoonIfNotInMelee, "HarvestMoonIfNotInMelee", "Use only HarvestMoon if outside melee") - .AddOption(GaugeStrategy.ForceHarvestMoon, "ForceHarvestMoon", "Force Harvest Moon") - .AddOption(GaugeStrategy.ComboFitBeforeDowntime, "ComboFitBeforeDowntime", "Use combo, unless it can't be finished before downtime"); - - res.Define(Track.Bloodstalk).As("SOUL", uiPriority: 80) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.SoulSlice).As("SS", uiPriority: 70) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.TrueNorth).As("TrN", uiPriority: 60) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.Enshroud).As("ENSH", uiPriority: 50) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.ArcaneCircle).As("ARC", uiPriority: 40) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.Gluttony).As("Glut", uiPriority: 30) - .AddOption(OffensiveStrategy.Automatic, "Automatic") - .AddOption(OffensiveStrategy.Delay, "Delay") - .AddOption(OffensiveStrategy.Force, "Force"); - - res.Define(Track.Potion).As("Potion", uiPriority: 20) - .AddOption(PotionStrategy.Manual, "Manual", "Potion won't be used automatically") - .AddOption(PotionStrategy.Opener, "Opener") - .AddOption(PotionStrategy.Burst, "Burst", "2+ minute windows") - .AddOption(PotionStrategy.Force, "Force") - .AddOption(PotionStrategy.Special, "Special", "Special (Needs testing)"); - - return res; - } - - // full state needed for determining next action - public class State(RotationModule module) : CommonState(module) - { - public int ShroudGauge; - public int SoulGauge; - public int LemureShroudCount; - public int VoidShroudCount; - public float SoulReaverLeft; - public float EnhancedGibbetLeft; - public float EnhancedGallowsLeft; - public float EnhancedVoidReapingLeft; - public float EnhancedCrossReapingLeft; - public float BloodsownCircleLeft; - public float EnshroudedLeft; - public float ArcaneCircleLeft; - public float ImmortalSacrificeLeft; - public float TrueNorthLeft; - public float EnhancedHarpeLeft; - public bool HasSoulsow; - public float TargetDeathDesignLeft; - public float CircleofSacrificeLeft; - public bool lastActionisSoD; - - public int NumAOEGCDTargets; - public bool UseAOERotation; - - public RPR.AID Beststalk => EnhancedGallowsLeft > AnimationLock ? RPR.AID.UnveiledGallows - : EnhancedGibbetLeft > AnimationLock ? RPR.AID.UnveiledGibbet - : EnshroudedLeft > AnimationLock ? RPR.AID.LemuresSlice - : RPR.AID.BloodStalk; - public RPR.AID BestGallow => EnshroudedLeft > AnimationLock ? RPR.AID.CrossReaping : RPR.AID.Gallows; - public RPR.AID BestGibbet => EnshroudedLeft > AnimationLock ? RPR.AID.VoidReaping : RPR.AID.Gibbet; - public RPR.AID BestSow => HasSoulsow ? RPR.AID.HarvestMoon : RPR.AID.SoulSow; - public RPR.SID ExpectedShadowofDeath => RPR.SID.DeathsDesign; - public RPR.AID ComboLastMove => (RPR.AID)ComboLastAction; - - public bool Unlocked(RPR.AID aid) => Module.ActionUnlocked(ActionID.MakeSpell(aid)); - public bool Unlocked(RPR.TraitID tid) => Module.TraitUnlocked((uint)tid); - - public override string ToString() - { - return $"AOE={NumAOEGCDTargets}, no-dots={ForbidDOTs}, shg={ShroudGauge}, Bloodsown={BloodsownCircleLeft} sog={SoulGauge}, RB={RaidBuffsLeft:f1}, DD={TargetDeathDesignLeft:f1}, EGI={EnhancedGibbetLeft:f1}, EGA={EnhancedGallowsLeft:f1}, CircleofSac={CircleofSacrificeLeft} SoulSlice={CD(RPR.AID.SoulSlice)}, Enshroud={CD(RPR.AID.Enshroud)}, AC={ArcaneCircleLeft}, ACCD={CD(RPR.AID.ArcaneCircle):f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}"; - } - } - - private readonly State _state; - - public LegacyRPR(RotationModuleManager manager, Actor player) : base(manager, player) - { - _state = new(this); - } - - public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) - { - _state.UpdateCommon(primaryTarget, estimatedAnimLockDelay); - _state.HasSoulsow = Player.FindStatus(RPR.SID.Soulsow) != null; - - var gauge = World.Client.GetGauge(); - _state.LemureShroudCount = gauge.LemureShroud; - _state.VoidShroudCount = gauge.VoidShroud; - _state.ShroudGauge = gauge.Shroud; - _state.SoulGauge = gauge.Soul; - //if (_state.ComboLastMove == RPR.AID.InfernalSlice) - // _state.ComboTimeLeft = 0; - - _state.SoulReaverLeft = _state.StatusDetails(Player, RPR.SID.SoulReaver, Player.InstanceID).Left; - _state.ImmortalSacrificeLeft = _state.StatusDetails(Player, RPR.SID.ImmortalSacrifice, Player.InstanceID).Left; - _state.ArcaneCircleLeft = _state.StatusDetails(Player, RPR.SID.ArcaneCircle, Player.InstanceID).Left; - _state.EnhancedGibbetLeft = _state.StatusDetails(Player, RPR.SID.EnhancedGibbet, Player.InstanceID).Left; - _state.EnhancedGallowsLeft = _state.StatusDetails(Player, RPR.SID.EnhancedGallows, Player.InstanceID).Left; - _state.EnhancedVoidReapingLeft = _state.StatusDetails(Player, RPR.SID.EnhancedVoidReaping, Player.InstanceID).Left; - _state.EnhancedCrossReapingLeft = _state.StatusDetails(Player, RPR.SID.EnhancedCrossReaping, Player.InstanceID).Left; - _state.EnhancedHarpeLeft = _state.StatusDetails(Player, RPR.SID.EnhancedHarpe, Player.InstanceID).Left; - _state.EnshroudedLeft = _state.StatusDetails(Player, RPR.SID.Enshrouded, Player.InstanceID).Left; - _state.TrueNorthLeft = _state.StatusDetails(Player, RPR.SID.TrueNorth, Player.InstanceID).Left; - _state.BloodsownCircleLeft = _state.StatusDetails(Player, RPR.SID.BloodsownCircle, Player.InstanceID).Left; - _state.CircleofSacrificeLeft = _state.StatusDetails(Player, RPR.SID.CircleofSacrifice, Player.InstanceID).Left; - //_state.lastActionisSoD = ev.Action.Type == ActionType.Spell && (AID)ev.Action.ID is AID.ShadowofDeath or AID.WhorlofDeath; - note: modules can't really react to actions reliably, they could be recreated at any point... - - // TODO: multidot support - //var adjTarget = initial; - //if (_state.Unlocked(AID.WhorlofDeath) && !WithoutDOT(initial.Actor)) - //{ - // var multidotTarget = Autorot.Hints.PriorityTargets.FirstOrDefault(e => e != initial && !e.ForbidDOTs && e.Actor.Position.InCircle(Player.Position, 5) && WithoutDOT(e.Actor)); - // if (multidotTarget != null) - // adjTarget = multidotTarget; - //} - _state.TargetDeathDesignLeft = _state.StatusDetails(primaryTarget, _state.ExpectedShadowofDeath, Player.InstanceID).Left; - - // TODO: see how BRD/DRG do aoes - var aoe = strategy.Option(Track.AOE).As() switch - { - AOEStrategy.AutoOnPrimary => NumTargetsHitByAOEGCD() >= 3, - AOEStrategy.ForceAOE => true, - _ => false - }; - - _state.UpdatePositionals(primaryTarget, GetNextPositional(), _state.TrueNorthLeft > _state.GCD); - - // TODO: refactor all that, it's kinda senseless now - RPR.AID gcd = GetNextBestGCD(strategy, aoe); - PushResult(gcd, primaryTarget); - - ActionID ogcd = default; - var deadline = _state.GCD > 0 && gcd != default ? _state.GCD : float.MaxValue; - if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline - _state.OGCDSlotLength, aoe); - if (!ogcd && _state.CanWeave(deadline)) // second/only ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline, aoe); - PushResult(ogcd, primaryTarget); - } - - //protected override void QueueAIActions() - //{ - // if (_state.Unlocked(AID.LegSweep)) - // { - // var interruptibleEnemy = Autorot.Hints.PotentialTargets.Find(e => e.ShouldBeInterrupted && (e.Actor.CastInfo?.Interruptible ?? false) && e.Actor.Position.InCircle(Player.Position, 25 + e.Actor.HitboxRadius + Player.HitboxRadius)); - // SimulateManualActionForAI(ActionID.MakeSpell(AID.LegSweep), interruptibleEnemy?.Actor, interruptibleEnemy != null); - // } - // 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); - // // TODO: true north... - //} - - public override string DescribeState() => _state.ToString(); - - private int NumTargetsHitByAOEGCD() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 5); - //private bool WithoutDOT(Actor a) => RefreshDOT(_state.StatusDetails(a, RPR.SID.DeathsDesign, Player.InstanceID).Left); - - // old RPRRotation - //private int SoulGaugeGainedFromAction(RPR.AID action) => action switch - //{ - // RPR.AID.Slice or RPR.AID.WaxingSlice or RPR.AID.InfernalSlice => 10, - // RPR.AID.SoulSlice => 50, - // RPR.AID.SoulScythe => 50, - // RPR.AID.SpinningScythe or RPR.AID.NightmareScythe => 10, - // _ => 0 - //}; - - //private int ShroudGaugeGainedFromAction(RPR.AID action) => action switch - //{ - // RPR.AID.Gibbet or RPR.AID.Gallows or RPR.AID.Guillotine => 10, - // RPR.AID.PlentifulHarvest => 50, - // _ => 0 - //}; - - //private RPR.AID GetNextSTComboAction(RPR.AID comboLastMove, RPR.AID finisher) => comboLastMove switch - //{ - // RPR.AID.WaxingSlice => finisher, - // RPR.AID.Slice => RPR.AID.WaxingSlice, - // _ => RPR.AID.Slice - //}; - - //private int GetSTComboLength(RPR.AID comboLastMove) => comboLastMove switch - //{ - // RPR.AID.WaxingSlice => 1, - // RPR.AID.Slice => 2, - // _ => 3 - //}; - - //private int GetAOEComboLength(RPR.AID comboLastMove) => comboLastMove == RPR.AID.SpinningScythe ? 1 : 2; - - //private RPR.AID GetNextMaimComboAction(RPR.AID comboLastMove) => comboLastMove == RPR.AID.Slice ? RPR.AID.WaxingSlice : RPR.AID.Slice; - - //private RPR.AID GetNextAOEComboAction(RPR.AID comboLastMove) => comboLastMove == RPR.AID.SpinningScythe ? RPR.AID.NightmareScythe : RPR.AID.SpinningScythe; - - private RPR.AID GetNextUnlockedComboAction(bool aoe) - { - if (aoe && _state.Unlocked(RPR.AID.SpinningScythe)) - { - return _state.Unlocked(RPR.AID.NightmareScythe) && _state.ComboLastMove == RPR.AID.SpinningScythe ? RPR.AID.NightmareScythe : RPR.AID.SpinningScythe; - } - else - { - return _state.ComboLastMove switch - { - RPR.AID.WaxingSlice => _state.Unlocked(RPR.AID.InfernalSlice) ? - RPR.AID.InfernalSlice : RPR.AID.Slice, - RPR.AID.Slice => _state.Unlocked(RPR.AID.WaxingSlice) ? RPR.AID.WaxingSlice : RPR.AID.Slice, - _ => RPR.AID.Slice - }; - } - } - - private RPR.AID GetNextBSAction(bool aoe) - { - if (!aoe) - { - if (_state.EnhancedGibbetLeft > _state.GCD) - return RPR.AID.Gibbet; - - if (_state.EnhancedGallowsLeft > _state.GCD) - return RPR.AID.Gallows; - } - - if (aoe) - return RPR.AID.Guillotine; - - return RPR.AID.Gallows; - } - - //private bool RefreshDOT(float timeLeft) => timeLeft < _state.GCD; - - private bool ShouldUseBloodstalk(StrategyValues strategy, bool aoe) - { - bool soulReaver = _state.Unlocked(RPR.AID.BloodStalk) && _state.SoulReaverLeft > _state.AnimationLock; - bool enshrouded = _state.Unlocked(RPR.AID.Enshroud) && _state.EnshroudedLeft > _state.AnimationLock; - switch (strategy.Option(Track.Bloodstalk).As()) - { - case OffensiveStrategy.Delay: - return false; - case OffensiveStrategy.Force: - if (_state.SoulGauge >= 50) - return true; - return false; - - default: - if (!_state.TargetingEnemy) - return false; - if (soulReaver) - return false; - - if (enshrouded) - return false; - - if (ShouldUseEnshroud(strategy) && _state.CD(RPR.AID.Enshroud) < _state.GCD) - return false; - - if (_state.SoulGauge >= 50 && _state.CD(RPR.AID.Gluttony) > 28 && !aoe && (_state.ComboTimeLeft > 2.5 || _state.ComboTimeLeft == 0) && _state.ShroudGauge <= 90 && _state.CD(RPR.AID.ArcaneCircle) > 9) - return true; - - if (_state.SoulGauge == 100 && _state.CD(RPR.AID.Gluttony) > _state.AnimationLock && !aoe && (_state.ComboTimeLeft > 2.5 || _state.ComboTimeLeft == 0) && _state.ShroudGauge <= 90 && _state.ImmortalSacrificeLeft < _state.AnimationLock) - return true; - - if (_state.SoulGauge >= 50 && !aoe && (_state.ComboTimeLeft > 2.5 || _state.ComboTimeLeft == 0) && _state.ImmortalSacrificeLeft > _state.AnimationLock && _state.BloodsownCircleLeft > 4.8f && (_state.CD(RPR.AID.SoulSlice) > 30 || _state.CD(RPR.AID.SoulSlice) < 60) && _state.ShroudGauge <= 40) - return true; - - if ((_state.CD(RPR.AID.ArcaneCircle) < 9 || _state.CD(RPR.AID.ArcaneCircle) > 60) && _state.ShroudGauge >= 50 && (_state.ComboTimeLeft > 11 || _state.ComboTimeLeft == 0)) - return false; - return false; - } - } - - private bool ShouldUseGrimSwathe(StrategyValues strategy, bool aoe) - { - bool soulReaver = _state.Unlocked(RPR.AID.BloodStalk) && _state.SoulReaverLeft > _state.AnimationLock; - bool enshrouded = _state.Unlocked(RPR.AID.Enshroud) && _state.EnshroudedLeft > _state.AnimationLock; - switch (strategy.Option(Track.Bloodstalk).As()) - { - case OffensiveStrategy.Delay: - return false; - case OffensiveStrategy.Force: - if (_state.SoulGauge >= 50) - return true; - return false; - - default: - if (!_state.TargetingEnemy) - return false; - if (soulReaver) - return false; - - if (enshrouded) - return false; - - if (_state.SoulGauge >= 50 && _state.CD(RPR.AID.Gluttony) > 28 && aoe && _state.ShroudGauge <= 90) - return true; - - if (_state.SoulGauge == 100 && _state.CD(RPR.AID.Gluttony) > _state.AnimationLock && aoe && _state.ShroudGauge <= 90) - return true; - - return false; - } - } - - private bool ShouldUseGluttony(StrategyValues strategy) - { - bool soulReaver = _state.Unlocked(RPR.AID.BloodStalk) && _state.SoulReaverLeft > _state.AnimationLock; - bool enshrouded = _state.Unlocked(RPR.AID.Enshroud) && _state.EnshroudedLeft > _state.AnimationLock; - bool plentifulReady = _state.Unlocked(RPR.AID.PlentifulHarvest) && ((_state.ImmortalSacrificeLeft > _state.AnimationLock) || (_state.CircleofSacrificeLeft > _state.AnimationLock)); - switch (strategy.Option(Track.Gluttony).As()) - { - case OffensiveStrategy.Delay: - return false; - case OffensiveStrategy.Force: - if (_state.SoulGauge >= 50) - return true; - return false; - - default: - if (!_state.TargetingEnemy) - return false; - if (soulReaver) - return false; - - if (enshrouded) - return false; - - if (!_state.Unlocked(RPR.AID.Gluttony)) - return false; - - if (ShouldUseEnshroud(strategy)) - return false; - - if ((!plentifulReady || (plentifulReady && _state.BloodsownCircleLeft > 4.8f)) && (_state.ComboTimeLeft > 5 || _state.ComboTimeLeft == 0) && _state.SoulGauge >= 50 && _state.ShroudGauge <= 80 && _state.TargetDeathDesignLeft > 0) - return true; - - return false; - } - } - - private bool ShouldUseEnshroud(StrategyValues strategy) - { - bool soulReaver = _state.Unlocked(RPR.AID.BloodStalk) && _state.SoulReaverLeft > _state.AnimationLock; - bool enshrouded = _state.Unlocked(RPR.AID.Enshroud) && _state.EnshroudedLeft > _state.AnimationLock; - switch (strategy.Option(Track.Enshroud).As()) - { - case OffensiveStrategy.Delay: - return false; - - case OffensiveStrategy.Force: - if (_state.ShroudGauge >= 50) - return true; - return false; - - default: - if (!_state.TargetingEnemy) - return false; - if (soulReaver) - return false; - if (!_state.Unlocked(RPR.AID.Enshroud)) - return false; - - if (enshrouded) - return false; - if (_state.ArcaneCircleLeft > _state.AnimationLock && _state.ShroudGauge >= 50 && (_state.ComboTimeLeft > 11 || _state.ComboTimeLeft == 0) && _state.CD(RPR.AID.Enshroud) < _state.GCD) - return true; - if ((_state.CD(RPR.AID.ArcaneCircle) < 9 || _state.CD(RPR.AID.ArcaneCircle) > 60) && _state.ShroudGauge >= 50 && (_state.ComboTimeLeft > 11 || _state.ComboTimeLeft == 0) && _state.CD(RPR.AID.Enshroud) < _state.GCD) - return true; - - return false; - } - } - - private bool ShouldUseArcaneCircle(StrategyValues strategy) - { - bool soulReaver = _state.Unlocked(RPR.AID.BloodStalk) && _state.SoulReaverLeft > _state.AnimationLock; - bool enshrouded = _state.Unlocked(RPR.AID.Enshroud) && _state.EnshroudedLeft > _state.AnimationLock; - - if (strategy.Option(Track.ArcaneCircle).As() == OffensiveStrategy.Delay) - return false; - - else if (strategy.Option(Track.ArcaneCircle).As() == OffensiveStrategy.Force) - return true; - - else - { - if (!_state.TargetingEnemy) - return false; - if (soulReaver) - return false; - - if (enshrouded && _state.LemureShroudCount is 3 && _state.TargetDeathDesignLeft > 30) - return true; - if (_state.ShroudGauge < 50 && !enshrouded && _state.TargetDeathDesignLeft > 0) - return true; - return false; - } - } - - private bool ShouldUsePotion(StrategyValues strategy) => strategy.Option(Track.Potion).As() switch - { - PotionStrategy.Manual => false, - PotionStrategy.Opener => _state.CD(RPR.AID.ArcaneCircle) > _state.GCD && _state.CD(RPR.AID.SoulSlice) > 0, - PotionStrategy.Burst => _state.CD(RPR.AID.ArcaneCircle) < 9 && _state.lastActionisSoD && _state.TargetDeathDesignLeft > 28, - PotionStrategy.Force => true, - _ => false - }; - - private (Positional, bool) GetNextPositional() - { - if (_state.UseAOERotation) - return default; - - if (_state.Unlocked(RPR.AID.Gibbet) && !_state.UseAOERotation) - { - if (_state.EnhancedGibbetLeft > _state.GCD) - return (Positional.Flank, true); - if (_state.EnhancedGallowsLeft > _state.GCD) - return (Positional.Rear, true); - - return (Positional.Rear, true); - } - else - { - return default; - } - } - - private bool ShouldUseTrueNorth(StrategyValues strategy) - { - switch (strategy.Option(Track.TrueNorth).As()) - { - case OffensiveStrategy.Delay: - return false; - case OffensiveStrategy.Force: - return true; - - default: - if (!_state.TargetingEnemy) - return false; - if (_state.TrueNorthLeft > _state.AnimationLock) - return false; - if (GetNextPositional().Item2 && _state.NextPositionalCorrect && _state.SoulReaverLeft > _state.AnimationLock) - return false; - if (GetNextPositional().Item2 && !_state.NextPositionalCorrect && _state.SoulReaverLeft > _state.AnimationLock) - return true; - if (GetNextPositional().Item2 && !_state.NextPositionalCorrect && ShouldUseGluttony(strategy) && _state.CD(RPR.AID.Gluttony) < 2.5) - return true; - return false; - } - } - - private bool ShouldUseSoulSlice(StrategyValues strategy, bool aoe) - { - //bool plentifulReady = _state.Unlocked(RPR.AID.PlentifulHarvest) && _state.ImmortalSacrificeLeft > _state.AnimationLock && _state.CircleofSacrificeLeft < _state.GCD; - bool soulReaver = _state.Unlocked(RPR.AID.BloodStalk) && _state.SoulReaverLeft > _state.AnimationLock; - bool enshrouded = _state.Unlocked(RPR.AID.Enshroud) && _state.EnshroudedLeft > _state.AnimationLock; - switch (strategy.Option(Track.SoulSlice).As()) - { - case OffensiveStrategy.Delay: - return false; - - case OffensiveStrategy.Force: - return true; - - default: - if (!_state.TargetingEnemy) - return false; - if (_state.SoulGauge <= 50 && _state.CD(RPR.AID.SoulSlice) - 30 < _state.GCD && (_state.ComboTimeLeft > 5 || _state.ComboTimeLeft == 0 || (_state.ArcaneCircleLeft > _state.AnimationLock && _state.ComboTimeLeft > 11)) && _state.CD(RPR.AID.ArcaneCircle) > 11.5f) - return true; - if (enshrouded) - return false; - if (soulReaver) - return false; - if (_state.ArcaneCircleLeft > _state.AnimationLock && _state.ComboTimeLeft < 11) - return false; - return false; - } - } - - private RPR.AID GetNextBestGCD(StrategyValues strategy, bool aoe) - { - if (!_state.TargetingEnemy) - return RPR.AID.None; - - bool plentifulReady = _state.Unlocked(RPR.AID.PlentifulHarvest) && _state.ImmortalSacrificeLeft > _state.AnimationLock && _state.CircleofSacrificeLeft < _state.GCD; - bool soulReaver = _state.Unlocked(RPR.AID.BloodStalk) && _state.SoulReaverLeft > _state.AnimationLock; - bool enshrouded = _state.Unlocked(RPR.AID.Enshroud) && _state.EnshroudedLeft > _state.AnimationLock; - // prepull - if (_state.CountdownRemaining > 4.2f && !_state.HasSoulsow) - return RPR.AID.SoulSow; - if (_state.CountdownRemaining > 1.6f) - return RPR.AID.None; - if (_state.CountdownRemaining > 0) - return RPR.AID.Harpe; - - var gaugeStrategy = strategy.Option(Track.Gauge).As(); - if (gaugeStrategy == GaugeStrategy.HarvestMoonIfNotInMelee && _state.HasSoulsow && _state.RangeToTarget > 3 && Player.InCombat) - return RPR.AID.HarvestMoon; - if (gaugeStrategy == GaugeStrategy.ForceHarvestMoon && _state.HasSoulsow) - return RPR.AID.HarvestMoon; - if (gaugeStrategy == GaugeStrategy.HarpeorHarvestMoonIfNotInMelee && _state.RangeToTarget > 3 && !Player.InCombat) - return RPR.AID.Harpe; - if (gaugeStrategy == GaugeStrategy.HarpeorHarvestMoonIfNotInMelee && _state.HasSoulsow && _state.RangeToTarget > 3 && Player.InCombat) - return RPR.AID.HarvestMoon; - if (gaugeStrategy == GaugeStrategy.HarpeorHarvestMoonIfNotInMelee && !_state.HasSoulsow && _state.RangeToTarget > 3 && Player.InCombat) - return RPR.AID.Harpe; - if (gaugeStrategy == GaugeStrategy.ForceExtendDD && _state.Unlocked(RPR.AID.ShadowofDeath) && !soulReaver) - return aoe ? RPR.AID.WhorlofDeath : RPR.AID.ShadowofDeath; - - var potionStrategy = strategy.Option(Track.Potion).As(); - if (potionStrategy == PotionStrategy.Special && _state.HasSoulsow && (_state.CD(RPR.AID.ArcaneCircle) < 11.5 || _state.CD(RPR.AID.ArcaneCircle) > 115)) - { - if (_state.CD(RPR.AID.ArcaneCircle) < 11.5f && _state.TargetDeathDesignLeft < 30) - return RPR.AID.ShadowofDeath; - if (_state.ComboTimeLeft != 0 || !enshrouded && !soulReaver) - return GetNextUnlockedComboAction(aoe); - if (_state.LemureShroudCount is 3 && !_state.lastActionisSoD && _state.PotionCD < 1) - return RPR.AID.ShadowofDeath; - if (_state.LemureShroudCount is 1) - return RPR.AID.HarvestMoon; - if (enshrouded && !aoe) - { - if (_state.Unlocked(RPR.AID.Communio) && _state.LemureShroudCount is 1 && _state.VoidShroudCount is 0) - return RPR.AID.Communio; - if (_state.EnhancedVoidReapingLeft > _state.AnimationLock) - return RPR.AID.VoidReaping; - if (_state.EnhancedCrossReapingLeft > _state.AnimationLock) - return RPR.AID.CrossReaping; - - return RPR.AID.CrossReaping; - } - - return GetNextUnlockedComboAction(aoe); - } - - if (!aoe) - { - if (_state.Unlocked(RPR.AID.ShadowofDeath) && _state.TargetDeathDesignLeft <= _state.GCD + 2.5f && !soulReaver) - return RPR.AID.ShadowofDeath; - } - else - { - if (_state.Unlocked(RPR.AID.WhorlofDeath) && _state.TargetDeathDesignLeft <= _state.GCD + 2.5f && !soulReaver) - return RPR.AID.WhorlofDeath; - } - - if (plentifulReady && _state.BloodsownCircleLeft < 1 && !soulReaver && !enshrouded && (_state.ComboTimeLeft > 2.5 || _state.ComboTimeLeft == 0)) - return RPR.AID.PlentifulHarvest; - - if ((_state.CD(RPR.AID.Gluttony) < 7.5 && _state.Unlocked(RPR.AID.Gluttony) && !enshrouded && !soulReaver && _state.TargetDeathDesignLeft < 10) || (_state.CD(RPR.AID.Gluttony) > 25 && _state.Unlocked(RPR.AID.Gluttony) && _state.SoulGauge >= 50 && !soulReaver && !enshrouded && _state.TargetDeathDesignLeft < 7.5)) - return RPR.AID.ShadowofDeath; - - if (enshrouded && !aoe) - { - if ((_state.LemureShroudCount == 4 || _state.LemureShroudCount == 3) && (!_state.lastActionisSoD || potionStrategy == PotionStrategy.Burst && !_state.lastActionisSoD && _state.PotionCD < 1) && _state.CD(RPR.AID.ArcaneCircle) < 9) - return RPR.AID.ShadowofDeath; - if (_state.Unlocked(RPR.AID.Communio) && _state.LemureShroudCount is 1 && _state.VoidShroudCount is 0) - return RPR.AID.Communio; - if (_state.Unlocked(RPR.AID.LemuresSlice) && _state.VoidShroudCount >= 2 && _state.CD(RPR.AID.ArcaneCircle) > 10) - return RPR.AID.LemuresSlice; - if (_state.EnhancedVoidReapingLeft > _state.AnimationLock) - return RPR.AID.VoidReaping; - if (_state.EnhancedCrossReapingLeft > _state.AnimationLock) - return RPR.AID.CrossReaping; - - return RPR.AID.CrossReaping; - } - - if (enshrouded && aoe) - { - if (_state.CD(RPR.AID.ArcaneCircle) < 6) - return RPR.AID.WhorlofDeath; - if (_state.Unlocked(RPR.AID.Communio) && _state.LemureShroudCount is 1 && _state.VoidShroudCount is 0) - return RPR.AID.Communio; - - return RPR.AID.GrimReaping; - } - - if (_state.SoulReaverLeft > _state.GCD) - return GetNextBSAction(aoe); - - if (ShouldUseSoulSlice(strategy, aoe) && aoe) - return RPR.AID.SoulScythe; - - if (ShouldUseSoulSlice(strategy, aoe) && !aoe) - return RPR.AID.SoulSlice; - - return GetNextUnlockedComboAction(aoe); - } - - private ActionID GetNextBestOGCD(StrategyValues strategy, float deadline, bool aoe) - { - if (!_state.TargetingEnemy) - return default; - - //bool soulReaver = _state.Unlocked(RPR.AID.BloodStalk) && _state.SoulReaverLeft > _state.AnimationLock; - bool enshrouded = _state.Unlocked(RPR.AID.Enshroud) && _state.EnshroudedLeft > _state.AnimationLock; - //var (positional, shouldUsePositional) = GetNextPositional(); - //if (strategy.Option(Track.ArcaneCircle).As() == Strategy.ArcaneCircleUse.Delay) - // return ActionID.MakeSpell(RPR.AID.Enshroud); - if (strategy.Option(Track.Potion).As() == PotionStrategy.Special && _state.HasSoulsow) - { - if (_state.CD(RPR.AID.ArcaneCircle) < 7.5f && _state.ShroudGauge >= 50 && _state.CanWeave(RPR.AID.Enshroud, 0.6f, deadline)) - return ActionID.MakeSpell(RPR.AID.Enshroud); - if (_state.LemureShroudCount is 3 && _state.CanWeave(_state.PotionCD, 1.1f, deadline) && _state.lastActionisSoD) - return ActionDefinitions.IDPotionStr; - if (_state.LemureShroudCount is 2 && _state.CanWeave(RPR.AID.ArcaneCircle, 0.6f, deadline)) - return ActionID.MakeSpell(RPR.AID.ArcaneCircle); - if (_state.CD(RPR.AID.ArcaneCircle) > 11 && _state.VoidShroudCount >= 2 && _state.CanWeave(RPR.AID.LemuresSlice, 0.6f, deadline)) - return ActionID.MakeSpell(RPR.AID.LemuresSlice); - } - - if (ShouldUsePotion(strategy) && _state.CanWeave(_state.PotionCD, 1.1f, deadline)) - return ActionDefinitions.IDPotionStr; - if (ShouldUseTrueNorth(strategy) && _state.CanWeave(RPR.AID.TrueNorth - 45, 0.6f, deadline) && !aoe && _state.GCD < 0.8) - return ActionID.MakeSpell(RPR.AID.TrueNorth); - if (ShouldUseEnshroud(strategy) && _state.Unlocked(RPR.AID.Enshroud) && _state.CanWeave(RPR.AID.Enshroud, 0.6f, deadline)) - return ActionID.MakeSpell(RPR.AID.Enshroud); - if (ShouldUseArcaneCircle(strategy) && _state.Unlocked(RPR.AID.ArcaneCircle) && _state.CanWeave(RPR.AID.ArcaneCircle, 0.6f, deadline)) - return ActionID.MakeSpell(RPR.AID.ArcaneCircle); - if (_state.VoidShroudCount >= 2 && _state.CanWeave(RPR.AID.LemuresSlice, 0.6f, deadline) && !aoe && _state.CD(RPR.AID.ArcaneCircle) > 10) - return ActionID.MakeSpell(RPR.AID.LemuresSlice); - if (_state.VoidShroudCount >= 2 && _state.CanWeave(RPR.AID.LemuresSlice, 0.6f, deadline) && aoe && _state.CD(RPR.AID.ArcaneCircle) > 10) - return ActionID.MakeSpell(RPR.AID.LemuresScythe); - if (ShouldUseGluttony(strategy) && _state.Unlocked(RPR.AID.Gluttony) && _state.CanWeave(RPR.AID.Gluttony, 0.6f, deadline) && !enshrouded && _state.TargetDeathDesignLeft > 5) - return ActionID.MakeSpell(RPR.AID.Gluttony); - if (ShouldUseBloodstalk(strategy, aoe) && _state.Unlocked(RPR.AID.BloodStalk) && _state.CanWeave(RPR.AID.BloodStalk, 0.6f, deadline) && !enshrouded && _state.TargetDeathDesignLeft > 2.5) - return ActionID.MakeSpell(_state.Beststalk); - if (ShouldUseGrimSwathe(strategy, aoe) && _state.Unlocked(RPR.AID.GrimSwathe) && _state.CanWeave(RPR.AID.BloodStalk, 0.6f, deadline) && !enshrouded && _state.TargetDeathDesignLeft > 2.5) - return ActionID.MakeSpell(RPR.AID.GrimSwathe); - - return new(); - } -} diff --git a/BossMod/Autorotation/Legacy/LegacyWAR.cs b/BossMod/Autorotation/Legacy/LegacyWAR.cs deleted file mode 100644 index c7ddebce9e..0000000000 --- a/BossMod/Autorotation/Legacy/LegacyWAR.cs +++ /dev/null @@ -1,630 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; - -namespace BossMod.Autorotation.Legacy; - -public sealed class LegacyWAR : LegacyModule -{ - public enum Track { AOE, GCD, Infuriate, Potion, InnerRelease, Upheaval, PrimalRend, Onslaught } - public enum AOEStrategy { SingleTarget, ForceAOE, Auto, AutoFinishCombo } - public enum GCDStrategy { Automatic, Spend, ConserveIfNoBuffs, Conserve, ForceExtendST, ForceSPCombo, TomahawkIfNotInMelee, ComboFitBeforeDowntime, PenultimateComboThenSpend, ForceSpend } - public enum InfuriateStrategy { Automatic, Delay, ForceIfNoNC, AutoUnlessIR, ForceIfChargesCapping } - public enum PotionStrategy { Manual, Immediate, DelayUntilRaidBuffs, Force } - public enum OffensiveStrategy { Automatic, Delay, Force } - public enum OnslaughtStrategy { Automatic, Forbid, NoReserve, Force, ForceReserve, ReserveTwo, UseOutsideMelee } - - public static RotationModuleDefinition Definition() - { - // TODO: think about target overrides where they make sense (ST stuff, esp things like onslaught?) - var res = new RotationModuleDefinition("Legacy WAR", "Old pre-refactoring module", "Legacy (pre-DT)", "veyn", RotationModuleQuality.WIP, BitMask.Build((int)Class.WAR), 100); - - res.Define(Track.AOE).As("AOE", uiPriority: 90) - .AddOption(AOEStrategy.SingleTarget, "ST", "Use single-target rotation") - .AddOption(AOEStrategy.ForceAOE, "AOE", "Use aoe rotation") - .AddOption(AOEStrategy.Auto, "Auto", "Use aoe rotation if 3+ targets would be hit, otherwise use single-target rotation; break combo if necessary") - .AddOption(AOEStrategy.AutoFinishCombo, "AutoFinishCombo", "Use aoe rotation if 3+ targets would be hit, otherwise use single-target rotation; finish combo route before switching"); - - res.Define(Track.GCD).As("Gauge", "GCD", uiPriority: 80) - .AddOption(GCDStrategy.Automatic, "Automatic", "Spend gauge either under raid buffs or if next downtime is soon (so that next raid buff window won't cover at least 4 GCDs)") // TODO reconsider... - .AddOption(GCDStrategy.Spend, "Spend", "Spend gauge freely, ensure ST is properly maintained") - .AddOption(GCDStrategy.ConserveIfNoBuffs, "ConserveIfNoBuffs", "Conserve unless under raid buffs") - .AddOption(GCDStrategy.Conserve, "Conserve", "Conserve as much as possible") - .AddOption(GCDStrategy.ForceExtendST, "ForceExtendST", "Force extend ST buff, potentially overcapping gauge and/or ST") - .AddOption(GCDStrategy.ForceSPCombo, "ForceSPCombo", "Force SP combo, potentially overcapping gauge") - .AddOption(GCDStrategy.TomahawkIfNotInMelee, "TomahawkIfNotInMelee", "Use tomahawk if outside melee") - .AddOption(GCDStrategy.ComboFitBeforeDowntime, "ComboFitBeforeDowntime", "Use combo, unless it can't be finished before downtime and unless gauge and/or ST would overcap") - .AddOption(GCDStrategy.PenultimateComboThenSpend, "PenultimateComboThenSpend", "Use combo until second-last step, then spend gauge") - .AddOption(GCDStrategy.ForceSpend, "ForceSpend", "Force gauge spender if possible, even if ST is not up/running out soon"); - - res.Define(Track.Infuriate).As("Infuriate", uiPriority: 70) - .AddOption(InfuriateStrategy.Automatic, "Automatic", "Try to delay uses until raidbuffs, avoiding overcap") - .AddOption(InfuriateStrategy.Delay, "Delay", "Delay, even if risking overcap") - .AddOption(InfuriateStrategy.ForceIfNoNC, "ForceIfNoNC", "Force unless NC active") - .AddOption(InfuriateStrategy.AutoUnlessIR, "AutoUnlessIR", "Use normally, but not during IR") - .AddOption(InfuriateStrategy.ForceIfChargesCapping, "ForceIfChargesCapping", "Force use if charges are about to overcap (unless NC is already active), even if it would overcap gauge") - .AddAssociatedActions(WAR.AID.Infuriate); - - res.Define(Track.Potion).As("Potion", uiPriority: 60) - .AddOption(PotionStrategy.Manual, "Manual", "Do not use automatically") - .AddOption(PotionStrategy.Immediate, "Immediate", "Use ASAP, but delay slightly during opener", 270, 30) - .AddOption(PotionStrategy.DelayUntilRaidBuffs, "DelayUntilRaidBuffs", "Delay until raidbuffs", 270, 30) - .AddOption(PotionStrategy.Force, "Force", "Use ASAP, even if without ST", 270, 30) - .AddAssociatedAction(ActionDefinitions.IDPotionStr); - - res.Define(Track.InnerRelease).As("IR", uiPriority: 50) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "Use normally") - .AddOption(OffensiveStrategy.Delay, "Delay", "Delay") - .AddOption(OffensiveStrategy.Force, "Force", "Force use ASAP (even during downtime or without ST)") - .AddAssociatedActions(WAR.AID.Berserk, WAR.AID.InnerRelease); - - res.Define(Track.Upheaval).As("Upheaval", uiPriority: 40) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "Use normally") - .AddOption(OffensiveStrategy.Delay, "Delay", "Delay") - .AddOption(OffensiveStrategy.Force, "Force", "Force use ASAP (even without ST)") - .AddAssociatedActions(WAR.AID.Upheaval, WAR.AID.Orogeny); - - res.Define(Track.PrimalRend).As("PR", uiPriority: 30) - .AddOption(OffensiveStrategy.Automatic, "Automatic", "Use normally") - .AddOption(OffensiveStrategy.Delay, "Delay", "Delay") - .AddOption(OffensiveStrategy.Force, "Force", "Force use ASAP (do not delay to raidbuffs)") - .AddAssociatedActions(WAR.AID.PrimalRend); - - res.Define(Track.Onslaught).As("Onslaught", uiPriority: 20) - .AddOption(OnslaughtStrategy.Automatic, "Automatic", "Always keep one charge reserved, use other charges under raidbuffs or to prevent overcapping") - .AddOption(OnslaughtStrategy.Forbid, "Forbid", "Forbid automatic use") - .AddOption(OnslaughtStrategy.NoReserve, "NoReserve", "Do not reserve charges: use all charges if under raidbuffs, otherwise use as needed to prevent overcapping") - .AddOption(OnslaughtStrategy.Force, "Force", "Use all charges ASAP") - .AddOption(OnslaughtStrategy.ForceReserve, "ForceReserve", "Use all charges except one ASAP") - .AddOption(OnslaughtStrategy.ReserveTwo, "ReserveTwo", "Reserve 2 charges, trying to prevent overcap") - .AddOption(OnslaughtStrategy.UseOutsideMelee, "UseOutsideMelee", "Use as gapcloser if outside melee range") - .AddAssociatedActions(WAR.AID.Onslaught); - - // TODO: consider these: - //public bool Aggressive; // if true, we use buffs and stuff at last possible moment; otherwise we make sure to keep at least 1 GCD safety net - //public bool OnslaughtHeadroom; // if true, consider onslaught to have slightly higher animation lock than in reality, to account for potential small movement animation delay - - return res; - } - - // full state needed for determining next action - public class State(RotationModule module) : CommonState(module) - { - public int Gauge; // 0 to 100 - public float SurgingTempestLeft; // 0 if buff not up, max 60 - public float NascentChaosLeft; // 0 if buff not up, max 30 - public float PrimalRendLeft; // 0 if buff not up, max 30 - public float PrimalRuinationLeft; // 0 if buff not up, max 30 - public float WrathfulLeft; // 0 if buff not up, max 30 - public float InnerReleaseLeft; // 0 if buff not up, max 15 - public int InnerReleaseStacks; // 0 if buff not up, max 3 - - // upgrade paths - public WAR.AID BestFellCleave => NascentChaosLeft > GCD && Unlocked(WAR.AID.InnerChaos) ? WAR.AID.InnerChaos : Unlocked(WAR.AID.FellCleave) ? WAR.AID.FellCleave : WAR.AID.InnerBeast; - public WAR.AID BestDecimate => NascentChaosLeft > GCD ? WAR.AID.ChaoticCyclone : Unlocked(WAR.AID.Decimate) ? WAR.AID.Decimate : WAR.AID.SteelCyclone; - public WAR.AID BestInnerRelease => Unlocked(WAR.AID.InnerRelease) ? WAR.AID.InnerRelease : WAR.AID.Berserk; - public WAR.AID BestBloodwhetting => Unlocked(WAR.AID.Bloodwhetting) ? WAR.AID.Bloodwhetting : WAR.AID.RawIntuition; - - public WAR.AID ComboLastMove => (WAR.AID)ComboLastAction; - //public float InnerReleaseCD => CD(UnlockedInnerRelease ? AID.InnerRelease : AID.Berserk); // note: technically berserk and IR don't share CD, and with level sync you can have both... - - public bool Unlocked(WAR.AID aid) => Module.ActionUnlocked(ActionID.MakeSpell(aid)); - public bool Unlocked(WAR.TraitID tid) => Module.TraitUnlocked((uint)tid); - - public override string ToString() - { - return $"g={Gauge}, RB={RaidBuffsLeft:f1}, ST={SurgingTempestLeft:f1}, NC={NascentChaosLeft:f1}, PR={PrimalRendLeft:f1}/{PrimalRuinationLeft:f1}, IR={InnerReleaseStacks}/{InnerReleaseLeft:f1}/{WrathfulLeft:f1}, IRCD={CD(WAR.AID.Berserk):f1}/{CD(WAR.AID.InnerRelease):f1}, InfCD={CD(WAR.AID.Infuriate):f1}, UphCD={CD(WAR.AID.Upheaval):f1}, OnsCD={CD(WAR.AID.Onslaught):f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}"; - } - } - - private readonly State _state; - - public LegacyWAR(RotationModuleManager manager, Actor player) : base(manager, player) - { - _state = new(this); - } - - public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) - { - _state.UpdateCommon(primaryTarget, estimatedAnimLockDelay); - _state.HaveTankStance = Player.FindStatus(WAR.SID.Defiance) != null; - - _state.Gauge = World.Client.GetGauge().BeastGauge; - _state.SurgingTempestLeft = _state.StatusDetails(Player, WAR.SID.SurgingTempest, Player.InstanceID).Left; - _state.NascentChaosLeft = _state.StatusDetails(Player, WAR.SID.NascentChaos, Player.InstanceID).Left; - _state.PrimalRendLeft = _state.StatusDetails(Player, WAR.SID.PrimalRend, Player.InstanceID).Left; - _state.PrimalRuinationLeft = _state.StatusDetails(Player, WAR.SID.PrimalRuinationReady, Player.InstanceID).Left; - _state.WrathfulLeft = _state.StatusDetails(Player, WAR.SID.Wrathful, Player.InstanceID).Left; - (_state.InnerReleaseLeft, _state.InnerReleaseStacks) = _state.StatusDetails(Player, _state.Unlocked(WAR.AID.InnerRelease) ? WAR.SID.InnerRelease : WAR.SID.Berserk, Player.InstanceID); - - var aoe = strategy.Option(Track.AOE).As() switch - { - AOEStrategy.ForceAOE => true, - AOEStrategy.Auto => PreferAOE(), - AOEStrategy.AutoFinishCombo => _state.ComboLastMove switch - { - WAR.AID.HeavySwing => !_state.Unlocked(WAR.AID.Maim) && PreferAOE(), - WAR.AID.Maim => !_state.Unlocked(WAR.AID.StormPath) && PreferAOE(), - WAR.AID.Overpower => _state.Unlocked(WAR.AID.MythrilTempest) || PreferAOE(), - _ => PreferAOE() - }, - _ => false, - }; - - // TODO: refactor all that, it's kinda senseless now - WAR.AID gcd = GetNextBestGCD(strategy, aoe); - PushResult(gcd, primaryTarget); - - ActionID ogcd = default; - var deadline = _state.GCD > 0 && gcd != default ? _state.GCD : float.MaxValue; - if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline - _state.OGCDSlotLength, aoe); - if (!ogcd && _state.CanWeave(deadline)) // second/only ogcd slot - ogcd = GetNextBestOGCD(strategy, deadline, aoe); - PushResult(ogcd, primaryTarget); - } - - //protected override void QueueAIActions(ActionQueue queue) - //{ - // if (_state.Unlocked(AID.Interject)) - // { - // var interruptibleEnemy = Autorot.Hints.PotentialTargets.Find(e => e.ShouldBeInterrupted && (e.Actor.CastInfo?.Interruptible ?? false) && e.Actor.Position.InCircle(Player.Position, 3 + e.Actor.HitboxRadius + Player.HitboxRadius)); - // if (interruptibleEnemy != null) - // queue.Push(ActionID.MakeSpell(AID.Interject), interruptibleEnemy.Actor, ActionQueue.Priority.VeryLow + 100); - // } - // if (_state.Unlocked(AID.Defiance)) - // { - // var wantStance = WantStance(); - // if (_state.HaveTankStance != wantStance) - // queue.Push(ActionID.MakeSpell(wantStance ? AID.Defiance : AID.ReleaseDefiance), Player, ActionQueue.Priority.VeryLow + 200); - // } - // if (_state.Unlocked(AID.Provoke)) - // { - // var provokeEnemy = Autorot.Hints.PotentialTargets.Find(e => e.ShouldBeTanked && e.PreferProvoking && e.Actor.TargetID != Player.InstanceID && e.Actor.Position.InCircle(Player.Position, 25 + e.Actor.HitboxRadius + Player.HitboxRadius)); - // if (provokeEnemy != null) - // queue.Push(ActionID.MakeSpell(AID.Provoke), provokeEnemy.Actor, ActionQueue.Priority.VeryLow + 300); - // } - //} - - public override string DescribeState() => _state.ToString(); - - private int NumTargetsHitByAOE() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 5); - private bool PreferAOE() => NumTargetsHitByAOE() >= 3; - - // old WARRotation - private int GaugeGainedFromAction(WAR.AID action) => action switch - { - WAR.AID.Maim or WAR.AID.StormEye => 10, - WAR.AID.StormPath => 20, - WAR.AID.MythrilTempest => _state.Unlocked(WAR.TraitID.MasteringTheBeast) ? 20 : 0, - _ => 0 - }; - - private WAR.AID GetNextSTComboAction(WAR.AID comboLastMove, WAR.AID finisher) => comboLastMove switch - { - WAR.AID.Maim => finisher, - WAR.AID.HeavySwing => WAR.AID.Maim, - _ => WAR.AID.HeavySwing - }; - - private int GetSTComboLength(WAR.AID comboLastMove) => comboLastMove switch - { - WAR.AID.Maim => 1, - WAR.AID.HeavySwing => 2, - _ => 3 - }; - - private int GetAOEComboLength(WAR.AID comboLastMove) => comboLastMove == WAR.AID.Overpower ? 1 : 2; - - //private WAR.AID GetNextMaimComboAction(WAR.AID comboLastMove) => comboLastMove == WAR.AID.HeavySwing ? WAR.AID.Maim : WAR.AID.HeavySwing; - - private WAR.AID GetNextAOEComboAction(WAR.AID comboLastMove) => comboLastMove == WAR.AID.Overpower ? WAR.AID.MythrilTempest : WAR.AID.Overpower; - - private WAR.AID GetNextUnlockedComboAction(State state, float minBuffToRefresh, bool aoe) - { - if (aoe && state.Unlocked(WAR.AID.Overpower)) - { - // for AOE rotation, assume dropping ST combo is fine - return state.Unlocked(WAR.AID.MythrilTempest) && state.ComboLastMove == WAR.AID.Overpower ? WAR.AID.MythrilTempest : WAR.AID.Overpower; - } - else - { - // for ST rotation, assume dropping AOE combo is fine (HS is 200 pot vs MT 100, is 20 gauge + 30 sec ST worth it?..) - return state.ComboLastMove switch - { - WAR.AID.Maim => state.Unlocked(WAR.AID.StormPath) ? (state.Unlocked(WAR.AID.StormEye) && state.SurgingTempestLeft < minBuffToRefresh ? WAR.AID.StormEye : WAR.AID.StormPath) : WAR.AID.HeavySwing, - WAR.AID.HeavySwing => state.Unlocked(WAR.AID.Maim) ? WAR.AID.Maim : WAR.AID.HeavySwing, - _ => WAR.AID.HeavySwing - }; - } - } - - private WAR.AID GetNextFCAction(bool aoe) - { - // note: under nascent chaos, if IC is not unlocked yet, we want to use cyclone even in non-aoe situations - if (_state.NascentChaosLeft > _state.GCD) - return _state.Unlocked(WAR.AID.InnerChaos) && !aoe ? WAR.AID.InnerChaos : WAR.AID.ChaoticCyclone; - - // aoe gauge spender - if (aoe && _state.Unlocked(WAR.AID.SteelCyclone)) - return _state.Unlocked(WAR.AID.Decimate) ? WAR.AID.Decimate : WAR.AID.SteelCyclone; - - // single-target gauge spender - return _state.Unlocked(WAR.AID.FellCleave) ? WAR.AID.FellCleave : WAR.AID.InnerBeast; - } - - // by default, we spend resources either under raid buffs or if another raid buff window will cover at least 4 GCDs of the fight - private bool ShouldSpendGauge(GCDStrategy strategy, bool aoe) => strategy switch - { - GCDStrategy.Automatic or GCDStrategy.TomahawkIfNotInMelee => (_state.RaidBuffsLeft > _state.GCD || _state.FightEndIn <= _state.RaidBuffsIn + 10) && _state.SurgingTempestLeft > _state.GCD, - GCDStrategy.Spend or GCDStrategy.ForceSpend => true, - GCDStrategy.ConserveIfNoBuffs => _state.RaidBuffsLeft > _state.GCD, - GCDStrategy.Conserve => false, - GCDStrategy.ForceExtendST => false, - GCDStrategy.ForceSPCombo => false, - GCDStrategy.ComboFitBeforeDowntime => _state.SurgingTempestLeft > _state.GCD && _state.FightEndIn <= _state.GCD + 2.5f * ((aoe ? GetAOEComboLength(_state.ComboLastMove) : GetSTComboLength(_state.ComboLastMove)) - 1), - GCDStrategy.PenultimateComboThenSpend => _state.ComboLastMove is WAR.AID.Maim or WAR.AID.Overpower, - _ => true - }; - - private bool ShouldUseInfuriate(InfuriateStrategy strategy, GCDStrategy gcdStrategy, bool aoe) - { - switch (strategy) - { - case InfuriateStrategy.Delay: - return false; - - case InfuriateStrategy.ForceIfNoNC: - return _state.NascentChaosLeft <= _state.GCD; - - case InfuriateStrategy.ForceIfChargesCapping: - return _state.NascentChaosLeft <= _state.GCD && _state.CD(WAR.AID.Infuriate) <= _state.AnimationLock; - - default: - if (!_state.TargetingEnemy) - return false; // don't cast during downtime - if (_state.Gauge > 50) - return false; // never cast infuriate if doing so would overcap gauge - if (_state.NascentChaosLeft > _state.GCD) - return false; // never cast infuriate if NC from previous infuriate is still up for next GCD - if (_state.Unlocked(WAR.AID.ChaoticCyclone) && _state.InnerReleaseLeft > _state.GCD && _state.InnerReleaseLeft <= _state.GCD + 2.5f * _state.InnerReleaseStacks) - return false; // never cast infuriate if it will cause us to lose IR stacks - - // different logic before IR and after IR - if (_state.Unlocked(WAR.AID.InnerRelease)) - { - if (strategy == InfuriateStrategy.AutoUnlessIR && _state.InnerReleaseLeft > _state.GCD) - return false; - - // with IR, main purpose of infuriate is to generate gauge to burn in spend mode - if (ShouldSpendGauge(gcdStrategy, aoe)) - return true; - - // don't delay if we risk overcapping stacks - // max safe cooldown calculation: - // - start with remaining GCD + grace period; if CD is smaller, by the time we get a chance to reconsider, we'll have 2 stacks - // grace period should at very least be LockDelay, but next-best GCD could be Primal Rend with longer animation lock, plus we might prioritize different oGCDs, so use full extra GCD to be safe - // - if next GCD could give us >50 gauge, we'd need one more GCD to cast FC (which would also reduce cd by extra 5 seconds), so add 7.5s - // - if IR is imminent, we delay infuriate now, cast some GCD that gives us >50 gauge, we'd need to cast 3xFCs, which would add extra 22.5s - // - if IR is active, we delay infuriate now, we might need to spend remaining GCDs on FCs, which would add extra N * 7.5s - float maxInfuriateCD = _state.GCD + 2.5f; - int gaugeCap = _state.ComboLastMove == WAR.AID.None ? 50 : (_state.ComboLastMove == WAR.AID.HeavySwing ? 40 : 30); - if (_state.Gauge > gaugeCap) - maxInfuriateCD += 7.5f; - bool irImminent = _state.CD(WAR.AID.InnerRelease) < _state.GCD + 2.5; - maxInfuriateCD += (irImminent ? 3 : _state.InnerReleaseStacks) * 7.5f; - if (_state.CD(WAR.AID.Infuriate) <= maxInfuriateCD) - return true; - } - else - { - // before IR, main purpose of infuriate is to maximize buffed FCs under Berserk - if (_state.InnerReleaseLeft > _state.GCD) - return true; - - // don't delay if we risk overcapping stacks - if (_state.CD(WAR.AID.Infuriate) <= _state.GCD + 10) - return true; - - // TODO: consider whether we want to spend both stacks in spend mode if Berserk is not imminent... - } - return false; - } - } - - // note: this check will not allow using non-forced potions before lvl 50, but who cares... - private bool ShouldUsePotion(PotionStrategy strategy) => strategy switch - { - PotionStrategy.Manual => false, - PotionStrategy.Immediate => _state.SurgingTempestLeft > 0 || _state.ComboLastMove == WAR.AID.Maim, // TODO: reconsider potion use during opener (delayed IR prefers after maim, early IR prefers after storm eye, to cover third IC on 13th GCD) - PotionStrategy.DelayUntilRaidBuffs => _state.SurgingTempestLeft > 0 && _state.RaidBuffsLeft > 0, - PotionStrategy.Force => true, - _ => false - }; - - // by default, we use IR asap as soon as ST is up - // TODO: early IR option: technically we can use right after heavy swing, we'll use maim->SE->IC->3xFC - private bool ShouldUseInnerRelease(OffensiveStrategy strategy) => strategy switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat && _state.TargetingEnemy && _state.SurgingTempestLeft > _state.GCD + 5 - }; - - // check whether berserk should be delayed (we want to spend it on FCs) - // this is relevant only until we unlock IR - private bool ShouldUseBerserk(OffensiveStrategy strategy, bool aoe) - { - if (strategy != OffensiveStrategy.Automatic) - return strategy == OffensiveStrategy.Force; - - if (!Player.InCombat) - return false; // don't use before pull - - if (!_state.TargetingEnemy) - return false; // no target, maybe downtime? - - if (_state.Unlocked(WAR.AID.StormEye) && _state.SurgingTempestLeft <= _state.GCD + 5) - return false; // no ST yet - - if (aoe) - return true; // don't delay during aoe - - if (_state.Unlocked(WAR.AID.Infuriate)) - { - // we really want to cast SP + 2xIB or 3xIB under berserk; check whether we'll have infuriate before third GCD - var availableGauge = _state.Gauge; - if (_state.CD(WAR.AID.Infuriate) <= 65) - availableGauge += 50; - return _state.ComboLastMove switch - { - WAR.AID.Maim => availableGauge >= 80, // TODO: this isn't a very good check, improve... - _ => availableGauge == 150 - }; - } - else if (_state.Unlocked(WAR.AID.InnerBeast)) - { - // pre level 50 we ideally want to cast SP + 2xIB under berserk (we need to have 80+ gauge for that) - // however, we are also content with casting Maim + SP + IB (we need to have 20+ gauge for that; but if we have 70+, it is better to delay for 1 GCD) - // alternatively, we could delay for 3 GCDs at 40+ gauge - TODO determine which is better - return _state.ComboLastMove switch - { - WAR.AID.HeavySwing => _state.Gauge is >= 20 and < 70, - WAR.AID.Maim => _state.Gauge >= 80, - _ => false, - }; - } - else - { - // pre level 35 there is no point delaying berserk at all - return true; - } - } - - // by default, we use upheaval asap as soon as ST is up - // TODO: consider delaying for 1 GCD during opener... - private bool ShouldUseUpheaval(OffensiveStrategy strategy) => strategy switch - { - OffensiveStrategy.Delay => false, - OffensiveStrategy.Force => true, - _ => Player.InCombat && _state.TargetingEnemy && _state.SurgingTempestLeft > MathF.Max(_state.CD(WAR.AID.Upheaval), _state.AnimationLock) - }; - - private bool ShouldUseOnslaught(OnslaughtStrategy strategy) - { - switch (strategy) - { - case OnslaughtStrategy.Forbid: - return false; - case OnslaughtStrategy.Force: - return true; - case OnslaughtStrategy.ForceReserve: - return _state.CD(WAR.AID.Onslaught) <= 30 + _state.AnimationLock; - case OnslaughtStrategy.ReserveTwo: - return _state.CD(WAR.AID.Onslaught) - (_state.Unlocked(WAR.TraitID.EnhancedOnslaught) ? 0 : 30) <= _state.GCD; - case OnslaughtStrategy.UseOutsideMelee: - return _state.RangeToTarget > 3; - default: - if (!Player.InCombat) - return false; // don't use out of combat - if (_state.RangeToTarget > 3) - return false; // don't use out of melee range to prevent fucking up player's position - if (_state.PositionLockIn <= _state.AnimationLock) - return false; // forbidden due to _state flags - if (_state.SurgingTempestLeft <= _state.AnimationLock) - return false; // delay until ST, even if overcapping charges - float chargeCapIn = _state.CD(WAR.AID.Onslaught) - (_state.Unlocked(WAR.TraitID.EnhancedOnslaught) ? 0 : 30); - if (chargeCapIn < _state.GCD + 2.5) - return true; // if we won't onslaught now, we risk overcapping charges - if (strategy != OnslaughtStrategy.NoReserve && _state.CD(WAR.AID.Onslaught) > 30 + _state.AnimationLock) - return false; // strategy prevents us from using last charge - if (_state.RaidBuffsLeft > _state.AnimationLock) - return true; // use now, since we're under raid buffs - return chargeCapIn <= _state.RaidBuffsIn; // use if we won't be able to delay until next raid buffs - } - } - - private WAR.AID GetNextBestGCD(StrategyValues strategy, bool aoe) - { - // prepull or no target - if (!_state.TargetingEnemy || _state.CountdownRemaining > 0.7f) - return WAR.AID.None; - - // 0. non-standard actions forced by strategy - // forced PR - var strategyPR = strategy.Option(Track.PrimalRend).As(); - if (strategyPR == OffensiveStrategy.Force && _state.PrimalRendLeft > _state.GCD) - return WAR.AID.PrimalRend; - // forced tomahawk - var strategyGCD = strategy.Option(Track.GCD).As(); - if (strategyGCD == GCDStrategy.TomahawkIfNotInMelee && _state.RangeToTarget > 3) - return WAR.AID.Tomahawk; - // forced surging tempest combo (TODO: at which point does AOE combo start giving ST?) - if (strategyGCD == GCDStrategy.ForceExtendST && _state.Unlocked(WAR.AID.StormEye)) - return aoe ? GetNextAOEComboAction(_state.ComboLastMove) : GetNextSTComboAction(_state.ComboLastMove, WAR.AID.StormEye); - // forced SP combo - if (strategyGCD == GCDStrategy.ForceSPCombo) - return GetNextSTComboAction(_state.ComboLastMove, WAR.AID.StormPath); - // forced combo until penultimate step - if (strategyGCD == GCDStrategy.PenultimateComboThenSpend && _state.ComboLastMove != WAR.AID.Maim && _state.ComboLastMove != WAR.AID.Overpower && (_state.ComboLastMove != WAR.AID.HeavySwing || _state.Gauge <= 90)) - return aoe ? WAR.AID.Overpower : _state.ComboLastMove == WAR.AID.HeavySwing ? WAR.AID.Maim : WAR.AID.HeavySwing; - // forced gauge spender - bool canUseFC = _state.Gauge >= 50 || _state.InnerReleaseStacks > 0 && _state.Unlocked(WAR.AID.InnerRelease); - if (strategyGCD == GCDStrategy.ForceSpend && canUseFC) - return GetNextFCAction(aoe); - - // forbid automatic PR when out of melee range, to avoid fucking up player positioning when avoiding mechanics - float primalRendWindow = (strategyPR == OffensiveStrategy.Delay || _state.RangeToTarget > 3) ? 0 : MathF.Min(_state.PrimalRendLeft, _state.PositionLockIn); - float primalRuinationWindow = _state.PrimalRuinationLeft; // TODO: reconsider - var irCD = _state.CD(_state.Unlocked(WAR.AID.InnerRelease) ? WAR.AID.InnerRelease : WAR.AID.Berserk); - - bool spendGauge = ShouldSpendGauge(strategyGCD, aoe); - if (!_state.Unlocked(WAR.AID.InnerRelease)) - spendGauge &= irCD > 5; // TODO: improve... - - // 1. if it is the last CD possible for PR/NC, don't waste them - bool aggressive = false; // TODO: strategy? or don't care? - float gcdDelay = _state.GCD + (aggressive ? 0 : 2.5f); - float secondGCDIn = gcdDelay + 2.5f; - float thirdGCDIn = gcdDelay + 5f; - if (primalRendWindow > _state.GCD && primalRendWindow < secondGCDIn) - return WAR.AID.PrimalRend; - if (primalRuinationWindow > _state.GCD && primalRuinationWindow < secondGCDIn) - return WAR.AID.PrimalRuination; // TODO: reconsider - if (_state.NascentChaosLeft > _state.GCD && _state.NascentChaosLeft < secondGCDIn) - return GetNextFCAction(aoe); - if (primalRendWindow > _state.GCD && _state.NascentChaosLeft > _state.GCD && primalRendWindow < thirdGCDIn && _state.NascentChaosLeft < thirdGCDIn) - return WAR.AID.PrimalRend; // either is fine - - // 2. if IR/berserk is up, don't waste charges - if (_state.InnerReleaseStacks > 0) - { - if (_state.Unlocked(WAR.AID.InnerRelease)) - { - // only consider not casting FC action if delaying won't cost IR stack - int fcCastsLeft = _state.InnerReleaseStacks; - if (_state.NascentChaosLeft > _state.GCD) - ++fcCastsLeft; - if (_state.InnerReleaseLeft <= _state.GCD + fcCastsLeft * 2.5f) - return GetNextFCAction(aoe); - - // don't delay if it won't give us anything (but still prefer PR under buffs) - TODO: reconsider... - if (spendGauge || _state.InnerReleaseLeft <= _state.RaidBuffsIn) - return !spendGauge ? GetNextFCAction(aoe) - : primalRendWindow > _state.GCD ? WAR.AID.PrimalRend - : primalRuinationWindow > _state.GCD ? WAR.AID.PrimalRuination - : GetNextFCAction(aoe); - - // don't delay FC if it can cause infuriate overcap (e.g. we use combo action, gain gauge and then can't spend it in time) - if (_state.CD(WAR.AID.Infuriate) < _state.GCD + (_state.InnerReleaseStacks + 1) * 7.5f) - return GetNextFCAction(aoe); - - } - else if (_state.Gauge >= 50 && (_state.Unlocked(WAR.AID.FellCleave) || _state.ComboLastMove != WAR.AID.Maim || aoe && _state.Unlocked(WAR.AID.SteelCyclone))) - { - // single-target: FC > SE/ST > IB > Maim > HS - // aoe: Decimate > SC > Combo - return GetNextFCAction(aoe); - } - } - - // 3. no ST (or it will expire if we don't combo asap) => apply buff asap - // TODO: what if we have really high gauge and low ST? is it worth it to delay ST application to avoid overcapping gauge? - if (!aoe) - { - if (_state.Unlocked(WAR.AID.StormEye) && _state.SurgingTempestLeft <= _state.GCD + 2.5f * GetSTComboLength(_state.ComboLastMove)) - return GetNextSTComboAction(_state.ComboLastMove, WAR.AID.StormEye); - } - else - { - if (_state.Unlocked(WAR.TraitID.MasteringTheBeast) && _state.SurgingTempestLeft <= _state.GCD + 2.5f * (_state.ComboLastMove != WAR.AID.Overpower ? 2 : 1)) - return GetNextAOEComboAction(_state.ComboLastMove); - } - - // 4. if we're delaying Infuriate due to gauge, cast FC asap (7.5 for FC) - if (_state.Gauge > 50 && _state.Unlocked(WAR.AID.Infuriate) && _state.CD(WAR.AID.Infuriate) <= gcdDelay + 7.5) - return GetNextFCAction(aoe); - - // 5. if we have >50 gauge, IR is imminent, and not spending gauge now will cause us to overcap infuriate, spend gauge asap - // 30 seconds is for FC + IR + 3xFC - this is 4 gcds (10 sec) and 4 FCs (another 20 sec) - if (_state.Gauge > 50 && _state.Unlocked(WAR.AID.Infuriate) && _state.CD(WAR.AID.Infuriate) <= gcdDelay + 30 && irCD < secondGCDIn) - return GetNextFCAction(aoe); - - // 6. if there is no chance we can delay PR until next raid buffs, just cast it now - if (primalRendWindow > _state.GCD && primalRendWindow <= _state.RaidBuffsIn) - return WAR.AID.PrimalRend; - if (primalRuinationWindow > _state.GCD && primalRuinationWindow <= _state.RaidBuffsIn) - return WAR.AID.PrimalRuination; - - // TODO: do not spend gauge if we're delaying berserk - if (!spendGauge) - { - // we want to delay spending gauge unless doing so will cause us problems later - var maxSTToAvoidOvercap = 20 + Math.Clamp(irCD, 0, 10); - var nextCombo = GetNextUnlockedComboAction(_state, maxSTToAvoidOvercap, aoe); - if (_state.Gauge + GaugeGainedFromAction(nextCombo) <= 100) - return nextCombo; - } - - // ok at this point, we just want to spend gauge - either because we're using greedy strategy, or something prevented us from casting combo - if (primalRendWindow > _state.GCD) - return WAR.AID.PrimalRend; - if (primalRuinationWindow > _state.GCD) - return WAR.AID.PrimalRuination; - if (canUseFC) - return GetNextFCAction(aoe); - - // TODO: reconsider min time left... - return GetNextUnlockedComboAction(_state, strategyGCD == GCDStrategy.ForceSpend ? 0 : gcdDelay + 12.5f, aoe); - } - - // window-end is either GCD or GCD - time-for-second-ogcd; we are allowed to use ogcds only if their animation lock would complete before window-end - private ActionID GetNextBestOGCD(StrategyValues strategy, float deadline, bool aoe) - { - // 0. onslaught as a gap-filler - this should be used asap even if we're delaying GCD, since otherwise we'll probably end up delaying it even more - var strategyOnslaught = strategy.Option(Track.Onslaught).As(); - bool wantOnslaught = _state.Unlocked(WAR.AID.Onslaught) && _state.TargetingEnemy && ShouldUseOnslaught(strategyOnslaught); - if (wantOnslaught && _state.RangeToTarget > 3) - return ActionID.MakeSpell(WAR.AID.Onslaught); - - // 1. potion - var strategyPotion = strategy.Option(Track.Potion).As(); - if (ShouldUsePotion(strategyPotion) && _state.CanWeave(_state.PotionCD, 1.1f, deadline)) - return ActionDefinitions.IDPotionStr; - - // 2. inner release / berserk - var strategyIR = strategy.Option(Track.InnerRelease).As(); - if (_state.Unlocked(WAR.AID.InnerRelease)) - { - if (ShouldUseInnerRelease(strategyIR) && _state.CanWeave(WAR.AID.InnerRelease, 0.6f, deadline)) - return ActionID.MakeSpell(WAR.AID.InnerRelease); - } - else if (_state.Unlocked(WAR.AID.Berserk)) - { - if (ShouldUseBerserk(strategyIR, aoe) && _state.CanWeave(WAR.AID.Berserk, 0.6f, deadline)) - return ActionID.MakeSpell(WAR.AID.Berserk); - } - - // 3. upheaval - // TODO: reconsider priority compared to IR - var strategyUpheaval = strategy.Option(Track.Upheaval).As(); - if (_state.Unlocked(WAR.AID.Upheaval) && ShouldUseUpheaval(strategyUpheaval) && _state.CanWeave(WAR.AID.Upheaval, 0.6f, deadline)) - return ActionID.MakeSpell(aoe && _state.Unlocked(WAR.AID.Orogeny) ? WAR.AID.Orogeny : WAR.AID.Upheaval); - - // 4. infuriate, if not forbidden and not delayed; note that infuriate can't be used out of combat - var strategyGCD = strategy.Option(Track.GCD).As(); - var strategyInfuriate = strategy.Option(Track.Infuriate).As(); - if (_state.Unlocked(WAR.AID.Infuriate) && Player.InCombat && _state.CanWeave(_state.CD(WAR.AID.Infuriate) - 60, 0.6f, deadline) && ShouldUseInfuriate(strategyInfuriate, strategyGCD, aoe)) - return ActionID.MakeSpell(WAR.AID.Infuriate); - - // 5. onslaught, if surging tempest up and not forbidden - bool onslaughtHeadroom = true; // TODO: customize via strategy?.. - if (wantOnslaught && _state.CanWeave(_state.CD(WAR.AID.Onslaught) - 60, onslaughtHeadroom ? 0.8f : 0.6f, deadline)) - return ActionID.MakeSpell(WAR.AID.Onslaught); - - // 6. primal wrath (TODO: reconsider) - if (_state.WrathfulLeft > _state.AnimationLock && Player.InCombat && _state.CanWeave(WAR.AID.PrimalWrath, 0.6f, deadline)) - return ActionID.MakeSpell(WAR.AID.PrimalWrath); - - // no suitable oGCDs... - return new(); - } -} diff --git a/BossMod/AutorotationLegacy/AutorotationLegacy.cs b/BossMod/AutorotationLegacy/AutorotationLegacy.cs deleted file mode 100644 index 19f989a2a6..0000000000 --- a/BossMod/AutorotationLegacy/AutorotationLegacy.cs +++ /dev/null @@ -1,26 +0,0 @@ -//using ImGuiNET; - -//namespace BossMod; - -//sealed class AutorotationLegacy : IDisposable -//{ -// public unsafe void Update(ActionQueue queue) -// { -// if (Config.Enabled && player != null) -// { -// classType = player.Class switch -// { -// //Class.RPR => typeof(RPR.Actions), -// //Class.SAM => typeof(SAM.Actions), -// //Class.DNC => typeof(DNC.Actions), -// //Class.GNB => typeof(GNB.Actions), -// //Class.PLD => player.Level <= 60 ? typeof(PLD.Actions) : null, -// //Class.BLM => player.Level <= 60 ? typeof(BLM.Actions) : null, -// //Class.SCH => player.Level <= 60 ? typeof(SCH.Actions) : null, -// //Class.SMN => player.Level <= 30 ? typeof(SMN.Actions) : null, -// //Class.WHM => typeof(WHM.Actions), -// _ => null -// }; -// } -// } -//} diff --git a/BossMod/AutorotationLegacy/BLM/BLMActions.cs b/BossMod/AutorotationLegacy/BLM/BLMActions.cs deleted file mode 100644 index 1b39e186ba..0000000000 --- a/BossMod/AutorotationLegacy/BLM/BLMActions.cs +++ /dev/null @@ -1,125 +0,0 @@ -//namespace BossMod.BLM; - -//class Actions : CommonActions -//{ -// public const int AutoActionST = AutoActionFirstCustom + 0; -// public const int AutoActionAOE = AutoActionFirstCustom + 1; - -// private readonly Rotation.State _state; -// private readonly Rotation.Strategy _strategy; -// private DateTime _lastManaTick; -// private uint _prevMP; -// private readonly ConfigListener _config; - -// public Actions(Autorotation autorot, Actor player) -// : base(autorot, player, Definitions.UnlockQuests) -// { -// _state = new(autorot.WorldState); -// _strategy = new(); -// _prevMP = player.HPMP.CurMP; -// _config = Service.Config.GetAndSubscribe(OnConfigModified); -// } - -// protected override void Dispose(bool disposing) -// { -// _config.Dispose(); -// base.Dispose(disposing); -// } - -// public override CommonRotation.PlayerState GetState() => _state; -// public override CommonRotation.Strategy GetStrategy() => _strategy; - -// public override Targeting SelectBetterTarget(AIHints.Enemy initial) -// { -// // TODO: multidot?.. -// var bestTarget = initial; -// if (_state.Unlocked(AID.Blizzard2)) -// { -// bestTarget = FindBetterTargetBy(initial, 25, e => NumTargetsHitByAOE(e.Actor)).Target; -// } -// return new(bestTarget, bestTarget.StayAtLongRange ? 25 : 15); -// } - -// protected override void UpdateInternalState(int autoAction) -// { -// UpdatePlayerState(); -// FillCommonStrategy(_strategy, ActionDefinitions.IDPotionInt); -// if (autoAction == AutoActionAIFight) -// { -// _strategy.NumAOETargets = Autorot.PrimaryTarget != null ? NumTargetsHitByAOE(Autorot.PrimaryTarget) : 0; -// } -// else -// { -// _strategy.NumAOETargets = autoAction == AutoActionAOE ? 100 : 0; // TODO: consider making AI-like check -// } -// } - -// protected override void QueueAIActions() -// { -// if (_state.Unlocked(AID.Transpose)) -// SimulateManualActionForAI(ActionID.MakeSpell(AID.Transpose), Player, !Player.InCombat && _state.ElementalLevel > 0 && _state.CurMP < 10000); -// if (_state.Unlocked(AID.Manaward)) -// SimulateManualActionForAI(ActionID.MakeSpell(AID.Manaward), Player, Player.HPMP.CurHP < Player.HPMP.MaxHP * 0.8f); -// } - -// protected override ActionQueue.Entry CalculateAutomaticGCD() -// { -// if (Autorot.PrimaryTarget == null || AutoAction < AutoActionAIFight) -// return default; -// var aid = Rotation.GetNextBestGCD(_state, _strategy); -// return MakeResult(aid, Autorot.PrimaryTarget); -// } - -// protected override ActionQueue.Entry CalculateAutomaticOGCD(float deadline) -// { -// if (Autorot.PrimaryTarget == null || AutoAction < AutoActionAIFight) -// return default; - -// ActionID res = new(); -// if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot -// res = Rotation.GetNextBestOGCD(_state, _strategy, deadline - _state.OGCDSlotLength); -// if (!res && _state.CanWeave(deadline)) // second/only ogcd slot -// res = Rotation.GetNextBestOGCD(_state, _strategy, deadline); -// return MakeResult(res, Autorot.PrimaryTarget); -// } - -// private void UpdatePlayerState() -// { -// FillCommonPlayerState(_state); - -// var gauge = Service.JobGauges.Get(); - -// // track mana ticks -// if (_prevMP < Player.HPMP.CurMP && !gauge.InAstralFire) -// { -// var expectedTick = Rotation.MPTick(-gauge.UmbralIceStacks); -// if (Player.HPMP.CurMP - _prevMP == expectedTick) -// { -// _lastManaTick = Autorot.WorldState.CurrentTime; -// } -// } -// _prevMP = Player.HPMP.CurMP; -// _state.TimeToManaTick = 3 - (_lastManaTick != default ? (float)(Autorot.WorldState.CurrentTime - _lastManaTick).TotalSeconds % 3 : 0); - -// _state.ElementalLevel = gauge.InAstralFire ? gauge.AstralFireStacks : -gauge.UmbralIceStacks; -// _state.ElementalLeft = gauge.ElementTimeRemaining * 0.001f; - -// _state.SwiftcastLeft = StatusDetails(Player, SID.Swiftcast, Player.InstanceID).Left; -// _state.ThundercloudLeft = StatusDetails(Player, SID.Thundercloud, Player.InstanceID).Left; -// _state.FirestarterLeft = StatusDetails(Player, SID.Firestarter, Player.InstanceID).Left; - -// _state.TargetThunderLeft = Math.Max(StatusDetails(Autorot.PrimaryTarget, _state.ExpectedThunder3, Player.InstanceID).Left, StatusDetails(Autorot.PrimaryTarget, SID.Thunder2, Player.InstanceID).Left); -// } - -// private void OnConfigModified(BLMConfig config) -// { -// // placeholders -// SupportedSpell(AID.Blizzard1).PlaceholderForAuto = config.FullRotation ? AutoActionST : AutoActionNone; -// SupportedSpell(AID.Blizzard2).PlaceholderForAuto = config.FullRotation ? AutoActionAOE : AutoActionNone; - -// // smart targets -// SupportedSpell(AID.AetherialManipulation).TransformTarget = config.MouseoverFriendly ? SmartTargetFriendly : null; -// } - -// private int NumTargetsHitByAOE(Actor primary) => Autorot.Hints.NumPriorityTargetsInAOECircle(primary.Position, 5); -//} diff --git a/BossMod/AutorotationLegacy/BLM/BLMRotation.cs b/BossMod/AutorotationLegacy/BLM/BLMRotation.cs deleted file mode 100644 index 520c7617f4..0000000000 --- a/BossMod/AutorotationLegacy/BLM/BLMRotation.cs +++ /dev/null @@ -1,244 +0,0 @@ -//namespace BossMod.BLM; - -//public static class Rotation -//{ -// // full state needed for determining next action -// public class State(WorldState ws) : CommonRotation.PlayerState(ws) -// { -// public float TimeToManaTick; // we assume mana tick happens every 3s -// public int ElementalLevel; // -3 (umbral ice 3) to +3 (astral fire 3) -// public float ElementalLeft; // 0 if elemental level is 0, otherwise buff duration, max 15 -// public float SwiftcastLeft; // 0 if buff not up, max 10 -// public float ThundercloudLeft; -// public float FirestarterLeft; -// public float TargetThunderLeft; // TODO: this shouldn't be here... - -// // upgrade paths -// public AID BestThunder3 => Unlocked(AID.Thunder3) ? AID.Thunder3 : AID.Thunder1; - -// // statuses -// public SID ExpectedThunder3 => Unlocked(AID.Thunder3) ? SID.Thunder3 : SID.Thunder1; - -// public bool Unlocked(AID aid) => Definitions.Unlocked(aid, Level, UnlockProgress); -// public bool Unlocked(TraitID tid) => Definitions.Unlocked(tid, Level, UnlockProgress); - -// public override string ToString() -// { -// return $"MP={CurMP} (tick={TimeToManaTick:f1}), RB={RaidBuffsLeft:f1}, Elem={ElementalLevel}/{ElementalLeft:f1}, Thunder={TargetThunderLeft:f1}, TC={ThundercloudLeft:f1}, FS={FirestarterLeft:f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}/{UnlockProgress}"; -// } -// } - -// // strategy configuration -// public class Strategy : CommonRotation.Strategy -// { -// public int NumAOETargets; - -// public override string ToString() -// { -// return $"AOE={NumAOETargets}, no-dots={ForbidDOTs}, movement-in={ForceMovementIn:f3}"; -// } -// } - -// public static bool CanCast(State state, Strategy strategy, float castTime) => state.SwiftcastLeft > state.GCD || strategy.ForceMovementIn >= state.GCD + castTime; - -// public static uint AdjustedFireCost(State state, uint baseCost) -// { -// return state.ElementalLevel switch -// { -// > 0 => baseCost * 2, -// < 0 => 0, -// _ => baseCost -// }; -// } - -// public static int MPTick(int elementalLevel) -// { -// return elementalLevel switch -// { -// -3 => 6200, -// -2 => 4700, -// -1 => 3200, -// 0 => 200, -// _ => 0 -// }; -// } - -// public static AID GetNextBestGCD(State state, Strategy strategy) -// { -// if (state.Unlocked(AID.Blizzard3)) -// { -// // starting from L35, fire/blizzard 2/3 automatically grant 3 fire/ice stacks, so we use them to swap between stances -// if (strategy.NumAOETargets >= 3) -// { -// // TODO: revise at L58+ -// if (state.ElementalLevel > 0) -// { -// // fire phase: F2 until oom > Flare > B2 -// bool flareUnlocked = state.Unlocked(AID.Flare); -// if (CanCast(state, strategy, flareUnlocked ? 4 : 3)) // TODO: flare is 4s cast time, B2 is 1.5s if at 3 stacks, other spells are 3s -// { -// if (flareUnlocked) -// return state.CurMP > AdjustedFireCost(state, 1500) ? AID.Fire2 : state.CurMP > 0 ? AID.Flare : AID.Blizzard2; -// else -// return state.CurMP >= AdjustedFireCost(state, 1500) ? AID.Fire2 : AID.Blizzard2; -// } -// if (!strategy.ForbidDOTs && state.ThundercloudLeft > state.GCD) -// return AID.Thunder2; -// return AID.None; // chill... -// } -// else if (state.ElementalLevel < 0) -// { -// // ice phase: Freeze/B2 if needed for mana tick > T2 if needed to refresh > F2 -// if (!strategy.ForbidDOTs && state.TargetThunderLeft <= state.GCD && (state.ThundercloudLeft > state.GCD || CanCast(state, strategy, 2.5f))) -// return AID.Thunder2; // if thunder is about to fall off, refresh before B2 - -// bool wantThunder = !strategy.ForbidDOTs && state.TargetThunderLeft < 10; // TODO: better threshold -// if (CanCast(state, strategy, 3)) -// { -// float minTimeToSwap = state.GCD + (wantThunder ? 2.5f : 0) + 1; // F2 is always hardcasted, time is 3 / 2 = 1.5 minus ~0.5 slidecast -// var mpTicksAtMinSwap = (int)((3 - state.TimeToManaTick + minTimeToSwap) / 3); -// var mpAtMinSwap = state.CurMP + mpTicksAtMinSwap * MPTick(state.ElementalLevel); -// if (mpAtMinSwap < 9600) -// return state.Unlocked(AID.Freeze) ? AID.Freeze : AID.Blizzard2; -// } -// if (!strategy.ForbidDOTs && state.ThundercloudLeft > state.GCD || wantThunder && CanCast(state, strategy, 2.5f)) -// return AID.Thunder2; -// if (state.CurMP >= 9600 && CanCast(state, strategy, state.ElementalLevel == -3 ? 1.5f : 3)) -// return AID.Fire2; -// return AID.None; // chill... -// } -// else -// { -// // opener or just dropped elemental for some reason - just F3 -// // TODO: should we open with dot instead?.. -// if (CanCast(state, strategy, 2.5f)) -// return state.CurMP >= 9600 ? AID.Fire2 : AID.Blizzard2; -// return AID.None; // chill?.. -// } -// } -// else -// { -// // TODO: revise at L58+ -// if (state.ElementalLevel > 0) -// { -// // fire phase: F3P > F1 until oom > B3 -// // TODO: Tx[P] now? or delay until ice phase? -// if (state.FirestarterLeft > state.GCD) -// return AID.Fire3; -// if (CanCast(state, strategy, 2.5f)) // TODO: B3 is 3.5, but since we typically have 3 fire stacks, it's actually 1.75 -// return state.CurMP >= AdjustedFireCost(state, 800) ? AID.Fire1 : AID.Blizzard3; -// // TODO: scathe on the move?.. -// if (!strategy.ForbidDOTs && state.ThundercloudLeft > state.GCD) -// return state.BestThunder3; -// return AID.None; // chill... -// } -// else if (state.ElementalLevel < 0) -// { -// // ice phase: B1 if needed for mana tick > T1/T3 if needed to refresh > F3 -// if (!strategy.ForbidDOTs && state.TargetThunderLeft <= state.GCD && (state.ThundercloudLeft > state.GCD || CanCast(state, strategy, 2.5f))) -// return state.BestThunder3; // if thunder is about to fall off, refresh before B1 - -// bool wantThunder = !strategy.ForbidDOTs && state.TargetThunderLeft < 10; // TODO: better threshold -// if (CanCast(state, strategy, 2.5f)) -// { -// float minTimeToSwap = state.GCD + (wantThunder ? 2.5f : 0); -// if (state.FirestarterLeft < minTimeToSwap) -// minTimeToSwap += 1.2f; // when hardcasting F3, swap happens around slidecast start; cast time for F3 is 3.5 / 2 = 1.75, action effect happens ~0.5s before cast end -// var mpTicksAtMinSwap = (int)((3 - state.TimeToManaTick + minTimeToSwap) / 3); -// var mpAtMinSwap = state.CurMP + mpTicksAtMinSwap * MPTick(state.ElementalLevel); -// if (mpAtMinSwap < 9800) -// return AID.Blizzard1; -// } -// if (!strategy.ForbidDOTs && state.ThundercloudLeft > state.GCD || wantThunder && CanCast(state, strategy, 2.5f)) -// return state.BestThunder3; -// if (state.CurMP >= 9800 && (state.FirestarterLeft > state.GCD || CanCast(state, strategy, state.ElementalLeft == -3 ? 1.75f : 3.5f))) -// return AID.Fire3; -// return AID.None; // chill... -// } -// else -// { -// // opener or just dropped elemental for some reason - just F3 -// // TODO: should we open with dot instead?.. -// if (state.CurMP >= 9800 && (state.FirestarterLeft > state.GCD || CanCast(state, strategy, 3.5f))) -// return AID.Fire3; -// else if (CanCast(state, strategy, 3.5f)) -// return AID.Blizzard3; -// return AID.None; // chill?.. -// } -// } -// } -// else -// { -// // before L35, fire/blizzard 1/2 cast under wrong element reset stance - so we use transpose rotation -// if (!CanCast(state, strategy, 3)) // TODO: B2/F2 are 3s, B1/F1 are 2.5s -// { -// // TODO: this is not really correct, we could have thundercloud, but w/e... -// if (state.Unlocked(AID.Scathe) && state.CurMP >= 800) -// return AID.Scathe; -// } -// else if (strategy.NumAOETargets >= 3 && state.Unlocked(AID.Blizzard2)) -// { -// if (!strategy.ForbidDOTs && state.Unlocked(AID.Thunder2) && state.TargetThunderLeft <= state.GCD) -// return AID.Thunder2; -// return state.Unlocked(AID.Fire2) ? TransposeRotationGCD(state, AID.Blizzard2, 800, AID.Fire2, 1500) : AID.Blizzard2; -// } -// else -// { -// if (!strategy.ForbidDOTs && state.Unlocked(AID.Thunder1) && state.TargetThunderLeft <= state.GCD) -// return AID.Thunder1; -// return state.Unlocked(AID.Fire1) ? TransposeRotationGCD(state, AID.Blizzard1, 400, AID.Fire1, 800) : AID.Blizzard1; -// } -// } -// return AID.None; -// } - -// public static ActionID GetNextBestOGCD(State state, Strategy strategy, float deadline) -// { -// if (state.Unlocked(AID.Blizzard3)) -// { -// // L35-Lxx: weave manafont in free slots (TODO: is that right?..) -// if (deadline >= 10000 && strategy.ForceMovementIn < 5 && state.Unlocked(AID.Swiftcast) && state.CanWeave(CDGroup.Swiftcast, 0.6f, deadline)) // TODO: better swiftcast condition... -// return ActionID.MakeSpell(AID.Swiftcast); -// if (state.Unlocked(AID.Manafont) && state.CanWeave(CDGroup.Manafont, 0.6f, deadline) && state.CurMP <= 7000) -// return ActionID.MakeSpell(AID.Manafont); -// } -// else -// { -// // before L35, use transpose to swap between elemental states -// // MP thresholds are not especially meaningful (they should work for both ST and AOE), who cares about low level... -// // we could also use manafont, but again who cares -// if (state.Unlocked(AID.Transpose) && state.CanWeave(CDGroup.Transpose, 0.6f, deadline) && (state.ElementalLevel < 0 && state.CurMP >= 9200 || state.ElementalLevel > 0 && state.CurMP < 3600)) -// return ActionID.MakeSpell(AID.Transpose); - -// // TODO: swiftcast if moving... -// } - -// return new(); -// } - -// private static AID TransposeRotationGCD(State state, AID iceSpell, int iceCost, AID fireSpell, int fireCost) -// { -// if (state.ElementalLevel < 0) -// { -// // continue blizzard1 spam until full mana, then swap to fire -// // TODO: take mana ticks into account, if it happens before GCD -// if (state.CurMP < 10000 - iceCost) -// return iceSpell; -// else -// return state.Unlocked(AID.Transpose) ? AID.None : fireSpell; -// } -// else if (state.ElementalLevel > 0) -// { -// // continue fire1 spam until oom, then swap to ice -// if (state.CurMP >= fireCost * 2 + iceCost * 3 / 4) -// return fireSpell; -// else -// return state.Unlocked(AID.Transpose) ? AID.None : iceSpell; -// } -// else -// { -// // dropped buff => fire if have some mana (TODO: better limit), blizzard otherwise -// return state.CurMP < fireCost * 3 + iceCost * 3 / 4 ? iceSpell : fireSpell; -// } -// } -//} diff --git a/BossMod/AutorotationLegacy/HealerActions.cs b/BossMod/AutorotationLegacy/HealerActions.cs deleted file mode 100644 index 40bd281e98..0000000000 --- a/BossMod/AutorotationLegacy/HealerActions.cs +++ /dev/null @@ -1,234 +0,0 @@ -//using Dalamud.Game.ClientState.Conditions; - -//namespace BossMod; - -//// extra utilities for healers -//abstract class HealerActions(AutorotationLegacy autorot, Actor player, uint[] unlockData) : CommonActions(autorot, player, unlockData) -//{ -// public struct PartyMemberState -// { -// public int PredictedHPCur; -// public int PredictedHPDeficit; -// public float AttackerStrength; // 0.05 per attacker by default -// public float PredictedHPRatio; // reduced by sum of attacker strengths, unless at full hp; increased by active hots and incoming heals from other healers -// public bool HaveRemovableDebuffs; -// } - -// protected bool AllowProtect { get; private set; } -// protected readonly PartyMemberState[] PartyMemberStates = new PartyMemberState[PartyState.MaxPartySize]; - -// protected override void UpdateInternalState(int autoAction) -// { -// AllowProtect = Service.Condition[ConditionFlag.BoundByDuty]; // TODO: validate... - -// BitMask incomingEsunas = new(); -// foreach (var esunaCaster in Autorot.WorldState.Party.WithoutSlot().Where(a => a.CastInfo?.IsSpell(WHM.AID.Esuna) ?? false)) -// incomingEsunas.Set(Autorot.WorldState.Party.FindSlot(esunaCaster.CastInfo!.TargetID)); - -// for (int i = 0; i < PartyMemberStates.Length; ++i) -// { -// var actor = Autorot.WorldState.Party[i]; -// ref var state = ref PartyMemberStates[i]; -// state.HaveRemovableDebuffs = false; -// if (actor == null || actor.IsDead || actor.HPMP.MaxHP == 0) -// { -// state.PredictedHPCur = state.PredictedHPDeficit = 0; -// state.PredictedHPRatio = 1; -// } -// else -// { -// state.PredictedHPCur = (int)actor.HPMP.CurHP + Autorot.WorldState.PendingEffects.PendingHPDifference(actor.InstanceID); -// state.PredictedHPDeficit = (int)actor.HPMP.MaxHP - state.PredictedHPCur; -// state.PredictedHPRatio = (float)state.PredictedHPCur / actor.HPMP.MaxHP; -// bool actorValidForEsuna = actor.IsTargetable && !incomingEsunas[i]; -// foreach (var s in actor.Statuses) -// { -// if (!state.HaveRemovableDebuffs && actorValidForEsuna && Utils.StatusIsRemovable(s.ID)) -// state.HaveRemovableDebuffs = true; -// if (IsHOT(s.ID)) -// state.PredictedHPRatio += 0.1f; -// } -// } -// state.AttackerStrength = 0; -// } -// foreach (var incomingHealTargets in Autorot.WorldState.Party.WithoutSlot().Select(a => CastedHealTargets(Autorot.WorldState, a))) -// { -// foreach (var slot in incomingHealTargets.SetBits()) -// { -// if (slot < PartyMemberStates.Length) -// { -// PartyMemberStates[slot].PredictedHPRatio += 0.2f; -// } -// } -// } -// foreach (var enemy in Autorot.Hints.PotentialTargets) -// { -// var targetSlot = Autorot.WorldState.Party.FindSlot(enemy.Actor.TargetID); -// if (targetSlot >= 0 && targetSlot < PartyMemberStates.Length) -// { -// ref var state = ref PartyMemberStates[targetSlot]; -// state.AttackerStrength += enemy.AttackStrength; -// if (state.PredictedHPRatio < 0.99f) -// state.PredictedHPRatio -= enemy.AttackStrength; -// } -// } -// } - -// // count alive players with hp ratio < threshold -// protected int CountAOEHealTargets(float radius, WPos center, float hpThreshold = 0.9f) -// { -// int res = 0; -// for (int i = 0; i < PartyMemberStates.Length; ++i) -// { -// var actor = Autorot.WorldState.Party[i]; -// if (PartyMemberStates[i].PredictedHPRatio < hpThreshold && actor != null && !actor.IsDead && actor.Position.InCircle(center, radius)) -// { -// ++res; -// } -// } -// return res; -// } - -// // count alive players with hp ratio < threshold -or- that are valid preshield targets (have no specified status and have predicted incoming damage in specified time) -// protected int CountAOEPreshieldTargets(float radius, WPos center, uint shieldSID, float minTime, float maxTime, float hpThreshold = 0.9f) -// { -// int res = 0; -// for (int i = 0; i < PartyMemberStates.Length; ++i) -// { -// var actor = Autorot.WorldState.Party[i]; -// if (actor != null && !actor.IsDead && actor.Position.InCircle(center, radius)) -// { -// bool valid = PartyMemberStates[i].PredictedHPRatio < hpThreshold; -// if (!valid) -// { -// var shield = StatusDetails(actor, shieldSID, Autorot.WorldState.Party.Player()?.InstanceID ?? 0).Left; -// if (shield <= minTime) -// { -// foreach (var e in Autorot.Hints.PredictedDamage.Where(e => e.players[i])) -// { -// var time = MathF.Max(0, (float)(e.activation - Autorot.WorldState.CurrentTime).TotalSeconds); -// if (time >= minTime && time <= maxTime) -// { -// valid = true; -// break; -// } -// } -// } -// } - -// if (valid) -// ++res; -// } -// } -// return res; -// } - -// // find best target for single-target heals; return current predicted HP ratio -// protected (Actor? Target, float HPRatio) FindBestSTHealTarget(float hpThreshold = 0.5f) -// { -// Actor? best = null; -// float bestHPRatio = hpThreshold; -// for (int i = 0; i < PartyMemberStates.Length; ++i) -// { -// var actor = Autorot.WorldState.Party[i]; -// if (PartyMemberStates[i].PredictedHPRatio < bestHPRatio && actor != null && !actor.IsDead) -// { -// best = actor; -// } -// } -// return (best, bestHPRatio); -// } - -//// check whether given actor has tank stance -//protected static bool HasTankStance(Actor a) -//{ -// var stanceSID = a.Class switch -// { -// Class.WAR => (uint)WAR.SID.Defiance, -// Class.PLD => (uint)PLD.SID.IronWill, -// Class.GNB => (uint)GNB.SID.RoyalGuard, -// _ => 0u -// }; -// return stanceSID != 0 && a.FindStatus(stanceSID) != null; -//} - -// // find best target for regen/preshield -// protected Actor? FindProtectTarget(float minAttackerStrengthInCombat = 0.15f) -// { -// if (!AllowProtect) -// return null; -// Actor? best = null; -// float bestScore = 0; // 1 if out of combat and has stance, 2+X if have more attacker strength than threshold -// for (int i = 0; i < PartyMemberStates.Length; ++i) -// { -// var actor = Autorot.WorldState.Party[i]; -// if (actor == null || actor.IsDead || actor.Role != Role.Tank) -// continue; // consider only alive tanks - -// float curScore = 0; -// var strength = PartyMemberStates[i].AttackerStrength; -// if (strength >= minAttackerStrengthInCombat) -// { -// curScore = 2 + strength - minAttackerStrengthInCombat; -// } -// else if (strength == 0 && !actor.InCombat && HasTankStance(actor)) -// { -// curScore = 1; -// } - -// if (curScore > bestScore) -// { -// best = actor; -// bestScore = curScore; -// } -// } -// return best; -// } - -// protected static bool IsHOT(uint sid) -// { -// return (WHM.SID)sid is WHM.SID.Medica2 or WHM.SID.Asylum or WHM.SID.Regen; -// } - -// protected static BitMask CastedHealTargets(WorldState ws, Actor a) -// { -// BitMask res = new(); -// if (a.CastInfo == null || a.Role != Role.Healer || !a.CastInfo.IsSpell()) -// return res; -// switch (a.CastInfo.Action.ID) -// { -// case (uint)WHM.AID.Cure1: -// case (uint)WHM.AID.Cure2: -// case (uint)SCH.AID.Physick: -// case (uint)SCH.AID.Adloquium: -// res.Set(ws.Party.FindSlot(a.CastInfo.TargetID)); -// break; -// case (uint)WHM.AID.Medica1: -// case (uint)SCH.AID.Succor: -// res = ws.Party.WithSlot().InRadius(a.Position, 15).Mask(); -// break; -// case (uint)WHM.AID.Medica2: -// res = ws.Party.WithSlot().InRadius(a.Position, 20).Mask(); -// break; -// case (uint)WHM.AID.Cure3: -// res = ws.Party.WithSlot().InRadius((ws.Actors.Find(a.CastInfo.TargetID) ?? a).Position, 20).Mask(); -// break; -// } -// return res; -// } - -// // find target for esuna, if available -// protected Actor? FindEsunaTarget() -// { -// for (int i = 0; i < PartyMemberStates.Length; ++i) -// { -// if (PartyMemberStates[i].HaveRemovableDebuffs) -// { -// var actor = Autorot.WorldState.Party[i]; -// if (actor != null && !actor.IsDead) -// return actor; -// } -// } -// return null; -// } -//} diff --git a/BossMod/AutorotationLegacy/PLD/PLDActions.cs b/BossMod/AutorotationLegacy/PLD/PLDActions.cs deleted file mode 100644 index eb45cd0817..0000000000 --- a/BossMod/AutorotationLegacy/PLD/PLDActions.cs +++ /dev/null @@ -1,115 +0,0 @@ -//namespace BossMod.PLD; - -//class Actions : TankActions -//{ -// public const int AutoActionST = AutoActionFirstCustom + 0; -// public const int AutoActionAOE = AutoActionFirstCustom + 1; - -// private bool _aoe; -// private readonly Rotation.State _state; -// private readonly Rotation.Strategy _strategy; -// private readonly ConfigListener _config; - -// public Actions(Autorotation autorot, Actor player) : base(autorot, player, Definitions.UnlockQuests) -// { -// _state = new(autorot.WorldState); -// _strategy = new(); - -// SupportedSpell(AID.Reprisal).Condition = _ => Autorot.Hints.PotentialTargets.Any(e => e.Actor.Position.InCircle(Player.Position, 5 + e.Actor.HitboxRadius)); // TODO: consider checking only target?.. -// SupportedSpell(AID.Interject).Condition = target => target?.CastInfo?.Interruptible ?? false; - -// _config = Service.Config.GetAndSubscribe(OnConfigModified); -// } - -// protected override void Dispose(bool disposing) -// { -// _config.Dispose(); -// base.Dispose(disposing); -// } - -// public override CommonRotation.PlayerState GetState() => _state; -// public override CommonRotation.Strategy GetStrategy() => _strategy; - -// protected override void UpdateInternalState(int autoAction) -// { -// base.UpdateInternalState(autoAction); -// _aoe = autoAction switch -// { -// AutoActionST => false, -// AutoActionAOE => true, // TODO: consider making AI-like check -// AutoActionAIFight => NumTargetsHitByAOE() >= 3, -// _ => false, // irrelevant... -// }; -// UpdatePlayerState(); -// FillCommonStrategy(_strategy, ActionDefinitions.IDPotionStr); -// } - -// protected override void QueueAIActions() -// { -// if (_state.Unlocked(AID.Interject)) -// { -// var interruptibleEnemy = Autorot.Hints.PotentialTargets.Find(e => e.ShouldBeInterrupted && (e.Actor.CastInfo?.Interruptible ?? false) && e.Actor.Position.InCircle(Player.Position, 3 + e.Actor.HitboxRadius + Player.HitboxRadius)); -// SimulateManualActionForAI(ActionID.MakeSpell(AID.Interject), interruptibleEnemy?.Actor, interruptibleEnemy != null); -// } -// if (_state.Unlocked(AID.IronWill)) -// SimulateManualActionForAI(ActionID.MakeSpell(AID.IronWill), Player, ShouldSwapStance()); -// if (_state.Unlocked(AID.Provoke)) -// { -// var provokeEnemy = Autorot.Hints.PotentialTargets.Find(e => e.ShouldBeTanked && e.PreferProvoking && e.Actor.TargetID != Player.InstanceID && e.Actor.Position.InCircle(Player.Position, 25 + e.Actor.HitboxRadius + Player.HitboxRadius)); -// SimulateManualActionForAI(ActionID.MakeSpell(AID.Provoke), provokeEnemy?.Actor, provokeEnemy != null); -// } -// } - -// protected override ActionQueue.Entry CalculateAutomaticGCD() -// { -// if (Autorot.PrimaryTarget == null || AutoAction < AutoActionAIFight) -// return default; -// if (AutoAction == AutoActionAIFight && !Autorot.PrimaryTarget.Position.InCircle(Player.Position, 3 + Autorot.PrimaryTarget.HitboxRadius + Player.HitboxRadius) && _state.Unlocked(AID.ShieldLob)) -// return MakeResult(AID.ShieldLob, Autorot.PrimaryTarget); // TODO: reconsider... -// var aid = Rotation.GetNextBestGCD(_state, _strategy, _aoe); -// return MakeResult(aid, Autorot.PrimaryTarget); -// } - -// protected override ActionQueue.Entry CalculateAutomaticOGCD(float deadline) -// { -// if (Autorot.PrimaryTarget == null || AutoAction < AutoActionAIFight) -// return default; - -// ActionID res = new(); -// if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot -// res = Rotation.GetNextBestOGCD(_state, _strategy, deadline - _state.OGCDSlotLength, _aoe); -// if (!res && _state.CanWeave(deadline)) // second/only ogcd slot -// res = Rotation.GetNextBestOGCD(_state, _strategy, deadline, _aoe); -// return MakeResult(res, Autorot.PrimaryTarget); -// } - -// private void UpdatePlayerState() -// { -// FillCommonPlayerState(_state); -// _state.HaveTankStance = Player.FindStatus(SID.IronWill) != null; - -// //s.Gauge = Service.JobGauges.Get().OathGauge; - -// _state.FightOrFlightLeft = StatusDetails(Player, SID.FightOrFlight, Player.InstanceID).Left; -// } - -// private void OnConfigModified(PLDConfig config) -// { -// // placeholders -// SupportedSpell(AID.FastBlade).PlaceholderForAuto = config.FullRotation ? AutoActionST : AutoActionNone; -// SupportedSpell(AID.TotalEclipse).PlaceholderForAuto = config.FullRotation ? AutoActionAOE : AutoActionNone; - -// // combo replacement -// SupportedSpell(AID.RiotBlade).TransformAction = config.STCombos ? () => ActionID.MakeSpell(Rotation.GetNextRiotBladeComboAction(ComboLastMove)) : null; -// SupportedSpell(AID.RageOfHalone).TransformAction = config.STCombos ? () => ActionID.MakeSpell(Rotation.GetNextSTComboAction(ComboLastMove, AID.RageOfHalone)) : null; -// SupportedSpell(AID.GoringBlade).TransformAction = config.STCombos ? () => ActionID.MakeSpell(Rotation.GetNextSTComboAction(ComboLastMove, AID.GoringBlade)) : null; - -// // smart targets -// SupportedSpell(AID.Shirk).TransformTarget = config.SmartShirkTarget ? SmartTargetCoTank : null; -// SupportedSpell(AID.Provoke).TransformTarget = config.ProvokeMouseover ? SmartTargetHostile : null; // TODO: also interject/low-blow -// } - -// private AID ComboLastMove => (AID)Autorot.ActionManager.ComboLastMove; - -// private int NumTargetsHitByAOE() => Autorot.Hints.NumPriorityTargetsInAOECircle(Player.Position, 5); -//} diff --git a/BossMod/AutorotationLegacy/PLD/PLDRotation.cs b/BossMod/AutorotationLegacy/PLD/PLDRotation.cs deleted file mode 100644 index 6172f4cb75..0000000000 --- a/BossMod/AutorotationLegacy/PLD/PLDRotation.cs +++ /dev/null @@ -1,83 +0,0 @@ -//namespace BossMod.PLD; - -//// note: correct up to ~L30 -//public static class Rotation -//{ -// // full state needed for determining next action -// public class State(WorldState ws) : CommonRotation.PlayerState(ws) -// { -// public float FightOrFlightLeft; // 0 if buff not up, max 25 - -// public AID ComboLastMove => (AID)ComboLastAction; - -// public bool Unlocked(AID aid) => Definitions.Unlocked(aid, Level, UnlockProgress); -// public bool Unlocked(TraitID tid) => Definitions.Unlocked(tid, Level, UnlockProgress); - -// public override string ToString() -// { -// return $"RB={RaidBuffsLeft:f1}, FF={FightOrFlightLeft:f1}/{CD(CDGroup.FightOrFlight):f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}/{UnlockProgress}"; -// } -// } - -// // strategy configuration -// public class Strategy : CommonRotation.Strategy -// { -// public override string ToString() -// { -// return $""; -// } -// } - -// public static AID GetNextRiotBladeComboAction(AID comboLastMove) -// { -// return comboLastMove == AID.FastBlade ? AID.RiotBlade : AID.FastBlade; -// } - -// public static AID GetNextSTComboAction(AID comboLastMove, AID finisher) -// { -// return comboLastMove switch -// { -// AID.RiotBlade => finisher, -// AID.FastBlade => AID.RiotBlade, -// _ => AID.FastBlade -// }; -// } - -// public static AID GetNextBestGCD(State state, Strategy strategy, bool aoe) -// { -// if (aoe) -// { -// if (state.Unlocked(AID.Prominence) && state.ComboLastMove == AID.TotalEclipse) -// return AID.Prominence; -// else -// return AID.TotalEclipse; -// } -// else -// { -// if (state.Unlocked(AID.RageOfHalone) && state.ComboLastMove == AID.RiotBlade) -// return AID.RageOfHalone; -// else if (state.Unlocked(AID.RiotBlade) && state.ComboLastMove == AID.FastBlade) -// return AID.RiotBlade; -// else -// return AID.FastBlade; -// } -// } - -// public static ActionID GetNextBestOGCD(State state, Strategy strategy, float deadline, bool aoe) -// { -// // 1. potion - TODO - -// // 2. fight or flight, if off gcd and late-weaving, after first combo action -// if (state.Unlocked(AID.FightOrFlight) && state.ComboLastMove == (aoe ? AID.TotalEclipse : AID.FastBlade) && state.CanWeave(CDGroup.FightOrFlight, 0.6f, deadline) && state.GCD <= 1.0f) -// return ActionID.MakeSpell(AID.FightOrFlight); - -// // 3. spirits within/circle of scorn, delayed until FoF if it's about to be off cooldown (TODO: think more about delay condition...) -// if (state.Unlocked(AID.SpiritsWithin) && state.CanWeave(CDGroup.SpiritsWithin, 0.6f, deadline) && (state.FightOrFlightLeft > 0 || state.CD(CDGroup.FightOrFlight) > 15)) -// return ActionID.MakeSpell(AID.SpiritsWithin); -// if (state.Unlocked(AID.CircleOfScorn) && state.CanWeave(CDGroup.CircleOfScorn, 0.6f, deadline) && (state.FightOrFlightLeft > 0 || state.CD(CDGroup.FightOrFlight) > 15)) -// return ActionID.MakeSpell(AID.CircleOfScorn); - -// // no suitable oGCDs... -// return new(); -// } -//} diff --git a/BossMod/AutorotationLegacy/SCH/SCHActions.cs b/BossMod/AutorotationLegacy/SCH/SCHActions.cs deleted file mode 100644 index 47f65f4a7b..0000000000 --- a/BossMod/AutorotationLegacy/SCH/SCHActions.cs +++ /dev/null @@ -1,186 +0,0 @@ -//namespace BossMod.SCH; - -//// TODO: this is shit, like all healer modules... -//class Actions : HealerActions -//{ -// private readonly Rotation.State _state; -// private readonly Rotation.Strategy _strategy; -// private bool _allowDelayingNextGCD; -// private readonly ConfigListener _config; - -// public Actions(Autorotation autorot, Actor player) : base(autorot, player, Definitions.UnlockQuests) -// { -// _state = new(autorot.WorldState); -// _strategy = new(); - -// // upgrades -// SupportedSpell(AID.Ruin1).TransformAction = SupportedSpell(AID.Broil1).TransformAction = SupportedSpell(AID.Broil2).TransformAction = SupportedSpell(AID.Broil3).TransformAction = SupportedSpell(AID.Broil4).TransformAction = () => ActionID.MakeSpell(_state.BestBroil); -// SupportedSpell(AID.Bio1).TransformAction = SupportedSpell(AID.Bio2).TransformAction = SupportedSpell(AID.Biolysis).TransformAction = () => ActionID.MakeSpell(_state.BestBio); -// SupportedSpell(AID.ArtOfWar1).TransformAction = SupportedSpell(AID.ArtOfWar2).TransformAction = () => ActionID.MakeSpell(_state.BestArtOfWar); - -// _config = Service.Config.GetAndSubscribe(OnConfigModified); -// } - -// protected override void Dispose(bool disposing) -// { -// _config.Dispose(); -// base.Dispose(disposing); -// } - -// public override CommonRotation.PlayerState GetState() => _state; -// public override CommonRotation.Strategy GetStrategy() => _strategy; - -// public override Targeting SelectBetterTarget(AIHints.Enemy initial) -// { -// // TODO: look for good place to cast art of war and move closer... - -// // look for target to multidot, if initial target already has dot -// if (_state.Unlocked(AID.Bio1) && !WithoutDOT(initial.Actor)) -// { -// var multidotTarget = Autorot.Hints.PriorityTargets.FirstOrDefault(t => t != initial && !t.ForbidDOTs && t.Actor.Position.InCircle(Player.Position, 25) && WithoutDOT(t.Actor)); -// if (multidotTarget != null) -// return new(multidotTarget, multidotTarget.StayAtLongRange ? 25 : 10); -// } - -// return new(initial, initial.StayAtLongRange ? 25 : 10); -// } - -// protected override void UpdateInternalState(int autoAction) -// { -// base.UpdateInternalState(autoAction); -// UpdatePlayerState(); -// FillCommonStrategy(_strategy, ActionDefinitions.IDPotionMnd); -// _strategy.NumWhisperingDawnTargets = _state.Fairy != null && _state.Unlocked(AID.WhisperingDawn) ? CountAOEHealTargets(15, _state.Fairy.Position) : 0; -// _strategy.NumSuccorTargets = _state.Unlocked(AID.Succor) ? CountAOEPreshieldTargets(15, Player.Position, (uint)SID.Galvanize, _state.GCD + 2, 10) : 0; -// _strategy.NumArtOfWarTargets = _state.Unlocked(AID.ArtOfWar1) ? Autorot.Hints.NumPriorityTargetsInAOECircle(Player.Position, 5) : 0; -// _strategy.BestSTHeal = FindBestSTHealTarget(); -// } - -// protected override void QueueAIActions() -// { -// } - -// protected override ActionQueue.Entry CalculateAutomaticGCD() -// { -// // TODO: rework, implement non-ai... -// if (_state.Unlocked(AID.SummonEos) && _state.Fairy == null) -// return MakeResult(AID.SummonEos, Player); - -// // AI: aoe heals > st heals > esuna > damage -// // i don't really think 'rotation/actions' split is particularly good fit for healers AI... -// // TODO: raise support... -// if (Rotation.CanCast(_state, _strategy, 2) && _strategy.NumSuccorTargets > 2 && _state.CurMP >= 1000) -// return MakeResult(AID.Succor, Player); - -// // now check ST heal -// if (Rotation.CanCast(_state, _strategy, 2) && _strategy.BestSTHeal.Target != null && _state.AetherflowStacks == 0 && StatusDetails(_strategy.BestSTHeal.Target, SID.Galvanize, Player.InstanceID).Left <= _state.GCD) -// return MakeResult(Rotation.GetNextBestSTHealGCD(_state, _strategy), _strategy.BestSTHeal.Target); - -// // now check esuna -// if (Rotation.CanCast(_state, _strategy, 1)) -// { -// var esunaTarget = FindEsunaTarget(); -// if (esunaTarget != null) -// return MakeResult(AID.Esuna, esunaTarget); -// } - -// // prepull adlo, if allowed -// var preshieldTarget = _state.Unlocked(AID.Adloquium) ? FindProtectTarget(0.5f) : null; -// if (preshieldTarget != null && Rotation.CanCast(_state, _strategy, 2) && StatusDetails(preshieldTarget, SID.Galvanize, Player.InstanceID).Left <= _state.GCD) -// return MakeResult(AID.Adloquium, preshieldTarget); - -// // finally perform damage rotation -// if (_state.CurMP > 3000 && Autorot.PrimaryTarget != null) -// return MakeResult(Rotation.GetNextBestDamageGCD(_state, _strategy), Autorot.PrimaryTarget); - -// return default; // chill -// } - -// protected override ActionQueue.Entry CalculateAutomaticOGCD(float deadline) -// { -// if (AutoAction < AutoActionAIFight) -// return default; - -// if (deadline < float.MaxValue && _allowDelayingNextGCD) -// deadline += 0.4f + _state.AnimationLockDelay; - -// ActionQueue.Entry res = default; -// if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot -// res = GetNextBestOGCD(deadline - _state.OGCDSlotLength); -// if (!res.Action && _state.CanWeave(deadline)) // second/only ogcd slot -// res = GetNextBestOGCD(deadline); -// return res; -// } - -// private ActionQueue.Entry GetNextBestOGCD(float deadline) -// { -// // TODO: L52+ -// // TODO: fey illumination - -// // whispering dawn, if it hits 3+ targets or hits st heal target -// if (_strategy.NumWhisperingDawnTargets > 0 && _state.CanWeave(CDGroup.WhisperingDawn, 0.6f, deadline)) -// { -// // TODO: better whispering dawn condition... -// if (_strategy.NumWhisperingDawnTargets > 2 || _strategy.BestSTHeal.Target != null && (_strategy.BestSTHeal.Target.Position - _state.Fairy!.Position).LengthSq() <= 15 * 15) -// return MakeResult(AID.WhisperingDawn, Player); -// } - -// // aetherflow, if no stacks left -// if (_state.AetherflowStacks == 0 && _state.Unlocked(AID.Aetherflow) && _state.CanWeave(CDGroup.Aetherflow, 0.6f, deadline)) -// return MakeResult(AID.Aetherflow, Player); - -// // lustrate, if want single-target heals -// if (_strategy.BestSTHeal.Target != null && _state.AetherflowStacks > 0 && _state.Unlocked(AID.Lustrate) && _state.CanWeave(CDGroup.Lustrate, 0.6f, deadline)) -// return MakeResult(AID.Lustrate, _strategy.BestSTHeal.Target); - -// // energy drain, if new aetherflow will come off cd soon (TODO: reconsider...) -// if (Autorot.PrimaryTarget != null && _state.AetherflowStacks > 0 && _state.CD(CDGroup.Aetherflow) <= _state.GCD + _state.AetherflowStacks * 2.5f && _state.Unlocked(AID.EnergyDrain) && _state.CanWeave(CDGroup.EnergyDrain, 0.6f, deadline)) -// return MakeResult(AID.EnergyDrain, Autorot.PrimaryTarget); - -// // swiftcast, if can't cast any gcd (TODO: current check is not very good...) -// if (deadline >= 10000 && _strategy.ForceMovementIn < 5 && _state.Unlocked(AID.Swiftcast) && _state.CanWeave(CDGroup.Swiftcast, 0.6f, deadline)) -// return MakeResult(AID.Swiftcast, Player); - -// // lucid dreaming, if we won't waste mana (TODO: revise mp limit) -// if (_state.CurMP <= 7000 && _state.Unlocked(AID.LucidDreaming) && _state.CanWeave(CDGroup.LucidDreaming, 0.6f, deadline)) -// return MakeResult(AID.LucidDreaming, Player); - -// return default; -// } - -// protected override void OnActionSucceeded(ActorCastEvent ev) -// { -// // note: our GCD heals have cast time 2 +0.1 anim lock => after them we have 0.4 or even slightly smaller window -// // we want to be able to cast at least one oGCD after them, even if it means slightly delaying next GCD -// _allowDelayingNextGCD = ev.Action.Type == ActionType.Spell && (AID)ev.Action.ID is AID.Adloquium or AID.Succor; -// } - -// private void UpdatePlayerState() -// { -// FillCommonPlayerState(_state); - -// if (_state.Fairy == null || _state.Fairy.IsDestroyed) -// _state.Fairy = Autorot.WorldState.Actors.FirstOrDefault(a => a.Type == ActorType.Pet && a.OwnerID == Player.InstanceID); - -// var gauge = Service.JobGauges.Get(); -// _state.AetherflowStacks = gauge.Aetherflow; - -// _state.SwiftcastLeft = StatusDetails(Player, SID.Swiftcast, Player.InstanceID).Left; -// _state.TargetBioLeft = StatusDetails(Autorot.PrimaryTarget, _state.ExpectedBio, Player.InstanceID).Left; -// } - -// private bool WithoutDOT(Actor a) => Rotation.RefreshDOT(_state, StatusDetails(a, _state.ExpectedBio, Player.InstanceID).Left); - -// private void OnConfigModified(SCHConfig config) -// { -// // placeholders -// //SupportedSpell(AID.Ruin1).PlaceholderForAuto = _config.FullRotation ? AutoActionST : AutoActionNone; -// //SupportedSpell(AID.ArtOfWar1).PlaceholderForAuto = _config.FullRotation ? AutoActionAOE : AutoActionNone; - -// // smart targets -// SupportedSpell(AID.Physick).TransformTarget = SupportedSpell(AID.Adloquium).TransformTarget = SupportedSpell(AID.Lustrate).TransformTarget -// = SupportedSpell(AID.DeploymentTactics).TransformTarget = SupportedSpell(AID.Excogitation).TransformTarget = SupportedSpell(AID.Aetherpact).TransformTarget -// = SupportedSpell(AID.Resurrection).TransformTarget = SupportedSpell(AID.Esuna).TransformTarget = SupportedSpell(AID.Rescue).TransformTarget -// = config.MouseoverFriendly ? SmartTargetFriendly : null; -// } -//} diff --git a/BossMod/AutorotationLegacy/SCH/SCHRotation.cs b/BossMod/AutorotationLegacy/SCH/SCHRotation.cs deleted file mode 100644 index 33aa5e3a8e..0000000000 --- a/BossMod/AutorotationLegacy/SCH/SCHRotation.cs +++ /dev/null @@ -1,86 +0,0 @@ -//namespace BossMod.SCH; - -//public static class Rotation -//{ -// // full state needed for determining next action -// public class State(WorldState ws) : CommonRotation.PlayerState(ws) -// { -// public Actor? Fairy; -// public int AetherflowStacks; // 3 max -// public float SwiftcastLeft; // 0 if buff not up, max 10 -// public float TargetBioLeft; // 0 if debuff not up, max 30 - -// // upgrade paths -// public AID BestBroil => Unlocked(AID.Broil4) ? AID.Broil4 : Unlocked(AID.Broil3) ? AID.Broil3 : Unlocked(AID.Broil2) ? AID.Broil2 : Unlocked(AID.Broil1) ? AID.Broil1 : AID.Ruin1; -// public AID BestBio => Unlocked(AID.Biolysis) ? AID.Biolysis : Unlocked(AID.Bio2) ? AID.Bio2 : AID.Bio1; -// public AID BestArtOfWar => Unlocked(AID.ArtOfWar2) ? AID.ArtOfWar2 : AID.ArtOfWar1; - -// // statuses -// public SID ExpectedBio => Unlocked(AID.Biolysis) ? SID.Biolysis : Unlocked(AID.Bio2) ? SID.Bio2 : SID.Bio1; - -// public bool Unlocked(AID aid) => Definitions.Unlocked(aid, Level, UnlockProgress); -// public bool Unlocked(TraitID tid) => Definitions.Unlocked(tid, Level, UnlockProgress); - -// public override string ToString() -// { -// return $"AF={AetherflowStacks}, RB={RaidBuffsLeft:f1}, Bio={TargetBioLeft:f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}/{UnlockProgress}"; -// } -// } - -// // strategy configuration -// public class Strategy : CommonRotation.Strategy -// { -// public int NumWhisperingDawnTargets; // how many targets would whispering dawn heal (15y around fairy) -// public int NumSuccorTargets; // how many targets would succor heal (15y around self) -// public int NumArtOfWarTargets; // how many targets art of war would hit (5y around self) -// public (Actor? Target, float HPRatio) BestSTHeal; - -// public override string ToString() -// { -// return $"AOE={NumArtOfWarTargets}, SH={BestSTHeal.Target?.Name[..4]}={BestSTHeal.HPRatio:f2}, AH={NumSuccorTargets}/{NumWhisperingDawnTargets}, no-dots={ForbidDOTs}, movement-in={ForceMovementIn:f3}"; -// } -// } - -// public static bool CanCast(State state, Strategy strategy, float castTime) => state.SwiftcastLeft > state.GCD || strategy.ForceMovementIn >= state.GCD + castTime; -// public static bool RefreshDOT(State state, float timeLeft) => timeLeft < state.GCD + 3.0f; // TODO: tweak threshold so that we don't overwrite or miss ticks... - -// public static AID GetNextBestSTHealGCD(State state, Strategy strategy) -// { -// return state.Unlocked(AID.Adloquium) && state.CurMP >= 1000 ? AID.Adloquium : AID.Physick; -// } - -// public static AID GetNextBestDamageGCD(State state, Strategy strategy) -// { -// // TODO: priorities change at L54, L64, L72, L82 -// bool allowRuin = CanCast(state, strategy, 1.5f); -// if (state.Unlocked(AID.ArtOfWar1)) -// { -// // L46: spam art of war at 3+ targets, otherwise bio > art of war (even at 1 target) > ruin (if out of range) > ruin2 (on the move) -// if (strategy.NumArtOfWarTargets >= 3) -// return AID.ArtOfWar1; -// else if (!strategy.ForbidDOTs && RefreshDOT(state, state.TargetBioLeft)) -// return AID.Bio2; -// else if (strategy.NumArtOfWarTargets >= 1) -// return AID.ArtOfWar1; -// else if (allowRuin) -// return AID.Ruin1; -// else -// return AID.Ruin2; -// } -// else if (state.Unlocked(AID.Bio2)) -// { -// // L26: bio2 on all targets is more important than ruin -// // L38: cast ruin2 on the move -// return !strategy.ForbidDOTs && RefreshDOT(state, state.TargetBioLeft) ? AID.Bio2 : allowRuin ? AID.Ruin1 : (state.Unlocked(AID.Ruin2) ? AID.Ruin2 : AID.None); -// } -// else if (state.Unlocked(AID.Bio1)) -// { -// // L2: bio1 is only used on the move (TODO: it is slightly more potency than ruin on single target, but only if it ticks to the end) -// return allowRuin ? AID.Ruin1 : !strategy.ForbidDOTs && RefreshDOT(state, state.TargetBioLeft) ? AID.Bio1 : AID.None; -// } -// else -// { -// return allowRuin ? AID.Ruin1 : AID.None; -// } -// } -//} diff --git a/BossMod/AutorotationLegacy/SMN/SMNActions.cs b/BossMod/AutorotationLegacy/SMN/SMNActions.cs deleted file mode 100644 index 68c0dfdaf0..0000000000 --- a/BossMod/AutorotationLegacy/SMN/SMNActions.cs +++ /dev/null @@ -1,125 +0,0 @@ -//namespace BossMod.SMN; - -//class Actions : CommonActions -//{ -// public const int AutoActionST = AutoActionFirstCustom + 0; -// public const int AutoActionAOE = AutoActionFirstCustom + 1; - -// private bool _aoe; -// private readonly Rotation.State _state; -// private readonly Rotation.Strategy _strategy; -// private readonly ConfigListener _config; - -// public Actions(Autorotation autorot, Actor player) -// : base(autorot, player, Definitions.UnlockQuests) -// { -// _state = new(autorot.WorldState); -// _strategy = new(); -// _config = Service.Config.GetAndSubscribe(OnConfigModified); -// } - -// protected override void Dispose(bool disposing) -// { -// _config.Dispose(); -// base.Dispose(disposing); -// } - -// public override CommonRotation.PlayerState GetState() => _state; -// public override CommonRotation.Strategy GetStrategy() => _strategy; - -// public override Targeting SelectBetterTarget(AIHints.Enemy initial) -// { -// // TODO: AOE & multidotting -// return new(initial, 25); -// } - -// protected override void UpdateInternalState(int autoAction) -// { -// _aoe = autoAction switch -// { -// AutoActionST => false, -// AutoActionAOE => true, // TODO: consider making AI-like check -// AutoActionAIFight => Autorot.PrimaryTarget != null && Autorot.Hints.NumPriorityTargetsInAOECircle(Autorot.PrimaryTarget.Position, 5) >= 3, -// _ => false, // irrelevant... -// }; -// UpdatePlayerState(); -// FillCommonStrategy(_strategy, ActionDefinitions.IDPotionInt); -// } - -// protected override void QueueAIActions() -// { -// } - -// protected override ActionQueue.Entry CalculateAutomaticGCD() -// { -// if (!Player.InCombat && _state.Unlocked(AID.SummonCarbuncle) && !_state.PetSummoned) -// return MakeResult(AID.SummonCarbuncle, Player); -// //if ((AutoStrategy & AutoAction.GCDHeal) != 0) -// // return MakeResult(AID.Physick, Autorot.SecondaryTarget); // TODO: automatic target selection - -// if (Autorot.PrimaryTarget == null || AutoAction < AutoActionAIFight) -// return default; -// var aid = Rotation.GetNextBestGCD(_state, _strategy, _aoe, _strategy.ForceMovementIn < 5); -// return MakeResult(aid, Autorot.PrimaryTarget); -// } - -// protected override ActionQueue.Entry CalculateAutomaticOGCD(float deadline) -// { -// if (Autorot.PrimaryTarget == null || AutoAction < AutoActionAIFight) -// return default; - -// ActionID res = new(); -// if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot -// res = Rotation.GetNextBestOGCD(_state, _strategy, deadline - _state.OGCDSlotLength, _aoe); -// if (!res && _state.CanWeave(deadline)) // second/only ogcd slot -// res = Rotation.GetNextBestOGCD(_state, _strategy, deadline, _aoe); -// return MakeResult(res, Autorot.PrimaryTarget); -// } - -// private void UpdatePlayerState() -// { -// FillCommonPlayerState(_state); - -// _state.PetSummoned = Autorot.WorldState.Actors.Any(a => a.Type == ActorType.Pet && a.OwnerID == Player.InstanceID); - -// var gauge = Service.JobGauges.Get(); -// _state.IfritReady = gauge.IsIfritReady; -// _state.TitanReady = gauge.IsTitanReady; -// _state.GarudaReady = gauge.IsGarudaReady; -// _state.Attunement = (Rotation.Attunement)(((int)gauge.AetherFlags >> 2) & 3); -// _state.AttunementStacks = gauge.Attunement; -// _state.AttunementLeft = gauge.AttunmentTimerRemaining * 0.001f; -// _state.SummonLockLeft = gauge.SummonTimerRemaining * 0.001f; -// _state.AetherflowStacks = gauge.AetherflowStacks; - -// _state.SwiftcastLeft = 0; -// foreach (var status in Player.Statuses) -// { -// switch ((SID)status.ID) -// { -// case SID.Swiftcast: -// _state.SwiftcastLeft = StatusDuration(status.ExpireAt); -// break; -// } -// } -// } - -// private void OnConfigModified(SMNConfig config) -// { -// // placeholders -// SupportedSpell(AID.Ruin1).PlaceholderForAuto = config.FullRotation ? AutoActionST : AutoActionNone; -// SupportedSpell(AID.Outburst).PlaceholderForAuto = config.FullRotation ? AutoActionAOE : AutoActionNone; - -// // smart targets -// SupportedSpell(AID.Physick).TransformTarget = config.MouseoverFriendly ? SmartTargetFriendly : null; -// } - -// //private AID SmartResurrectAction() -// //{ -// // // 1. swiftcast, if ready and not up yet -// // if (_state.Unlocked(AID.Swiftcast) && _state.SwiftcastLeft <= 0 && _state.CD(CDGroup.Swiftcast) <= 0) -// // return AID.Swiftcast; - -// // return AID.Resurrection; -// //} -//} diff --git a/BossMod/AutorotationLegacy/SMN/SMNRotation.cs b/BossMod/AutorotationLegacy/SMN/SMNRotation.cs deleted file mode 100644 index 9f47deeb23..0000000000 --- a/BossMod/AutorotationLegacy/SMN/SMNRotation.cs +++ /dev/null @@ -1,105 +0,0 @@ -//namespace BossMod.SMN; - -//public static class Rotation -//{ -// public enum Attunement { None, Ifrit, Titan, Garuda } - -// // full state needed for determining next action -// public class State(WorldState ws) : CommonRotation.PlayerState(ws) -// { -// public bool PetSummoned; -// public bool IfritReady; -// public bool TitanReady; -// public bool GarudaReady; -// public Attunement Attunement; -// public int AttunementStacks; -// public float AttunementLeft; -// public float SummonLockLeft; -// public int AetherflowStacks; // 0-2 -// public float SwiftcastLeft; // 0 if buff not up, max 10 - -// public bool Unlocked(AID aid) => Definitions.Unlocked(aid, Level, UnlockProgress); -// public bool Unlocked(TraitID tid) => Definitions.Unlocked(tid, Level, UnlockProgress); - -// public override string ToString() -// { -// return $"RB={RaidBuffsLeft:f1}, Att={Attunement}/{AttunementStacks}/{AttunementLeft:f1}, SummLock={SummonLockLeft:f1}, IfritR={IfritReady}, TitanR={TitanReady}, GarudaR={GarudaReady}, Aetherflow={AetherflowStacks}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}/{UnlockProgress}"; -// } -// } - -// // strategy configuration -// public class Strategy : CommonRotation.Strategy -// { -// public override string ToString() -// { -// return $""; -// } -// } - -// // TODO: this is valid up to ~L30 -// public static AID GetNextBestGCD(State state, Strategy strategy, bool aoe, bool moving) -// { -// // make sure pet is summoned -// if (!state.PetSummoned && state.Unlocked(AID.SummonCarbuncle)) -// return AID.SummonCarbuncle; - -// if (state.AttunementLeft > state.GCD) -// { -// AID action; -// if (aoe && state.Unlocked(AID.Outburst)) -// { -// action = state.Attunement switch -// { -// Attunement.Ifrit => moving ? AID.None : AID.RubyOutburst, -// Attunement.Titan => AID.TopazOutburst, -// Attunement.Garuda => AID.EmeraldOutburst, -// _ => AID.None -// }; -// } -// else -// { -// action = state.Attunement switch -// { -// Attunement.Ifrit => moving ? AID.None : state.Unlocked(AID.Ruin2) ? AID.RubyRuin2 : AID.RubyRuin1, -// Attunement.Titan => state.Unlocked(AID.Ruin2) ? AID.TopazRuin2 : AID.TopazRuin1, -// Attunement.Garuda => state.Unlocked(AID.Ruin2) ? AID.EmeraldRuin2 : AID.EmeraldRuin1, -// _ => AID.None -// }; -// } -// if (action != AID.None) -// return action; -// } - -// if (strategy.CombatTimer >= 0 && state.Unlocked(AID.SummonRuby) && state.Attunement == Attunement.None && !state.IfritReady && !state.TitanReady && !state.GarudaReady && state.CD(CDGroup.Aethercharge) <= state.GCD) -// return AID.Aethercharge; - -// if (state.SummonLockLeft <= state.GCD) -// { -// if (state.TitanReady && state.Unlocked(AID.SummonTopaz)) -// return AID.SummonTopaz; -// if (state.IfritReady && state.Unlocked(AID.SummonRuby)) -// return AID.SummonRuby; -// if (state.GarudaReady && state.Unlocked(AID.SummonEmerald)) -// return AID.SummonEmerald; -// } - -// if (moving) -// return AID.None; -// else if (aoe && state.Unlocked(AID.Outburst)) -// return AID.Outburst; -// else -// return state.Unlocked(AID.Ruin2) ? AID.Ruin2 : AID.Ruin1; -// } - -// public static ActionID GetNextBestOGCD(State state, Strategy strategy, float deadline, bool aoe) -// { -// // TODO: reconsider priorities, this kinda works at low level -// if (state.Unlocked(AID.EnergyDrain) && state.AetherflowStacks == 0 && state.CanWeave(CDGroup.EnergyDrain, 0.6f, deadline)) -// return ActionID.MakeSpell(AID.EnergyDrain); - -// if (state.Unlocked(AID.Fester) && state.AetherflowStacks > 0 && state.CanWeave(CDGroup.Fester, 0.6f, deadline)) -// return ActionID.MakeSpell(AID.Fester); - -// return new(); -// } -//} diff --git a/BossMod/AutorotationLegacy/TankActions.cs b/BossMod/AutorotationLegacy/TankActions.cs deleted file mode 100644 index d792ed14fa..0000000000 --- a/BossMod/AutorotationLegacy/TankActions.cs +++ /dev/null @@ -1,30 +0,0 @@ -//namespace BossMod; - -//// extra utilities for tanks -//abstract class TankActions(AutorotationLegacy autorot, Actor player, uint[] unlockData) : CommonActions(autorot, player, unlockData) -//{ -// protected bool IsOfftank { get; private set; } - -// public override Targeting SelectBetterTarget(AIHints.Enemy initial) -// { -// // note: most enemies have 'autoattack range' of 2 (compared to player's 3), so staying at max melee can cause enemy movement -// // 1. select closest target that should be tanked but is currently not -// var closestNeedOveraggro = Autorot.Hints.PotentialTargets.Where(e => e.ShouldBeTanked && e.Actor.TargetID != Player.InstanceID).MinBy(e => (e.Actor.Position - Player.Position).LengthSq()); -// if (closestNeedOveraggro != null) -// return new(closestNeedOveraggro, closestNeedOveraggro.TankDistance, Positional.Front, true); - -// // 2. if initial target is not to be tanked, select any that is to be -// if (!initial.ShouldBeTanked && Autorot.Hints.PotentialTargets.FirstOrDefault(e => e.ShouldBeTanked) is var enemyToTank && enemyToTank != null) -// return new(enemyToTank, enemyToTank.TankDistance, Positional.Front, true); - -// return new(initial, initial.TankDistance, Positional.Front, true); -// } - -// protected override void UpdateInternalState(int autoAction) -// { -// var assignments = Service.Config.Get(); -// IsOfftank = assignments[Autorot.WorldState.Party.ContentIDs[PartyState.PlayerSlot]] == PartyRolesConfig.Assignment.OT && Autorot.WorldState.Party.WithoutSlot().Any(a => a != Player && a.Role == Role.Tank); -// } - -// protected bool WantStance() => !IsOfftank || Autorot.Hints.PotentialTargets.Any(e => e.ShouldBeTanked); -//} diff --git a/BossMod/AutorotationLegacy/WHM/WHMActions.cs b/BossMod/AutorotationLegacy/WHM/WHMActions.cs deleted file mode 100644 index 2fe2147733..0000000000 --- a/BossMod/AutorotationLegacy/WHM/WHMActions.cs +++ /dev/null @@ -1,250 +0,0 @@ -//namespace BossMod.WHM; - -//// TODO: this is shit, like all healer modules... -//class Actions : HealerActions -//{ -// public const int AutoActionST = AutoActionFirstCustom + 0; -// public const int AutoActionAOE = AutoActionFirstCustom + 1; -// public const int AutoActionSTHeal = AutoActionFirstCustom + 2; -// public const int AutoActionAOEHeal = AutoActionFirstCustom + 3; - -// private readonly Rotation.State _state; -// private readonly Rotation.Strategy _strategy; -// private bool _allowDelayingNextGCD; -// private readonly ConfigListener _config; - -// public Actions(Autorotation autorot, Actor player) : base(autorot, player, Definitions.UnlockQuests) -// { -// _state = new(autorot.WorldState); -// _strategy = new(); - -// // upgrades -// SupportedSpell(AID.Stone1).TransformAction = SupportedSpell(AID.Stone2).TransformAction = SupportedSpell(AID.Stone3).TransformAction = SupportedSpell(AID.Stone4).TransformAction -// = SupportedSpell(AID.Glare1).TransformAction = SupportedSpell(AID.Glare3).TransformAction = () => ActionID.MakeSpell(_state.BestGlare); -// SupportedSpell(AID.Aero1).TransformAction = SupportedSpell(AID.Aero2).TransformAction = SupportedSpell(AID.Dia).TransformAction = () => ActionID.MakeSpell(_state.BestDia); -// SupportedSpell(AID.Holy1).TransformAction = SupportedSpell(AID.Holy3).TransformAction = () => ActionID.MakeSpell(_state.BestHoly); - -// SupportedSpell(AID.DivineBenison).Condition = target => target != null && target.FindStatus(SID.DivineBenison, Player.InstanceID) == null; // check whether potential divine benison target doesn't already have it applied - -// _config = Service.Config.GetAndSubscribe(OnConfigModified); -// } - -// protected override void Dispose(bool disposing) -// { -// _config.Dispose(); -// base.Dispose(disposing); -// } - -// public override CommonRotation.PlayerState GetState() => _state; -// public override CommonRotation.Strategy GetStrategy() => _strategy; - -// public override Targeting SelectBetterTarget(AIHints.Enemy initial) -// { -// // TODO: look for good place to cast holy and move closer... - -// // look for target to multidot, if initial target already has dot -// if (_state.Unlocked(AID.Aero1) && !WithoutDOT(initial.Actor)) -// { -// var multidotTarget = Autorot.Hints.PriorityTargets.FirstOrDefault(t => t != initial && !t.ForbidDOTs && t.Actor.Position.InCircle(Player.Position, 25) && WithoutDOT(t.Actor)); -// if (multidotTarget != null) -// return new(multidotTarget, multidotTarget.StayAtLongRange ? 25 : 10); -// } - -// return new(initial, initial.StayAtLongRange ? 25 : 10); -// } - -// protected override void UpdateInternalState(int autoAction) -// { -// base.UpdateInternalState(autoAction); -// UpdatePlayerState(); -// FillCommonStrategy(_strategy, ActionDefinitions.IDPotionMnd); -// _strategy.NumAssizeMedica1Targets = _state.Unlocked(AID.Medica1) ? CountAOEHealTargets(15, Player.Position, 0.5f) : 0; -// _strategy.NumRaptureMedica2Targets = _state.Unlocked(AID.Medica2) ? CountAOEHealTargets(20, Player.Position) : 0; -// _strategy.NumCure3Targets = _state.Unlocked(AID.Cure3) ? SmartCure3Target().Item2 : 0; -// _strategy.NumHolyTargets = _state.Unlocked(AID.Holy1) ? Autorot.Hints.NumPriorityTargetsInAOECircle(Player.Position, 8) : 0; -// _strategy.EnableAssize = AllowAssize(); // note: should be plannable... -// _strategy.AllowReplacingHealWithMisery = _config.Data.NeverOvercapBloodLilies && Autorot.PrimaryTarget?.Type == ActorType.Enemy; -// _strategy.Heal = _strategy.AOE = false; -// _strategy.BestSTHeal = FindBestSTHealTarget(); -// if (autoAction >= AutoActionFirstCustom) -// { -// _strategy.Heal = autoAction is AutoActionSTHeal or AutoActionAOEHeal; -// _strategy.AOE = autoAction is AutoActionAOE or AutoActionAOEHeal; -// } -// } - -// protected override void QueueAIActions() -// { -// } - -// protected override ActionQueue.Entry CalculateAutomaticGCD() -// { -// // TODO: rework, especially non-ai... -// switch (AutoAction) -// { -// case AutoActionST: -// return Autorot.PrimaryTarget != null ? MakeResult(Rotation.GetNextBestSTDamageGCD(_state, _strategy), Autorot.PrimaryTarget) : default; -// case AutoActionAOE: -// return Autorot.PrimaryTarget != null ? MakeResult(Rotation.GetNextBestAOEDamageGCD(_state, _strategy), Autorot.PrimaryTarget) : default; -// case AutoActionSTHeal: -// return MakeResult(Rotation.GetNextBestSTHealGCD(_state, _strategy), (_config.Data.MouseoverFriendly ? SmartTargetFriendly(Autorot.PrimaryTarget) : Autorot.PrimaryTarget) ?? Player); -// case AutoActionAOEHeal: -// return MakeResult(Rotation.GetNextBestAOEHealGCD(_state, _strategy), SmartCure3Target().Item1 ?? Player); -// default: -// // AI: aoe heals > st heals > esuna > damage -// // i don't really think 'rotation/actions' split is particularly good fit for healers AI... -// // TODO: raise support... - -// // TODO: L52+ (afflatus rapture) -// // 2. medica 2, if possible and useful, and buff is not already up; we consider it ok to overwrite last tick -// if (Rotation.CanCast(_state, _strategy, 2) && _strategy.NumRaptureMedica2Targets > 2 && _state.CanCastMedica2 && _state.MedicaLeft <= _state.GCD + 2.5f) -// return MakeResult(AID.Medica2, Player); -// // 3. cure 3, if possible and useful -// if (Rotation.CanCast(_state, _strategy, 2) && _strategy.NumCure3Targets > 2 && _state.CanCastCure3) -// return MakeResult(AID.Cure3, SmartCure3Target().Item1 ?? Player); -// // 4. medica 1, if possible and useful -// if (Rotation.CanCast(_state, _strategy, 2) && _strategy.NumAssizeMedica1Targets > 2 && _state.CanCastMedica1) -// return MakeResult(AID.Medica1, Player); - -// // now check ST heals (TODO: afflatus solace) -// if (Rotation.CanCast(_state, _strategy, 2) && _strategy.BestSTHeal.Target != null && _strategy.BestSTHeal.HPRatio <= 0.5f) -// return MakeResult(_state.CanCastCure2 ? AID.Cure2 : AID.Cure1, _strategy.BestSTHeal.Target); - -// // now check esuna -// if (Rotation.CanCast(_state, _strategy, 1)) -// { -// var esunaTarget = FindEsunaTarget(); -// if (esunaTarget != null) -// return MakeResult(AID.Esuna, esunaTarget); -// } - -// // regen, if allowed -// var regenTarget = _state.Unlocked(AID.Regen) ? FindProtectTarget() : null; -// if (regenTarget != null && StatusDetails(regenTarget, SID.Regen, Player.InstanceID).Left <= _state.GCD) -// return MakeResult(AID.Regen, regenTarget); - -// // finally perform damage rotation -// if (_state.CurMP > 3000) -// { -// if (Rotation.CanCast(_state, _strategy, 2.5f) && _strategy.NumHolyTargets >= 3) -// return MakeResult(_state.BestHoly, Player); -// if (Autorot.PrimaryTarget != null) -// return MakeResult(Rotation.GetNextBestSTDamageGCD(_state, _strategy), Autorot.PrimaryTarget); -// } - -// return default; // chill -// } -// } - -// protected override ActionQueue.Entry CalculateAutomaticOGCD(float deadline) -// { -// if (AutoAction < AutoActionAIFight) -// return default; - -// if (deadline < float.MaxValue && _allowDelayingNextGCD) -// deadline += 0.4f + _state.AnimationLockDelay; - -// ActionQueue.Entry res = default; -// if (_state.CanWeave(deadline - _state.OGCDSlotLength)) // first ogcd slot -// res = GetNextBestOGCD(deadline - _state.OGCDSlotLength); -// if (!res.Action && _state.CanWeave(deadline)) // second/only ogcd slot -// res = GetNextBestOGCD(deadline); -// return res; -// } - -// private ActionQueue.Entry GetNextBestOGCD(float deadline) -// { -// // TODO: L52+ - -// // benediction at extremely low hp (TODO: unless planned, tweak threshold) -// if (_strategy.BestSTHeal.Target != null && _strategy.BestSTHeal.HPRatio <= 0 && _state.Unlocked(AID.Benediction) && _state.CanWeave(CDGroup.Benediction, 0.6f, deadline)) -// return MakeResult(AID.Benediction, _strategy.BestSTHeal.Target); - -// // swiftcast, if can't cast any gcd (TODO: current check is not very good...) -// if (deadline >= 10000 && _strategy.ForceMovementIn < 5 && _state.Unlocked(AID.Swiftcast) && _state.CanWeave(CDGroup.Swiftcast, 0.6f, deadline)) -// return MakeResult(AID.Swiftcast, Player); - -// // pom (TODO: consider delaying until raidbuffs?) -// if (_state.Unlocked(AID.PresenceOfMind) && _state.CanWeave(CDGroup.PresenceOfMind, 0.6f, deadline)) -// return MakeResult(AID.PresenceOfMind, Player); - -// // lucid dreaming, if we won't waste mana (TODO: revise mp limit) -// if (_state.CurMP <= 7000 && _state.Unlocked(AID.LucidDreaming) && _state.CanWeave(CDGroup.LucidDreaming, 0.6f, deadline)) -// return MakeResult(AID.LucidDreaming, Player); - -// return default; -// } - -// protected override void OnActionSucceeded(ActorCastEvent ev) -// { -// // note: our GCD heals have cast time 2 => 1.6 under POM (minus haste), +0.1 anim lock => after them we have 0.3 or even slightly smaller window -// // we want to be able to cast at least one oGCD after them, even if it means slightly delaying next GCD -// _allowDelayingNextGCD = ev.Action.Type == ActionType.Spell && (AID)ev.Action.ID is AID.Medica1 or AID.Medica2 or AID.Cure2 or AID.Cure3; -// } - -// private void UpdatePlayerState() -// { -// FillCommonPlayerState(_state); - -// var gauge = Service.JobGauges.Get(); -// _state.NormalLilies = gauge.Lily; -// _state.BloodLilies = gauge.BloodLily; -// _state.NextLilyIn = 30 - gauge.LilyTimer * 0.001f; - -// _state.SwiftcastLeft = StatusDetails(Player, SID.Swiftcast, Player.InstanceID).Left; -// _state.ThinAirLeft = StatusDetails(Player, SID.ThinAir, Player.InstanceID).Left; -// _state.FreecureLeft = StatusDetails(Player, SID.Freecure, Player.InstanceID).Left; -// _state.MedicaLeft = StatusDetails(Player, SID.Medica2, Player.InstanceID).Left; - -// _state.TargetDiaLeft = StatusDetails(Autorot.PrimaryTarget, _state.ExpectedDia, Player.InstanceID).Left; -// } - -// private bool WithoutDOT(Actor a) => Rotation.RefreshDOT(_state, StatusDetails(a, _state.ExpectedDia, Player.InstanceID).Left); - -// private void OnConfigModified(WHMConfig config) -// { -// // placeholders -// //SupportedSpell(AID.Stone1).PlaceholderForAuto = SupportedSpell(AID.Stone2).PlaceholderForAuto = SupportedSpell(AID.Stone3).PlaceholderForAuto = SupportedSpell(AID.Stone4).PlaceholderForAuto -// // = SupportedSpell(AID.Glare1).PlaceholderForAuto = SupportedSpell(AID.Glare3).PlaceholderForAuto = _config.FullRotation ? AutoActionST : AutoActionNone; -// //SupportedSpell(AID.Holy1).PlaceholderForAuto = SupportedSpell(AID.Holy3).PlaceholderForAuto = _config.FullRotation ? AutoActionAOE : AutoActionNone; -// //SupportedSpell(AID.Cure1).PlaceholderForAuto = _config.FullRotation ? AutoActionSTHeal : AutoActionNone; -// //SupportedSpell(AID.Medica1).PlaceholderForAuto = _config.FullRotation ? AutoActionAOEHeal : AutoActionNone; - -// // raise replacement -// SupportedSpell(AID.Raise).TransformAction = config.SwiftFreeRaise ? () => ActionID.MakeSpell(SmartRaiseAction()) : null; - -// // smart targets -// SupportedSpell(AID.Cure1).TransformTarget = SupportedSpell(AID.Cure2).TransformTarget = SupportedSpell(AID.Regen).TransformTarget -// = SupportedSpell(AID.AfflatusSolace).TransformTarget = SupportedSpell(AID.Raise).TransformTarget = SupportedSpell(AID.Esuna).TransformTarget -// = SupportedSpell(AID.Rescue).TransformTarget = SupportedSpell(AID.DivineBenison).TransformTarget = SupportedSpell(AID.Tetragrammaton).TransformTarget -// = SupportedSpell(AID.Benediction).TransformTarget = SupportedSpell(AID.Aquaveil).TransformTarget -// = config.MouseoverFriendly ? SmartTargetFriendly : null; -// SupportedSpell(AID.Cure3).TransformTarget = config.SmartCure3Target ? _ => SmartCure3Target().Item1 : config.MouseoverFriendly ? SmartTargetFriendly : null; -// } - -// private AID SmartRaiseAction() -// { -// // 1. swiftcast, if ready and not up yet -// if (_state.Unlocked(AID.Swiftcast) && _state.SwiftcastLeft <= 0 && _state.CD(CDGroup.Swiftcast) <= 0) -// return AID.Swiftcast; - -// // 2. thin air, if ready and not up yet -// if (_state.Unlocked(AID.ThinAir) && _state.ThinAirLeft <= 0 && _state.CD(CDGroup.ThinAir) <= 60) -// return AID.ThinAir; - -// return AID.Raise; -// } - -// // select best target for cure3, such that most people are hit -// private (Actor?, int) SmartCure3Target() -// { -// var rsq = 30 * 30; -// return Autorot.WorldState.Party.WithoutSlot().Select(o => (o, (o.Position - Player.Position).LengthSq() <= rsq ? CountAOEHealTargets(10, o.Position, 0.5f) : -1)).MaxBy(oc => oc.Item2); -// } - -// // check whether any targetable enemies are in assize range -// private bool AllowAssize() -// { -// return Autorot.Hints.NumPriorityTargetsInAOECircle(Player.Position, 15) > 0; -// } -//} diff --git a/BossMod/AutorotationLegacy/WHM/WHMConfig.cs b/BossMod/AutorotationLegacy/WHM/WHMConfig.cs deleted file mode 100644 index e38df73d1b..0000000000 --- a/BossMod/AutorotationLegacy/WHM/WHMConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -//namespace BossMod; - -//// TODO: reconsider all this... -//[ConfigDisplay(Parent = typeof(ActionTweaksConfig))] -//class LegacyWHMConfig : ConfigNode -//{ -// [PropertyDisplay("When trying to cast raise, apply swiftcast and thin air automatically, if possible")] -// public bool SwiftFreeRaise = true; - -// [PropertyDisplay("Never overcap blood lilies: cast misery instead of solace/rapture if needed")] -// public bool NeverOvercapBloodLilies = false; -//} diff --git a/BossMod/AutorotationLegacy/WHM/WHMRotation.cs b/BossMod/AutorotationLegacy/WHM/WHMRotation.cs deleted file mode 100644 index d720e04193..0000000000 --- a/BossMod/AutorotationLegacy/WHM/WHMRotation.cs +++ /dev/null @@ -1,184 +0,0 @@ -//namespace BossMod.WHM; - -//public static class Rotation -//{ -// // full state needed for determining next action -// public class State(WorldState ws) : CommonRotation.PlayerState(ws) -// { -// public int NormalLilies; -// public int BloodLilies; -// public float NextLilyIn; // max 20 -// public float SwiftcastLeft; // 0 if buff not up, max 10 -// public float ThinAirLeft; // 0 if buff not up, max 12 -// public float FreecureLeft; // 0 if buff not up, max 15 -// public float MedicaLeft; // 0 if hot not up, max 15 -// public float TargetDiaLeft; // 0 if debuff not up, max 30 - -// // upgrade paths -// public AID BestGlare => Unlocked(AID.Glare3) ? AID.Glare3 : Unlocked(AID.Glare1) ? AID.Glare1 : Unlocked(AID.Stone4) ? AID.Stone4 : Unlocked(AID.Stone3) ? AID.Stone3 : Unlocked(AID.Stone2) ? AID.Stone2 : AID.Stone1; -// public AID BestDia => Unlocked(AID.Dia) ? AID.Dia : Unlocked(AID.Aero2) ? AID.Aero2 : AID.Aero1; -// public AID BestHoly => Unlocked(AID.Holy3) ? AID.Holy3 : AID.Holy1; - -// // statuses -// public SID ExpectedDia => Unlocked(AID.Dia) ? SID.Dia : Unlocked(AID.Aero2) ? SID.Aero2 : SID.Aero1; - -// // num lilies on next GCD -// public int NormalLiliesOnNextGCD => Unlocked(AID.AfflatusSolace) ? Math.Min(NormalLilies + (NextLilyIn < GCD ? 1 : 0), 3) : 0; - -// // can-cast checks -// public bool EnoughMPForGCD(int cost) => CurMP >= cost || ThinAirLeft > GCD; -// public bool CanCastMedica1 => Unlocked(AID.Medica1) && EnoughMPForGCD(900); -// public bool CanCastMedica2 => Unlocked(AID.Medica2) && EnoughMPForGCD(1000); -// public bool CanCastCure2 => Unlocked(AID.Cure2) && (EnoughMPForGCD(1000) || FreecureLeft > GCD); -// public bool CanCastCure3 => Unlocked(AID.Cure3) && EnoughMPForGCD(1500); - -// public bool Unlocked(AID aid) => Definitions.Unlocked(aid, Level, UnlockProgress); -// public bool Unlocked(TraitID tid) => Definitions.Unlocked(tid, Level, UnlockProgress); - -// public override string ToString() -// { -// return $"g={NormalLilies}/{BloodLilies}/{NextLilyIn:f1}, MP={CurMP}, RB={RaidBuffsLeft:f1}, Dia={TargetDiaLeft:f1}, Medica={MedicaLeft:f1}, Swiftcast={SwiftcastLeft:f1}, TA={ThinAirLeft:f1}, Freecure={FreecureLeft:f1}, AssizeCD={CD(CDGroup.Assize):f1}, PoMCD={CD(CDGroup.PresenceOfMind):f1}, LDCD={CD(CDGroup.LucidDreaming):f1}, TACD={CD(CDGroup.ThinAir):f1}, PlenCD={CD(CDGroup.PlenaryIndulgence):f1}, AsylumCD={CD(CDGroup.Asylum):f1}, BeniCD={CD(CDGroup.DivineBenison):f1}, TetraCD={CD(CDGroup.Tetragrammaton):f1}, PotCD={PotionCD:f1}, GCD={GCD:f3}, ALock={AnimationLock:f3}+{AnimationLockDelay:f3}, lvl={Level}/{UnlockProgress}"; -// } -// } - -// public class Strategy : CommonRotation.Strategy -// { -// public bool Heal; -// public bool AOE; -// public int NumAssizeMedica1Targets; // how many targets would assize/medica1 heal (15y around self) -// public int NumRaptureMedica2Targets; // how many targets would rapture/medica2 heal (20y around self) -// public int NumCure3Targets; // how many targets cure3 would heal (10y around selected/best target) -// public int NumHolyTargets; // how many targets holy would hit (8y around self) -// public bool EnableAssize; -// public bool AllowReplacingHealWithMisery; // if true, allow replacing solace/rapture with misery -// public (Actor? Target, float HPRatio) BestSTHeal; - -// public override string ToString() -// { -// return $"AOE={NumHolyTargets}, SH={BestSTHeal.Target?.Name[..4]}={BestSTHeal.HPRatio:f2}, AH={NumRaptureMedica2Targets}/{NumCure3Targets}/{NumAssizeMedica1Targets}, no-dots={ForbidDOTs}, movement-in={ForceMovementIn:f3}"; -// } -// } - -// public static bool CanCast(State state, Strategy strategy, float castTime) => state.SwiftcastLeft > state.GCD || strategy.ForceMovementIn >= state.GCD + castTime; -// public static bool RefreshDOT(State state, float timeLeft) => timeLeft < state.GCD + 3.0f; // TODO: tweak threshold so that we don't overwrite or miss ticks... - -// public static ActionID GetNextBestOGCD(State state, Strategy strategy, float deadline) -// { -// // 1. plenary indulgence, if we're gonna cast aoe gcd heal that will actually heal someone... (TODO: reconsider priority) -// if (strategy.AOE && strategy.Heal && (strategy.NumRaptureMedica2Targets > 0 || strategy.NumCure3Targets > 0) && state.Unlocked(AID.PlenaryIndulgence) && state.CanWeave(CDGroup.PlenaryIndulgence, 0.6f, deadline)) -// return ActionID.MakeSpell(AID.PlenaryIndulgence); - -// // 3. potion (TODO) - -// // 4. assize, if allowed and if we have some mana deficit (TODO: consider delaying until raidbuffs?) -// if (strategy.EnableAssize && state.CurMP <= 9000 && state.Unlocked(AID.Assize) && state.CanWeave(CDGroup.Assize, 0.6f, deadline)) -// return ActionID.MakeSpell(AID.Assize); - -// // 5. pom (TODO: consider delaying until raidbuffs?) -// if (state.Unlocked(AID.PresenceOfMind) && state.CanWeave(CDGroup.PresenceOfMind, 0.6f, deadline)) -// return ActionID.MakeSpell(AID.PresenceOfMind); - -// // 6. lucid dreaming, if we won't waste mana (TODO: revise mp limit) -// if (state.CurMP <= 7000 && state.Unlocked(AID.LucidDreaming) && state.CanWeave(CDGroup.LucidDreaming, 0.6f, deadline)) -// return ActionID.MakeSpell(AID.LucidDreaming); - -// // 7. thin air, if we have some mana deficit and we're either sitting on 2 charges or we're gonna cast expensive spell (TODO: revise mp limit) -// if (state.CurMP <= 9000 && state.Unlocked(AID.ThinAir) && state.CanWeave(state.CD(CDGroup.ThinAir) - 60, 0.6f, deadline)) -// { -// if (state.CD(CDGroup.ThinAir) < state.GCD) -// return ActionID.MakeSpell(AID.ThinAir); // spend second charge to start cooldown ticking, even if we gonna cast glare -// if (strategy.Heal && state.NormalLiliesOnNextGCD == 0) -// return ActionID.MakeSpell(AID.ThinAir); // spend last charge if we're gonna cast expensive GCD -// } - -// return new(); -// } - -// public static AID GetNextBestSTDamageGCD(State state, Strategy strategy) -// { -// bool allowCasts = CanCast(state, strategy, 1.5f); - -// // 0. just use glare before pull -// if (allowCasts && strategy.CombatTimer < 0) -// return state.BestGlare; - -// // 1. refresh dia/aero, if needed -// if (!strategy.ForbidDOTs && state.Unlocked(AID.Aero1) && RefreshDOT(state, state.TargetDiaLeft)) -// return state.BestDia; - -// // 2. glare, if not moving or if under swiftcast -// if (allowCasts) -// return state.BestGlare; - -// // 3. afflatus misery -// if (state.BloodLilies >= 3) -// return AID.AfflatusMisery; - -// return AID.None; -// // 4. slidecast glare (TODO: consider early dia refresh if GCD is zero...) -// //return strategy.Moving ? state.BestDia : state.BestGlare; -// } - -// public static AID GetNextBestAOEDamageGCD(State state, Strategy strategy) -// { -// // misery > holy -// return state.BloodLilies >= 3 ? AID.AfflatusMisery : state.BestHoly; -// } - -// public static AID GetNextBestSTHealGCD(State state, Strategy strategy) -// { -// // 1. cure 2, if free -// if (state.Unlocked(AID.Cure2) && (state.FreecureLeft > state.GCD || state.ThinAirLeft > state.GCD)) -// return AID.Cure2; - -// // 2. afflatus solace, if possible (replace with misery if needed) -// if (state.NormalLiliesOnNextGCD > 0) -// return strategy.AllowReplacingHealWithMisery && state.BloodLilies >= 3 ? AID.AfflatusMisery : AID.AfflatusSolace; - -// // 3. cure 2, if possible -// if (state.Unlocked(AID.Cure2) && state.CurMP >= 1000) -// return AID.Cure2; - -// // 4. fallback to cure1 -// return AID.Cure1; -// } - -// public static AID GetNextBestAOEHealGCD(State state, Strategy strategy) -// { -// // 1. afflatus rapture, if possible (replace with misery if needed) -// bool canCastRapture = state.Unlocked(AID.AfflatusRapture) && state.NormalLiliesOnNextGCD > 0; -// if (canCastRapture && strategy.NumRaptureMedica2Targets > 0) -// return strategy.AllowReplacingHealWithMisery && state.BloodLilies >= 3 ? AID.AfflatusMisery : AID.AfflatusRapture; - -// // 2. medica 2, if possible and useful, and buff is not already up; we consider it ok to overwrite last tick -// bool canCastMedica2 = state.CanCastMedica2; -// if (canCastMedica2 && strategy.NumRaptureMedica2Targets > 0 && state.MedicaLeft <= state.GCD + 2.5f) -// return AID.Medica2; - -// // 3. cure 3, if possible and useful -// if (strategy.NumCure3Targets > 0 && state.CanCastCure3) -// return AID.Cure3; - -// // 4. medica 1, if possible and useful -// if (strategy.NumAssizeMedica1Targets > 0 && state.CanCastMedica1) -// return AID.Medica1; - -// // 5. fallback: overheal medica 2 for hot (e.g. during downtime) -// if (canCastMedica2 && CanCast(state, strategy, 2) && state.MedicaLeft <= state.GCD + 5) -// return AID.Medica2; - -// // 6. fallback: overheal rapture, unless capped on blood lilies -// if (canCastRapture && state.BloodLilies < 3) -// return AID.AfflatusRapture; - -// // 7. fallback to medica 1/2 -// return canCastMedica2 ? AID.Medica2 : AID.Medica1; -// } - -// public static AID GetNextBestGCD(State state, Strategy strategy) -// { -// return strategy.Heal -// ? (strategy.AOE ? GetNextBestAOEHealGCD(state, strategy) : GetNextBestSTHealGCD(state, strategy)) -// : (strategy.AOE ? GetNextBestAOEDamageGCD(state, strategy) : GetNextBestSTDamageGCD(state, strategy)); -// } -//} From d9e2a84185105e74125112a6505c8e50cfe4aaad Mon Sep 17 00:00:00 2001 From: Akechi-kun <167795370+Akechi-kun@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:46:09 -0800 Subject: [PATCH 2/2] Update TODO --- TODO | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/TODO b/TODO index 87e61381e1..7eb3448c30 100644 --- a/TODO +++ b/TODO @@ -2,7 +2,6 @@ immediate plans - refactor replay history feature - remove 'ex' variants of strategies, replace with cooldown/effect functors, write a converter - add strategy param, use for 'apply unless already active' flavours of utility strategies -- get rid of legacyxxx - ishape -- rasterization should automatically deal with 0.03 position imprecision (and angle imprecision?..) @@ -79,23 +78,17 @@ autorotation: - action history/queue visualization - simulation in replay analysis - spell out shared cooldowns in actiondefinitions comments instead of group? -- delete/rework commonstate/legacy -- delete autorotationlegacy dir - dot/regen server tick tracking - brd -- take traits into account (ij proccing rs, ea proccing repertoire) -- better handle bad misalignment (see JeunoTheFirstWalk_BRD100_VeynHumanmale_2025_01_05_20_42_19.log) - drg --- priorities... --- dragon sight is a true north --- cd planning +-- write new module(?) - war -- simulate gauge changes (message can arrive few frames after ActionEffect...) -- low-level rotation - improve berserk delay logic - whm: --- resurrect (6.1 and later changes) --- planner --- smart-targeting for ST actions +-- write new module(?) ai: - improve healing AI: analyze incoming dps @@ -113,7 +106,6 @@ misc: - memory show/watch utility - clip circles to arena bounds... - draw target max-melee and boss positioning hints?.. -- assignments/config sharing (webservice?) notes on targeting: - aoe (e.g. cone) will hit if shape intersects target's hitbox; for players it is == 0.5