From 072607182dd0f840e69ba29fe49d88ea6e07ea7e Mon Sep 17 00:00:00 2001 From: AceAkechi123 Date: Sun, 19 Jan 2025 05:23:51 -0800 Subject: [PATCH 01/33] less spaghetti --- BossMod/Autorotation/akechi/AkechiBLM.cs | 1247 +++++++++------------- 1 file changed, 522 insertions(+), 725 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index d677562dea..dc0449f057 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -130,13 +130,13 @@ public static RotationModuleDefinition Definition() "Standard Rotation Module", //Description "Standard rotation (Akechi)", //Category "Akechi", //Contributor - RotationModuleQuality.Basic, //Quality + RotationModuleQuality.Ok, //Quality BitMask.Build(Class.THM, Class.BLM), //Job 100); //Level supported #region Custom strategies res.Define(Track.AOE).As("AOE", "AOE", uiPriority: 200) - .AddOption(AOEStrategy.Auto, "Auto", "Automatically decide when to use ST or AOE abilities") + .AddOption(AOEStrategy.Auto, "Auto", "Automatically decide when to use ST or AOE abilities", supportedTargets: ActionTargets.Hostile) .AddOption(AOEStrategy.ForceST, "Force ST", "Force use of ST abilities only", supportedTargets: ActionTargets.Hostile) .AddOption(AOEStrategy.ForceAOE, "Force AOE", "Force use of AOE abilities only", supportedTargets: ActionTargets.Hostile); res.Define(Track.Movement).As("Movement", uiPriority: 195) @@ -146,11 +146,11 @@ public static RotationModuleDefinition Definition() .AddOption(MovementStrategy.OnlyScathe, "OnlyScathe", "Only use Scathe for movement") .AddOption(MovementStrategy.Forbid, "Forbid", "Forbid the use of any abilities for movement"); res.Define(Track.Thunder).As("Thunder", "DOT", uiPriority: 190) - .AddOption(ThunderStrategy.Thunder3, "Thunder3", "Use Thunder if target has 3s or less remaining on DoT effect", 0, 30, ActionTargets.Hostile, 6) - .AddOption(ThunderStrategy.Thunder6, "Thunder6", "Use Thunder if target has 6s or less remaining on DoT effect", 0, 30, ActionTargets.Hostile, 6) - .AddOption(ThunderStrategy.Thunder9, "Thunder9", "Use Thunder if target has 9s or less remaining on DoT effect", 0, 30, ActionTargets.Hostile, 6) - .AddOption(ThunderStrategy.Thunder0, "Thunder0", "Use Thunder if target does not have DoT effect", 0, 30, ActionTargets.Hostile, 6) - .AddOption(ThunderStrategy.Force, "Force", "Force use of Thunder regardless of DoT effect", 0, 30, ActionTargets.Hostile, 6) + .AddOption(ThunderStrategy.Thunder3, "Thunder3", "Use Thunder if target has 3s or less remaining on DoT effect", 0, 27, ActionTargets.Hostile, 6) + .AddOption(ThunderStrategy.Thunder6, "Thunder6", "Use Thunder if target has 6s or less remaining on DoT effect", 0, 27, ActionTargets.Hostile, 6) + .AddOption(ThunderStrategy.Thunder9, "Thunder9", "Use Thunder if target has 9s or less remaining on DoT effect", 0, 27, ActionTargets.Hostile, 6) + .AddOption(ThunderStrategy.Thunder0, "Thunder0", "Use Thunder if target does not have DoT effect", 0, 27, ActionTargets.Hostile, 6) + .AddOption(ThunderStrategy.Force, "Force", "Force use of Thunder regardless of DoT effect", 0, 27, ActionTargets.Hostile, 6) .AddOption(ThunderStrategy.Delay, "Delay", "Delay the use of Thunder for manual or strategic usage", 0, 0, ActionTargets.Hostile, 6) .AddAssociatedActions(AID.Thunder1, AID.Thunder2, AID.Thunder3, AID.Thunder4, AID.HighThunder, AID.HighThunder2); res.Define(Track.Polyglot).As("Polyglot", "Polyglot", uiPriority: 180) @@ -161,11 +161,11 @@ public static RotationModuleDefinition Definition() .AddOption(PolyglotStrategy.XenoSpendAll, "XenoSpendAll", "Use Xenoglossy as optimal spender, regardless of targets nearby; spends all Polyglots", 0, 0, ActionTargets.Hostile, 80) .AddOption(PolyglotStrategy.XenoHold1, "XenoHold1", "Use Xenoglossy as optimal spender, regardless of targets nearby; holds one Polyglot for manual usage", 0, 0, ActionTargets.Hostile, 80) .AddOption(PolyglotStrategy.XenoHold2, "XenoHold2", "Use Xenoglossy as optimal spender, regardless of targets nearby; holds two Polyglots for manual usage", 0, 0, ActionTargets.Hostile, 80) - .AddOption(PolyglotStrategy.XenoHold3, "XenoHold3", "Holds all Polyglots for as long as possible", 0, 0, ActionTargets.Hostile, 80) + .AddOption(PolyglotStrategy.XenoHold3, "XenoHold3", "Use Xenoglossy as optimal spender; Holds all Polyglots for as long as possible", 0, 0, ActionTargets.Hostile, 80) .AddOption(PolyglotStrategy.FoulSpendAll, "FoulSpendAll", "Use Foul as optimal spender, regardless of targets nearby", 0, 0, ActionTargets.Hostile, 70) .AddOption(PolyglotStrategy.FoulHold1, "FoulHold1", "Use Foul as optimal spender, regardless of targets nearby; holds one Polyglot for manual usage", 0, 0, ActionTargets.Hostile, 70) .AddOption(PolyglotStrategy.FoulHold2, "FoulHold2", "Use Foul as optimal spender, regardless of targets nearby; holds two Polyglots for manual usage", 0, 0, ActionTargets.Hostile, 70) - .AddOption(PolyglotStrategy.FoulHold3, "FoulHold3", "Holds all Polyglots for as long as possible", 0, 0, ActionTargets.Hostile, 70) + .AddOption(PolyglotStrategy.FoulHold3, "FoulHold3", "Use Foul as optimal spender; Holds all Polyglots for as long as possible", 0, 0, ActionTargets.Hostile, 70) .AddOption(PolyglotStrategy.ForceXeno, "Force Xenoglossy", "Force use of Xenoglossy", 0, 0, ActionTargets.Hostile, 80) .AddOption(PolyglotStrategy.ForceFoul, "Force Foul", "Force use of Foul", 0, 0, ActionTargets.Hostile, 70) .AddOption(PolyglotStrategy.Delay, "Delay", "Delay the use of Polyglot abilities for manual or strategic usage", 0, 0, ActionTargets.Hostile, 70) @@ -180,19 +180,19 @@ public static RotationModuleDefinition Definition() .AddAssociatedActions(AID.Manafont); res.Define(Track.Triplecast).As("T.cast", uiPriority: 170) .AddOption(TriplecastStrategy.Automatic, "Auto", "Use any charges available during Ley Lines window or every 2 minutes (NOTE: does not take into account charge overcap, will wait for 2 minute windows to spend both)", 0, 0, ActionTargets.Self, 66) - .AddOption(TriplecastStrategy.Force, "Force", "Force the use of Triplecast; uses all charges", 60, 0, ActionTargets.Self, 66) - .AddOption(TriplecastStrategy.Force1, "Force1", "Force the use of Triplecast; holds one charge for manual usage", 60, 0, ActionTargets.Self, 66) - .AddOption(TriplecastStrategy.ForceWeave, "ForceWeave", "Force the use of Triplecast in any next possible weave slot", 60, 0, ActionTargets.Self, 66) - .AddOption(TriplecastStrategy.ForceWeave1, "ForceWeave1", "Force the use of Triplecast in any next possible weave slot; holds one charge for manual usage", 60, 0, ActionTargets.Self, 66) - .AddOption(TriplecastStrategy.Delay, "Delay", "Delay the use of Triplecast", 60, 0, ActionTargets.Self, 66) + .AddOption(TriplecastStrategy.Force, "Force", "Force the use of Triplecast; uses all charges", 60, 15, ActionTargets.Self, 66) + .AddOption(TriplecastStrategy.Force1, "Force1", "Force the use of Triplecast; holds one charge for manual usage", 60, 15, ActionTargets.Self, 66) + .AddOption(TriplecastStrategy.ForceWeave, "ForceWeave", "Force the use of Triplecast in any next possible weave slot", 60, 15, ActionTargets.Self, 66) + .AddOption(TriplecastStrategy.ForceWeave1, "ForceWeave1", "Force the use of Triplecast in any next possible weave slot; holds one charge for manual usage", 60, 15, ActionTargets.Self, 66) + .AddOption(TriplecastStrategy.Delay, "Delay", "Delay the use of Triplecast", 0, 0, ActionTargets.Self, 66) .AddAssociatedActions(AID.Triplecast); res.Define(Track.LeyLines).As("L.Lines", uiPriority: 170) .AddOption(LeyLinesStrategy.Automatic, "Auto", "Automatically decide when to use Ley Lines", 0, 0, ActionTargets.Self, 52) - .AddOption(LeyLinesStrategy.Force, "Force", "Force the use of Ley Lines, regardless of weaving conditions", 120, 0, ActionTargets.Self, 52) - .AddOption(LeyLinesStrategy.Force1, "Force1", "Force the use of Ley Lines; holds one charge for manual usage", 120, 0, ActionTargets.Self, 52) - .AddOption(LeyLinesStrategy.ForceWeave, "ForceWeave", "Force the use of Ley Lines in any next possible weave slot", 120, 0, ActionTargets.Self, 52) - .AddOption(LeyLinesStrategy.ForceWeave1, "ForceWeave1", "Force the use of Ley Lines in any next possible weave slot; holds one charge for manual usage", 120, 0, ActionTargets.Self, 52) - .AddOption(LeyLinesStrategy.Delay, "Delay", "Delay the use of Ley Lines", 120, 0, ActionTargets.Self, 52) + .AddOption(LeyLinesStrategy.Force, "Force", "Force the use of Ley Lines, regardless of weaving conditions", 120, 30, ActionTargets.Self, 52) + .AddOption(LeyLinesStrategy.Force1, "Force1", "Force the use of Ley Lines; holds one charge for manual usage", 120, 30, ActionTargets.Self, 52) + .AddOption(LeyLinesStrategy.ForceWeave, "ForceWeave", "Force the use of Ley Lines in any next possible weave slot", 120, 30, ActionTargets.Self, 52) + .AddOption(LeyLinesStrategy.ForceWeave1, "ForceWeave1", "Force the use of Ley Lines in any next possible weave slot; holds one charge for manual usage", 120, 30, ActionTargets.Self, 52) + .AddOption(LeyLinesStrategy.Delay, "Delay", "Delay the use of Ley Lines", 0, 0, ActionTargets.Self, 52) .AddAssociatedActions(AID.LeyLines); res.Define(Track.Potion).As("Potion", uiPriority: 160) .AddOption(PotionStrategy.Manual, "Manual", "Do not use automatically") @@ -215,7 +215,7 @@ public static RotationModuleDefinition Definition() .AddOption(OffensiveStrategy.Force, "Force", "Force the use of Transpose, regardless of weaving conditions", 5, 0, ActionTargets.Self, 4) .AddOption(OffensiveStrategy.AnyWeave, "AnyWeave", "Force the use of Transpose in any next possible weave slot", 5, 0, ActionTargets.Self, 4) .AddOption(OffensiveStrategy.EarlyWeave, "EarlyWeave", "Force the use of Transpose in very next FIRST weave slot only", 5, 0, ActionTargets.Self, 4) - .AddOption(OffensiveStrategy.LateWeave, "LateWeave", "Force the use of Transpose in very next LAST weave slot only", 0, 0, ActionTargets.Self, 4) + .AddOption(OffensiveStrategy.LateWeave, "LateWeave", "Force the use of Transpose in very next LAST weave slot only", 5, 0, ActionTargets.Self, 4) .AddOption(OffensiveStrategy.Delay, "Delay", "Delay the use of Transpose", 0, 0, ActionTargets.Self, 4) .AddAssociatedActions(AID.Transpose); res.Define(Track.Amplifier).As("Amplifier", uiPriority: 170) @@ -242,7 +242,6 @@ public static RotationModuleDefinition Definition() .AddOption(OffensiveStrategy.LateWeave, "LateWeave", "Force the use of Between The Lines in very next LAST weave slot only", 3, 0, ActionTargets.Self, 62) .AddOption(OffensiveStrategy.Delay, "Delay", "Delay the use of Between The Lines", 0, 0, ActionTargets.Self, 62) .AddAssociatedActions(AID.BetweenTheLines); - #endregion return res; @@ -250,20 +249,20 @@ public static RotationModuleDefinition Definition() #endregion #region Priorities - //TODO: Fix this shit later, looks crazy public enum GCDPriority //priorities for GCDs (higher number = higher priority) { - None = 0, //default - Step1 = 100, //Step 1 - Step2 = 110, //Step 2 - Step3 = 120, //Step 3 - Step4 = 130, //Step 4 - Step5 = 140, //Step 5 - Step6 = 150, //Step 6 - Step7 = 160, //Step 7 - Step8 = 170, //Step 8 - Step9 = 180, //Step 9 - Step10 = 190, //Step 10 + None = 0, + + //Rotation + SixthStep = 100, + FifthStep = 125, + FourthStep = 150, + ThirdStep = 175, + SecondStep = 200, + FirstStep = 250, + ForcedStep = 299, + + //GCDs Standard = 300, //standard abilities DOT = 350, //damage-over-time abilities FlareStar = 375, //Flare Star @@ -272,19 +271,23 @@ public static RotationModuleDefinition Definition() NeedB3 = 460, //Need to use Blizzard III Polyglot = 475, //Polyglots Paradox = 500, //Paradox + + //Necessities NeedDOT = 600, //Need to apply DOTs NeedF3P = 625, //Need to use Fire III proc - NeedDespair = 640, //Need to use Despair NeedPolyglot = 650, //Need to use Polyglots + + //Moving Moving3 = 700, //Moving (3rd priority) Moving2 = 710, //Moving (2nd priority) Moving1 = 720, //Moving (1st priority) + + //Forced ForcedGCD = 900, //Forced GCDs - BlockAll = 2000, //Block all GCDs } public enum OGCDPriority //priorities for oGCDs (higher number = higher priority) { - None = 0, //default + None = 0, Transpose = 400, //Transpose Manafont = 450, //Manafont LeyLines = 500, //Ley Lines @@ -303,7 +306,8 @@ private AID BestThunderST private AID BestThunderAOE => Unlocked(AID.HighThunder2) ? AID.HighThunder2 : Unlocked(AID.Thunder4) ? AID.Thunder4 - : AID.Thunder2; + : Unlocked(AID.Thunder2) ? AID.Thunder2 + : AID.Thunder1; private AID BestThunder => ShouldUseAOE ? BestThunderAOE : BestThunderST; private AID BestPolyglot @@ -380,50 +384,18 @@ private bool JustUsed(AID aid, float variance) } #region Targeting - private int TargetsInRange() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 25); //Returns the number of targets hit by AOE within a 25-yalm radius around the player - private Actor? TargetChoice(StrategyValues.OptionRef strategy) => ResolveTargetOverride(strategy.Value); //Resolves the target choice based on the strategy - private Actor? FindBestSplashTarget() - { - float splashPriorityFunc(Actor actor) - { - var distanceToPlayer = actor.DistanceToHitbox(Player); - if (distanceToPlayer <= 24f) - { - var targetsInSplashRadius = 0; - foreach (var enemy in Hints.PriorityTargets) - { - var targetActor = enemy.Actor; - if (targetActor != actor && targetActor.Position.InCircle(actor.Position, 5f)) - { - targetsInSplashRadius++; - } - } - return targetsInSplashRadius; - } - return float.MinValue; - } - - var (bestTarget, bestPrio) = FindBetterTargetBy(null, 25f, splashPriorityFunc); - - return bestTarget; - } - private Actor? BestAOETarget => FindBestSplashTarget(); // Find the best target for splash attack private bool ShouldUseAOE { get { - // Check if there's a valid target for the AoE attack var bestTarget = BestAOETarget; - - // If there is a best target and it has a significant number of other targets in its splash radius, we can use AoE if (bestTarget != null) { - // We can define a threshold to require a minimum number of targets within the splash radius to make AoE worthwhile - var minimumTargetsForAOE = 2; // Example: At least 2 other enemies within the 5-yard splash radius + var minimumTargetsForAOE = 2; float splashPriorityFunc(Actor actor) { var distanceToPlayer = actor.DistanceToHitbox(Player); - if (distanceToPlayer <= 24f) + if (distanceToPlayer <= 24.99f) { var targetsInSplashRadius = 0; foreach (var enemy in Hints.PriorityTargets) @@ -447,6 +419,35 @@ float splashPriorityFunc(Actor actor) return false; } } + private int TargetsInRange() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 25); //Returns the number of targets hit by AOE within a 25-yalm radius around the player + private Actor? TargetChoice(StrategyValues.OptionRef strategy) => ResolveTargetOverride(strategy.Value); //Resolves the target choice based on the strategy + private Actor? FindBestSplashTarget() + { + float splashPriorityFunc(Actor actor) + { + var distanceToPlayer = actor.DistanceToHitbox(Player); + if (distanceToPlayer <= 24f) + { + var targetsInSplashRadius = 0; + foreach (var enemy in Hints.PriorityTargets) + { + var targetActor = enemy.Actor; + if (targetActor != actor && targetActor.Position.InCircle(actor.Position, 5f)) + { + targetsInSplashRadius++; + } + } + return targetsInSplashRadius; + } + return float.MinValue; + } + + var (bestTarget, bestPrio) = FindBetterTargetBy(null, 25f, splashPriorityFunc); + + return bestTarget; + } + private Actor? BestAOETarget => FindBestSplashTarget(); // Find the best target for splash attack + //TODO: BestDOTTarget #endregion #endregion @@ -455,10 +456,10 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa { #region Variables var gauge = World.Client.GetGauge(); //Retrieve BLM gauge - NoStance = ElementStance is 0; //No stance + NoStance = ElementStance is 0 and not (1 or 2 or 3 or -1 or -2 or -3); //No stance ElementStance = gauge.ElementStance; //Elemental Stance - InAstralFire = ElementStance is 1 or 2 or 3; //In Astral Fire - InUmbralIce = ElementStance is -1 or -2 or -3; //In Umbral Ice + InAstralFire = ElementStance is 1 or 2 or 3 and not (0 or -1 or -2 or -3); //In Astral Fire + InUmbralIce = ElementStance is -1 or -2 or -3 and not (0 or 1 or 2 or 3); //In Umbral Ice Polyglots = gauge.PolyglotStacks; //Polyglot Stacks UmbralHearts = gauge.UmbralHearts; //Umbral Hearts MaxUmbralHearts = Unlocked(TraitID.UmbralHeart) ? 3 : 0; @@ -516,6 +517,8 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa var potionStrat = strategy.Option(Track.Potion).As(); //Potion strategy var tpusStrat = strategy.Option(Track.TPUS).As(); //Transpose/Umbral Soul strategy var movingOption = strategy.Option(Track.Casting).As(); //Casting while moving strategy + var forceST = AOEStrategy is AOEStrategy.ForceST; //Force single target + var forceAOE = AOEStrategy is AOEStrategy.ForceAOE; //Force AOE #endregion #endregion @@ -524,20 +527,20 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa #region ST / AOE if (movingOption is CastingOption.Allow || - movingOption is CastingOption.Forbid && + (movingOption is CastingOption.Forbid && (!isMoving || //if not moving - (PlayerHasEffect(SID.Swiftcast, 10) || //or has Swiftcast + PlayerHasEffect(SID.Swiftcast, 10) || //or has Swiftcast PlayerHasEffect(SID.Triplecast, 15) || //or has Triplecast - (canParadox && ElementTimer < (SpS * 3) && MP >= 1600 || canParadox && JustUsed(AID.Blizzard4, 5)) || //or can use Paradox + (canParadox && (ElementTimer < (SpS * 3) && MP >= 1600) || JustUsed(AID.Blizzard4, 5)) || //or can use Paradox SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 || //or can use F3P (Unlocked(TraitID.EnhancedAstralFire) && MP is < 1600 and not 0)))) //instant cast Despair { if (AOEStrategy is AOEStrategy.Auto) - BestRotation(TargetChoice(AOE) ?? BestAOETarget ?? primaryTarget); - if (AOEStrategy is AOEStrategy.ForceST) - BestST(TargetChoice(AOE) ?? primaryTarget); - if (AOEStrategy is AOEStrategy.ForceAOE) - BestAOE(TargetChoice(AOE) ?? BestAOETarget ?? primaryTarget); + BestRotation(TargetChoice(AOE) ?? primaryTarget ?? BestAOETarget); //target prio is user choice -> current target -> best AOE target + if (forceST) + BestST(TargetChoice(AOE) ?? primaryTarget); //target prio is user choice -> current target + if (forceAOE) + BestAOE(TargetChoice(AOE) ?? primaryTarget ?? BestAOETarget); //target prio is user choice -> best AOE target -> current target } #endregion @@ -552,9 +555,11 @@ movingOption is CastingOption.Forbid && if (!PlayerHasEffect(SID.Swiftcast, 10) || !PlayerHasEffect(SID.Triplecast, 15)) QueueGCD( - Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? BestPolyglot + Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? + (forceST ? BestXenoglossy : forceAOE ? AID.Foul : BestPolyglot) : PlayerHasEffect(SID.Firestarter, 30) ? AID.Fire3 - : hasThunderhead ? BestThunder + : hasThunderhead ? + (forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder) : AID.Scathe, Polyglots > 0 ? TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget @@ -577,9 +582,11 @@ movingOption is CastingOption.Forbid && if (!PlayerHasEffect(SID.Swiftcast, 10) || !PlayerHasEffect(SID.Triplecast, 15)) QueueGCD( - Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? BestPolyglot + Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? + (forceST ? BestXenoglossy : forceAOE ? AID.Foul : BestPolyglot) : PlayerHasEffect(SID.Firestarter, 30) ? AID.Fire3 - : hasThunderhead ? BestThunder + : hasThunderhead ? + (forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder) : AID.Scathe, Polyglots > 0 ? TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget @@ -599,7 +606,7 @@ movingOption is CastingOption.Forbid && } if (movementStrat is MovementStrategy.OnlyScathe) { - if (MP >= 800) + if (Unlocked(AID.Scathe) && MP >= 800) QueueGCD(AID.Scathe, primaryTarget, GCDPriority.Moving1); } } @@ -643,17 +650,17 @@ movingOption is CastingOption.Forbid && { if (AOEStrategy is AOEStrategy.Auto) QueueGCD(BestThunder, - TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget, + TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); - if (AOEStrategy is AOEStrategy.ForceST) + if (forceST) QueueGCD(BestThunderST, TargetChoice(thunder) ?? primaryTarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); if (AOEStrategy is AOEStrategy.ForceAOE) QueueGCD(BestThunderAOE, - TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget, + TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); } @@ -665,10 +672,10 @@ or PolyglotStrategy.AutoHold1 or PolyglotStrategy.AutoHold2 or PolyglotStrategy.AutoHold3) QueueGCD(BestPolyglot, - TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget, + TargetChoice(polyglot) ?? primaryTarget ?? BestAOETarget, polyglotStrat is PolyglotStrategy.ForceXeno ? GCDPriority.ForcedGCD - : Polyglots == MaxPolyglots && EnochianTimer < 5000 ? GCDPriority.NeedPolyglot - : GCDPriority.Paradox); + : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot + : GCDPriority.Polyglot); if (polyglotStrat is PolyglotStrategy.XenoSpendAll or PolyglotStrategy.XenoHold1 or PolyglotStrategy.XenoHold2 @@ -676,13 +683,17 @@ or PolyglotStrategy.XenoHold2 QueueGCD(BestXenoglossy, TargetChoice(polyglot) ?? primaryTarget, polyglotStrat is PolyglotStrategy.ForceXeno ? GCDPriority.ForcedGCD - : Polyglots == MaxPolyglots && EnochianTimer < 5000 ? GCDPriority.NeedPolyglot - : GCDPriority.Paradox); + : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot + : GCDPriority.Polyglot); if (polyglotStrat is PolyglotStrategy.FoulSpendAll or PolyglotStrategy.FoulHold1 or PolyglotStrategy.FoulHold2 or PolyglotStrategy.FoulHold3) - QueueGCD(AID.Foul, TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget, polyglotStrat is PolyglotStrategy.ForceFoul ? GCDPriority.ForcedGCD : Polyglots == MaxPolyglots && EnochianTimer < 5000 ? GCDPriority.NeedPolyglot : GCDPriority.Paradox); //Queue Foul + QueueGCD(AID.Foul, + TargetChoice(polyglot) ?? primaryTarget ?? BestAOETarget, + polyglotStrat is PolyglotStrategy.ForceFoul ? GCDPriority.ForcedGCD + : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot + : GCDPriority.Polyglot); } //LeyLines if (ShouldUseLeyLines(primaryTarget, llStrat)) @@ -796,6 +807,9 @@ public bool QueueAction(AID aid, Actor? target, float priority, float delay) Hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, priority, delay: delay, targetPos: targetPos); return true; } + #endregion + + #region Rotation Helpers private void BestRotation(Actor? target) //Best rotation based on targets nearby { if (ShouldUseAOE) @@ -807,697 +821,480 @@ private void BestRotation(Actor? target) //Best rotation based on targets nearby BestST(target); } } - #endregion - - #region Single-Target Helpers - private void STLv1toLv34(Actor? target) //Level 1-34 single-target rotation - { - //Fire - if (Unlocked(AID.Fire1) && //if Fire is unlocked - NoStance && MP >= 800 || //if no stance is active and MP is 800 or more - InAstralFire && MP >= 1600) //or if Astral Fire is active and MP is 1600 or more - QueueGCD(AID.Fire1, target, GCDPriority.Standard); //Queue Fire - //Ice - //TODO: Fix Blizzard I still casting once after at 10000MP due to MP tick not counting fast enough before next cast - if (InUmbralIce && MP < 9500) //if Umbral Ice is active and MP is not max - QueueGCD(AID.Blizzard1, target, GCDPriority.Standard); //Queue Blizzard - //Transpose - if (ActionReady(AID.Transpose) && //if Transpose is unlocked & off cooldown - InAstralFire && MP < 1600 || //if Astral Fire is active and MP is less than 1600 - InUmbralIce && MP == 10000) //or if Umbral Ice is active and MP is max - QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); //Queue Transpose - } - private void STLv35toLv59(Actor? target) //Level 35-59 single-target rotation + private void BestST(Actor? target) //Single-target rotation based on level { - if (NoStance) //if no stance is active + if (In25y(target)) { - if (Unlocked(AID.Blizzard3)) //if Blizzard III is unlocked + if (NoStance) //if no stance is active { - if (MP >= 10000) //if no stance is active and MP is max (opener) - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III - if (MP < 10000 && Player.InCombat) //or if in combat and no stance is active and MP is less than max (died or stopped attacking) + if (Unlocked(AID.Blizzard3)) //if Blizzard III is unlocked { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); //Queue Swiftcast->Blizzard III - else + if (MP >= 10000) //if no stance is active and MP is max (opener) + QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III + if (MP < 10000 && Player.InCombat) //or if in combat and no stance is active and MP is less than max (died or stopped attacking) QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III } } - } - if (InUmbralIce) //if Umbral Ice is active - { - //Step 1 - max stacks in UI - if (JustUsed(AID.Blizzard3, 5)) //if Blizzard III was just used + if (!Unlocked(AID.Blizzard3) || Player.Level is >= 1 and <= 34) { - if (!Unlocked(AID.Blizzard4) && UmbralStacks == 3) //if Blizzard IV is not unlocked and Umbral Ice stacks are max - QueueGCD(AID.Blizzard1, target, GCDPriority.Step2); //Queue Blizzard I - if (Unlocked(AID.Blizzard4) && UmbralHearts != MaxUmbralHearts) //if Blizzard IV is unlocked and Umbral Hearts are not max - QueueGCD(AID.Blizzard4, target, GCDPriority.Step2); //Queue Blizzard IV + //Fire + if (Unlocked(AID.Fire1) && //if Fire is unlocked + NoStance && MP >= 800 || //if no stance is active and MP is 800 or more + InAstralFire && MP >= 1600) //or if Astral Fire is active and MP is 1600 or more + QueueGCD(AID.Fire1, target, GCDPriority.Standard); //Queue Fire + //Ice + //TODO: Fix Blizzard I still casting once after at 10000MP due to MP tick not counting fast enough before next cast + if (InUmbralIce && MP < 9500) //if Umbral Ice is active and MP is not max + QueueGCD(AID.Blizzard1, target, GCDPriority.Standard); //Queue Blizzard + //Transpose + if (ActionReady(AID.Transpose) && //if Transpose is unlocked & off cooldown + InAstralFire && MP < 1600 || //if Astral Fire is active and MP is less than 1600 + InUmbralIce && MP == 10000) //or if Umbral Ice is active and MP is max + QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); //Queue Transpose + } - //Step 2 - swap from UI to AF - if (Unlocked(AID.Fire3) && //if Fire III is unlocked - JustUsed(AID.Blizzard1, 5) && //and Blizzard I was just used - MP < 10000 && //and MP is less than max - Unlocked(TraitID.UmbralHeart) ? UmbralHearts == MaxUmbralHearts : UmbralHearts == 0) //and Umbral Hearts are max if unlocked, or 0 if not - QueueGCD(AID.Fire3, target, JustUsed(AID.Blizzard1, 5) ? GCDPriority.Step10 : GCDPriority.Step1); //Queue Fire III, increase priority if Blizzard I was just used - } - if (InAstralFire) //if Astral Fire is active - { - //Step 1 - Fire 1 - if (MP >= 1600) //if MP is 1600 or more - QueueGCD(AID.Fire1, target, GCDPriority.Step3); //Queue Fire I - //Step 2B - F3P - if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 - AstralStacks == 3) //and Umbral Hearts are 0 - QueueGCD(AID.Fire3, target, GCDPriority.Step10); //Queue Fire III (AF3 F3P) - //Step 3 - swap from AF to UI - if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked - MP < 1600) //and MP is less than 400 - QueueGCD(AID.Blizzard3, target, GCDPriority.Step1); //Queue Blizzard III - } - } - private void STLv60toLv71(Actor? target) //Level 60-71 single-target rotation - { - if (NoStance) //if no stance is active - { - if (Unlocked(AID.Blizzard3)) //if Blizzard III is unlocked + if (!Unlocked(AID.Fire4) || Player.Level is >= 35 and <= 59) { - if (MP >= 10000) //if no stance is active and MP is max (opener) - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III - if (MP < 10000 && Player.InCombat) //or if in combat and no stance is active and MP is less than max (died or stopped attacking) + if (InUmbralIce) //if Umbral Ice is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); //Queue Swiftcast->Blizzard III - else - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III + //Step 1 - max stacks in UI + if (JustUsed(AID.Blizzard3, 5)) //if Blizzard III was just used + { + if (!Unlocked(AID.Blizzard4) && UmbralStacks == 3) //if Blizzard IV is not unlocked and Umbral Ice stacks are max + QueueGCD(AID.Blizzard1, target, GCDPriority.FirstStep); //Queue Blizzard I + if (Unlocked(AID.Blizzard4) && UmbralHearts != MaxUmbralHearts) //if Blizzard IV is unlocked and Umbral Hearts are not max + QueueGCD(AID.Blizzard4, target, GCDPriority.FirstStep); //Queue Blizzard IV + } + //Step 2 - swap from UI to AF + if (Unlocked(AID.Fire3) && //if Fire III is unlocked + JustUsed(AID.Blizzard1, 5) && //and Blizzard I was just used + MP < 10000 && //and MP is less than max + Unlocked(TraitID.UmbralHeart) ? UmbralHearts == MaxUmbralHearts : UmbralHearts == 0) //and Umbral Hearts are max if unlocked, or 0 if not + QueueGCD(AID.Fire3, target, JustUsed(AID.Blizzard1, 5) ? GCDPriority.ForcedStep : GCDPriority.SecondStep); //Queue Fire III, increase priority if Blizzard I was just used } - } - } - if (InUmbralIce) //if Umbral Ice is active - { - //Step 1 - max stacks in UI - if (Unlocked(AID.Blizzard4) && //if Blizzard IV is unlocked - JustUsed(AID.Blizzard3, 5) || UmbralHearts != MaxUmbralHearts) //and Blizzard III was just used or Umbral Hearts are not max - QueueGCD(AID.Blizzard4, target, GCDPriority.Step2); //Queue Blizzard IV - //Step 2 - swap from UI to AF - if (Unlocked(AID.Fire3) && //if Fire III is unlocked - UmbralHearts == MaxUmbralHearts) //and Umbral Hearts are max - QueueGCD(AID.Fire3, target, GCDPriority.Step1); //Queue Fire III - } - if (InAstralFire) //if Astral Fire is active - { - //Step 1-3, 5-7 - Fire IV - if (MP >= 1600) //and MP is 1600 or more - QueueGCD(AID.Fire4, target, GCDPriority.Step5); //Queue Fire IV - //Step 4A - Fire 1 - if (ElementTimer <= (SpS * 3) && //if time remaining on current element is less than 3x GCDs - MP >= 4000) //and MP is 4000 or more - QueueGCD(AID.Fire1, target, ElementTimer <= 5 && MP >= 4000 ? GCDPriority.Paradox : GCDPriority.Step4); //Queue Fire I, increase priority if less than 3s left on element - //Step 4B - F3P - if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 - AstralStacks == 3) //and Umbral Hearts are 0 - QueueGCD(AID.Fire3, target, GCDPriority.Step10); //Queue Fire III (AF3 F3P) - //Step 8 - swap from AF to UI - if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked - MP < 1600) //and MP is less than 400 - QueueGCD(AID.Blizzard3, target, GCDPriority.Step1); //Queue Blizzard III - } - } - private void STLv72toLv89(Actor? target) //Level 72-89 single-target rotation - { - if (NoStance) //if no stance is active - { - if (Unlocked(AID.Blizzard3)) //if Blizzard III is unlocked - { - if (MP >= 10000) //if no stance is active and MP is max (opener) - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III - if (MP < 10000 && Player.InCombat) //or if in combat and no stance is active and MP is less than max (died or stopped attacking) + if (InAstralFire) //if Astral Fire is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); //Queue Swiftcast->Blizzard III - else - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III + //Step 1 - Fire 1 + if (MP >= 1600) //if MP is 1600 or more + QueueGCD(AID.Fire1, target, GCDPriority.FirstStep); //Queue Fire I + //Step 2B - F3P + if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 + AstralStacks == 3) //and Umbral Hearts are 0 + QueueGCD(AID.Fire3, target, GCDPriority.ForcedStep); //Queue Fire III (AF3 F3P) + //Step 3 - swap from AF to UI + if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked + MP < 1600) //and MP is less than 400 + QueueGCD(AID.Blizzard3, target, GCDPriority.SecondStep); //Queue Blizzard III } } - } - if (InUmbralIce) //if Umbral Ice is active - { - //Step 1 - max stacks in UI - if (Unlocked(AID.Blizzard4) && //if Blizzard IV is unlocked - JustUsed(AID.Blizzard3, 5) || UmbralHearts != MaxUmbralHearts) //and Blizzard III was just used or Umbral Hearts are not max - QueueGCD(AID.Blizzard4, target, GCDPriority.Step2); //Queue Blizzard IV - //Step 2 - swap from UI to AF - if (Unlocked(AID.Fire3) && //if Fire III is unlocked - UmbralHearts == MaxUmbralHearts) //and Umbral Hearts are max - QueueGCD(AID.Fire3, target, GCDPriority.Step1); //Queue Fire III - } - if (InAstralFire) //if Astral Fire is active - { - //Step 1-3, 5-7 - Fire IV - if (MP >= 1600) //and MP is 1600 or more - QueueGCD(AID.Fire4, target, GCDPriority.Step5); //Queue Fire IV - //Step 4A - Fire 1 - if (ElementTimer <= (SpS * 3) && //if time remaining on current element is less than 3x GCDs - MP >= 4000) //and MP is 4000 or more - QueueGCD(AID.Fire1, target, ElementTimer <= 5 && MP >= 4000 ? GCDPriority.Paradox : GCDPriority.Step4); //Queue Fire I, increase priority if less than 3s left on element - //Step 4B - F3P - if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 - AstralStacks == 3) //and Umbral Hearts are 0 - QueueGCD(AID.Fire3, target, GCDPriority.Step10); //Queue Fire III (AF3 F3P) - //Step 8 - Despair - if (MP is < 1600 and not 0 && //if MP is less than 1600 and not 0 - Unlocked(AID.Despair)) //and Despair is unlocked - { - if (ActionReady(AID.Swiftcast) && ElementTimer < GetCastTime(AID.Despair)) - QueueGCD(AID.Swiftcast, target, GCDPriority.Step2); //Queue Swiftcast->Despair - else - QueueGCD(AID.Despair, target, GCDPriority.Step2); //Queue Despair - } - //Step 9 - swap from AF to UI - if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked - MP <= 400) //and MP is less than 400 - QueueGCD(AID.Blizzard3, target, GCDPriority.Step1); //Queue Blizzard III - } - } - private void STLv90toLv99(Actor? target) //Level 90-99 single-target rotation - { - if (NoStance) //if no stance is active - { - if (Unlocked(AID.Blizzard3)) //if Blizzard III is unlocked + if (!Unlocked(AID.Despair) || Player.Level is >= 60 and <= 71) { - if (MP >= 10000) //if no stance is active and MP is max (opener) - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III - if (MP < 10000 && Player.InCombat) //or if in combat and no stance is active and MP is less than max (died or stopped attacking) + if (InUmbralIce) //if Umbral Ice is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); //Queue Swiftcast->Blizzard III - else - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III + //Step 1 - max stacks in UI + if (Unlocked(AID.Blizzard4) && //if Blizzard IV is unlocked + JustUsed(AID.Blizzard3, 5) || UmbralHearts != MaxUmbralHearts) //and Blizzard III was just used or Umbral Hearts are not max + QueueGCD(AID.Blizzard4, target, GCDPriority.FirstStep); //Queue Blizzard IV + //Step 2 - swap from UI to AF + if (Unlocked(AID.Fire3) && //if Fire III is unlocked + UmbralHearts == MaxUmbralHearts) //and Umbral Hearts are max + QueueGCD(AID.Fire3, target, GCDPriority.SecondStep); //Queue Fire III } - } - } - if (InUmbralIce) //if Umbral Ice is active - { - //Step 1 - max stacks in UI - if (Unlocked(AID.Blizzard4) && //if Blizzard IV is unlocked - JustUsed(AID.Blizzard3, 5) || UmbralHearts != MaxUmbralHearts) //and Blizzard III was just used or Umbral Hearts are not max - QueueGCD(AID.Blizzard4, target, GCDPriority.Step3); //Queue Blizzard IV - //Step 2 - Ice Paradox - if (canParadox && //if Paradox is unlocked and Paradox is active - JustUsed(AID.Blizzard4, 5)) //and Blizzard IV was just used - QueueGCD(AID.Paradox, target, GCDPriority.Step2); //Queue Paradox - //Step 2 - swap from UI to AF - if (Unlocked(AID.Fire3) && //if Fire III is unlocked - UmbralHearts == MaxUmbralHearts) //and Umbral Hearts are max - QueueGCD(AID.Fire3, target, GCDPriority.Step1); //Queue Fire III - } - if (InAstralFire) //if Astral Fire is active - { - //Step 1-4, 6 & 7 - Fire IV - if (MP >= 1600) //and MP is 1600 or more - QueueGCD(AID.Fire4, target, GCDPriority.Step5); //Queue Fire IV - //Step 5A - Paradox - if (canParadox && //if Paradox is unlocked and Paradox is active - ElementTimer < (SpS * 3) && //and time remaining on current element is less than 3x GCDs - MP >= 1600) //and MP is 1600 or more - QueueGCD(AID.Paradox, target, ElementTimer <= 3 ? GCDPriority.Paradox : GCDPriority.Step4); //Queue Paradox, increase priority if less than 3s left on element - //Step 4B - F3P - if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 - AstralStacks == 3) //and Umbral Hearts are 0 - QueueGCD(AID.Fire3, target, GCDPriority.Step10); //Queue Fire III (AF3 F3P) - //Step 8 - Despair - if (MP is < 1600 and not 0 && //if MP is less than 1600 and not 0 - Unlocked(AID.Despair)) //and Despair is unlocked - { - if (ActionReady(AID.Swiftcast) && ElementTimer < GetCastTime(AID.Despair)) - QueueGCD(AID.Swiftcast, target, GCDPriority.Step2); //Queue Swiftcast->Despair - else - QueueGCD(AID.Despair, target, GCDPriority.Step2); //Queue Despair - } - //Step 9 - swap from AF to UI - if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked - MP <= 400) //and MP is less than 400 - QueueGCD(AID.Blizzard3, target, GCDPriority.Step1); //Queue Blizzard III - } - } - private void STLv100(Actor? target) //Level 100 single-target rotation - { - if (NoStance) //if no stance is active - { - if (Unlocked(AID.Blizzard3)) //if Blizzard III is unlocked - { - if (MP >= 10000) //if no stance is active and MP is max (opener) - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III - if (MP < 10000 && Player.InCombat) //or if in combat and no stance is active and MP is less than max (died or stopped attacking) + if (InAstralFire) //if Astral Fire is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); //Queue Swiftcast->Blizzard III - else - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III + //Step 1-3, 5-7 - Fire IV + if (MP >= 1600) //and MP is 1600 or more + QueueGCD(AID.Fire4, target, GCDPriority.FirstStep); //Queue Fire IV + //Step 4A - Fire 1 + if (ElementTimer <= (SpS * 3) && //if time remaining on current element is less than 3x GCDs + MP >= 4000) //and MP is 4000 or more + QueueGCD(AID.Fire1, target, ElementTimer <= 5 && MP >= 4000 ? GCDPriority.Paradox : GCDPriority.SecondStep); //Queue Fire I, increase priority if less than 3s left on element + //Step 4B - F3P + if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 + AstralStacks == 3) //and Umbral Hearts are 0 + QueueGCD(AID.Fire3, target, GCDPriority.ForcedStep); //Queue Fire III (AF3 F3P) + //Step 8 - swap from AF to UI + if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked + MP < 1600) //and MP is less than 400 + QueueGCD(AID.Blizzard3, target, GCDPriority.ThirdStep); //Queue Blizzard III } } - } - if (InUmbralIce) //if Umbral Ice is active - { - //Step 1 - max stacks in UI - if (Unlocked(AID.Blizzard4) && //if Blizzard IV is unlocked - JustUsed(AID.Blizzard3, 5) || UmbralHearts != MaxUmbralHearts) //and Blizzard III was just used or Umbral Hearts are not max - QueueGCD(AID.Blizzard4, target, GCDPriority.Step3); //Queue Blizzard IV - //Step 2 - Ice Paradox - if (canParadox && //if Paradox is unlocked and Paradox is active - JustUsed(AID.Blizzard4, 5)) //and Blizzard IV was just used - QueueGCD(AID.Paradox, target, GCDPriority.Step2); //Queue Paradox - //Step 2 - swap from UI to AF - if (Unlocked(AID.Fire3) && //if Fire III is unlocked - UmbralHearts == MaxUmbralHearts) //and Umbral Hearts are max - QueueGCD(AID.Fire3, target, GCDPriority.Step1); //Queue Fire III - } - if (InAstralFire) //if Astral Fire is active - { - //Step 1-4, 6 & 7 - Fire IV - if (AstralSoulStacks != 6 && //and Astral Soul stacks are not max - MP >= 1600) //and MP is 1600 or more - QueueGCD(AID.Fire4, target, GCDPriority.Step6); //Queue Fire IV - //Step 5A - Paradox - if (ParadoxActive && //if Paradox is active - ElementTimer < (SpS * 3) && //and time remaining on current element is less than 3x GCDs - MP >= 1600) //and MP is 1600 or more - QueueGCD(AID.Paradox, target, ElementTimer <= 3 ? GCDPriority.Paradox : GCDPriority.Step5); //Queue Paradox, increase priority if less than 3s left on element - //Step 4B - F3P - if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 - AstralStacks == 3) //and Umbral Hearts are 0 - QueueGCD(AID.Fire3, target, GCDPriority.Step10); //Queue Fire III (AF3 F3P) - //Step 8 - Despair - if (MP is < 1600 and not 0 && //if MP is less than 1600 and not 0 - Unlocked(AID.Despair)) //and Despair is unlocked - QueueGCD(AID.Despair, target, GCDPriority.Step3); //Queue Despair - //Step 9 - Flare Star - if (AstralSoulStacks == 6) //if Astral Soul stacks are max + if (!Unlocked(AID.Paradox) || Player.Level is >= 72 and <= 89) { - if (JustUsed(AID.Despair, 5f) && ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, Player, GCDPriority.Step2); //Queue Swiftcast->Flare Star - QueueGCD(AID.FlareStar, target, GCDPriority.Step2); //Queue Flare Star - } - //Step 10A - skip Flare Star if we cant use it (cryge) - if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked - MP <= 400 && //and MP is less than 400 - AstralSoulStacks is < 6 and > 0) //and Astral Soul stacks are less than 6 but greater than 0 - QueueGCD(AID.Blizzard3, target, GCDPriority.Step1); //Queue Blizzard III - //Step 10B - swap from AF to UI - if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked - MP <= 400 && //and MP is less than 400 - AstralSoulStacks == 0) //and Astral Soul stacks are 0 - QueueGCD(AID.Blizzard3, target, GCDPriority.Step1); //Queue Blizzard III - } - } - private void BestST(Actor? target) //Single-target rotation based on level - { - if (Player.Level is >= 1 and <= 34) - { - STLv1toLv34(target); - } - if (Player.Level is >= 35 and <= 59) - { - STLv35toLv59(target); - } - if (Player.Level is >= 60 and <= 71) - { - STLv60toLv71(target); - } - if (Player.Level is >= 72 and <= 89) - { - STLv72toLv89(target); - } - if (Player.Level is >= 90 and <= 99) - { - STLv90toLv99(target); - } - if (Player.Level is 100) - { - STLv100(target); - } - } - #endregion - - #region AOE Helpers - private void AOELv12toLv34(Actor? target) //Level 12-34 AOE rotation - { - if (NoStance) - { - if (Unlocked(AID.Blizzard2)) - { - if (MP >= 10000) - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); - if (MP < 10000 && Player.InCombat) + if (InUmbralIce) //if Umbral Ice is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); - else - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); + //Step 1 - max stacks in UI + if (Unlocked(AID.Blizzard4) && //if Blizzard IV is unlocked + JustUsed(AID.Blizzard3, 5) || UmbralHearts != MaxUmbralHearts) //and Blizzard III was just used or Umbral Hearts are not max + QueueGCD(AID.Blizzard4, target, GCDPriority.FirstStep); //Queue Blizzard IV + //Step 2 - swap from UI to AF + if (Unlocked(AID.Fire3) && //if Fire III is unlocked + UmbralHearts == MaxUmbralHearts) //and Umbral Hearts are max + QueueGCD(AID.Fire3, target, GCDPriority.SecondStep); //Queue Fire III } - } - } - //Fire - if (Unlocked(AID.Fire2) && //if Fire is unlocked - InAstralFire && MP >= 3000) //or if Astral Fire is active and MP is 1600 or more - QueueGCD(AID.Fire2, target, GCDPriority.Standard); //Queue Fire II - //Ice - //TODO: MP tick is not fast enough before next cast, this will cause an extra unnecessary cast - if (InUmbralIce && - MP <= 9600) - QueueGCD(AID.Blizzard2, target, GCDPriority.Standard); //Queue Blizzard II - //Transpose - if (ActionReady(AID.Transpose) && //if Transpose is unlocked & off cooldown - (InAstralFire && MP < 3000 || //if Astral Fire is active and MP is less than 1600 - InUmbralIce && MP > 9600)) //or if Umbral Ice is active and MP is max - QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); //Queue Transpose - } - private void AOELv35toLv39(Actor? target) //Level 35-39 AOE rotation - { - if (NoStance) - { - if (Unlocked(AID.Blizzard2)) - { - if (MP >= 10000) - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); - if (MP < 10000 && Player.InCombat) + if (InAstralFire) //if Astral Fire is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); - else - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); + //Step 1-3, 5-7 - Fire IV + if (MP >= 1600) //and MP is 1600 or more + QueueGCD(AID.Fire4, target, GCDPriority.FirstStep); //Queue Fire IV + //Step 4A - Fire 1 + if (ElementTimer <= (GetCastTime(AID.Fire1) * 3) && //if time remaining on current element is less than 3x GCDs + MP >= 4000) //and MP is 4000 or more + QueueGCD(AID.Fire1, target, ElementTimer <= (GetCastTime(AID.Fire1) * 3) && MP >= 4000 ? GCDPriority.Paradox : GCDPriority.SecondStep); //Queue Fire I, increase priority if less than 3s left on element + //Step 4B - F3P + if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 + AstralStacks == 3) //and Umbral Hearts are 0 + QueueGCD(AID.Fire3, target, GCDPriority.ForcedStep); //Queue Fire III (AF3 F3P) + //Step 8 - Despair + if (Unlocked(AID.Despair) && //if Despair is unlocked + ((MP is < 1600 and >= 800) || //if MP is less than 1600 and not 0 + (MP is <= 4000 and >= 800 && ElementTimer <= (GetCastTime(AID.Despair) * 2)))) //or if we dont have enough time for last F4s + QueueGCD(AID.Despair, target, ElementTimer <= (GetCastTime(AID.Despair) * 2) ? GCDPriority.ForcedGCD : GCDPriority.ThirdStep); //Queue Despair + //Step 9 - swap from AF to UI + if (MP <= 400) //and MP is less than 400 + QueueGCD(AID.Blizzard3, target, GCDPriority.FourthStep); //Queue Blizzard III } } - } - if (InUmbralIce) - { - //Step 1 - max stacks in UI - //TODO: MP tick is not fast enough before next cast, this will cause an extra unnecessary cast - if (Unlocked(AID.Blizzard2) && - MP < 9600) - QueueGCD(AID.Blizzard2, target, GCDPriority.Step2); - //Step 2 - swap from UI to AF - if (Unlocked(AID.Fire2) && - MP >= 9600 && - UmbralStacks == 3) - QueueGCD(AID.Fire2, target, GCDPriority.Step1); - } - if (InAstralFire) - { - if (MP >= 3000) - QueueGCD(AID.Fire2, target, GCDPriority.Step2); - if (Unlocked(AID.Blizzard2) && - MP < 3000) - QueueGCD(AID.Blizzard2, target, GCDPriority.Step1); - } - } - private void AOELv40toLv49(Actor? target) //Level 40-49 AOE rotation - { - if (NoStance) - { - if (Unlocked(AID.Blizzard2)) + if (!Unlocked(AID.FlareStar) || Player.Level is >= 90 and <= 99) { - if (MP >= 10000) - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); - if (MP < 10000 && Player.InCombat) + if (InUmbralIce) //if Umbral Ice is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); - else - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); + //Step 1 - max stacks in UI + if (Unlocked(AID.Blizzard4) && //if Blizzard IV is unlocked + JustUsed(AID.Blizzard3, 5) || UmbralHearts != MaxUmbralHearts) //and Blizzard III was just used or Umbral Hearts are not max + QueueGCD(AID.Blizzard4, target, GCDPriority.FirstStep); //Queue Blizzard IV + //Step 2 - Ice Paradox + if (canParadox && //if Paradox is unlocked and Paradox is active + JustUsed(AID.Blizzard4, 5)) //and Blizzard IV was just used + QueueGCD(AID.Paradox, target, GCDPriority.SecondStep); //Queue Paradox + //Step 3 - swap from UI to AF + if (Unlocked(AID.Fire3) && //if Fire III is unlocked + UmbralHearts == MaxUmbralHearts) //and Umbral Hearts are max + QueueGCD(AID.Fire3, target, GCDPriority.ThirdStep); //Queue Fire III + } + if (InAstralFire) //if Astral Fire is active + { + //Step 1-4, 6 & 7 - Fire IV + if (MP >= 1600) //and MP is 1600 or more + QueueGCD(AID.Fire4, target, GCDPriority.FirstStep); //Queue Fire IV + //Step 5A - Paradox + if (canParadox && //if Paradox is unlocked and Paradox is active + ElementTimer < (SpS * 3) && //and time remaining on current element is less than 3x GCDs + MP >= 1600) //and MP is 1600 or more + QueueGCD(AID.Paradox, target, ElementTimer <= 3 ? GCDPriority.Paradox : GCDPriority.SecondStep); //Queue Paradox, increase priority if less than 3s left on element + //Step 4B - F3P + if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 + AstralStacks == 3) //and Umbral Hearts are 0 + QueueGCD(AID.Fire3, target, GCDPriority.ForcedStep); //Queue Fire III (AF3 F3P) + //Step 8 - Despair + if (Unlocked(AID.Despair) && //if Despair is unlocked + ((MP is < 1600 and >= 800) || //if MP is less than 1600 and not 0 + (MP is <= 4000 and >= 800 && ElementTimer <= (GetCastTime(AID.Despair) * 2)))) //or if we dont have enough time for last F4s + QueueGCD(AID.Despair, target, ElementTimer <= (GetCastTime(AID.Despair) * 2) ? GCDPriority.ForcedGCD : GCDPriority.ThirdStep); //Queue Despair + //Step 9 - swap from AF to UI + if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked + MP <= 400) //and MP is less than 400 + QueueGCD(AID.Blizzard3, target, GCDPriority.FourthStep); //Queue Blizzard III } } - } - if (InUmbralIce) - { - //Step 1 - max stacks in UI - if (Unlocked(AID.Blizzard2) && - UmbralStacks < 3) - QueueGCD(AID.Blizzard2, target, GCDPriority.Step3); - //Step 2 - Freeze - if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && - (JustUsed(AID.Blizzard2, 5) || MP < 10000)) - QueueGCD(AID.Freeze, target, GCDPriority.Step2); - //Step 3 - swap from UI to AF - if (Unlocked(AID.Fire2) && - MP >= 10000 && - UmbralStacks == 3) - QueueGCD(AID.Fire2, target, GCDPriority.Step1); - } - if (InAstralFire) - { - if (MP >= 3000) - QueueGCD(AID.Fire2, target, GCDPriority.Step2); - if (Unlocked(AID.Blizzard2) && - MP < 3000) - QueueGCD(AID.Blizzard2, target, GCDPriority.Step1); - } - } - private void AOELv50toLv57(Actor? target) //Level 50-57 AOE rotation - { - if (NoStance) - { - if (Unlocked(AID.Blizzard2)) + if (!Unlocked(AID.FlareStar) || Player.Level is 100) { - if (MP >= 10000) - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); - if (MP < 10000 && Player.InCombat) + if (InUmbralIce) //if Umbral Ice is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); - else - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); + //Step 1 - max stacks in UI + if (Unlocked(AID.Blizzard4) && //if Blizzard IV is unlocked + JustUsed(AID.Blizzard3, 5) || UmbralHearts != MaxUmbralHearts) //and Blizzard III was just used or Umbral Hearts are not max + QueueGCD(AID.Blizzard4, target, GCDPriority.FirstStep); //Queue Blizzard IV + //Step 2 - Ice Paradox + if (canParadox && //if Paradox is unlocked and Paradox is active + JustUsed(AID.Blizzard4, 5)) //and Blizzard IV was just used + QueueGCD(AID.Paradox, target, GCDPriority.SecondStep); //Queue Paradox + //Step 3 - swap from UI to AF + if (Unlocked(AID.Fire3) && //if Fire III is unlocked + UmbralHearts == MaxUmbralHearts) //and Umbral Hearts are max + QueueGCD(AID.Fire3, target, GCDPriority.ThirdStep); //Queue Fire III + } + if (InAstralFire) //if Astral Fire is active + { + //Step 1-4, 6 & 7 - Fire IV + if (AstralSoulStacks != 6 && //and Astral Soul stacks are not max + MP >= 1600) //and MP is 1600 or more + QueueGCD(AID.Fire4, target, GCDPriority.FirstStep); //Queue Fire IV + //Step 5A - Paradox + if (ParadoxActive && //if Paradox is active + ElementTimer < (SpS * 3) && //and time remaining on current element is less than 3x GCDs + MP >= 1600) //and MP is 1600 or more + QueueGCD(AID.Paradox, target, ElementTimer <= 3 ? GCDPriority.Paradox : GCDPriority.SecondStep); //Queue Paradox, increase priority if less than 3s left on element + //Step 4B - F3P + if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 + AstralStacks == 3) //and Umbral Hearts are 0 + QueueGCD(AID.Fire3, target, GCDPriority.ForcedStep); //Queue Fire III (AF3 F3P) + //Step 8 - Despair + if (MP is < 1600 and not 0 && //if MP is less than 1600 and not 0 + Unlocked(AID.Despair)) //and Despair is unlocked + QueueGCD(AID.Despair, target, GCDPriority.ThirdStep); //Queue Despair + //Step 9 - Flare Star + if (AstralSoulStacks == 6) //if Astral Soul stacks are max + QueueGCD(AID.FlareStar, target, GCDPriority.FourthStep); //Queue Flare Star + //Step 10A - skip Flare Star if we cant use it (cryge) + if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked + MP <= 400 && //and MP is less than 400 + AstralSoulStacks is < 6 and > 0) //and Astral Soul stacks are less than 6 but greater than 0 + QueueGCD(AID.Blizzard3, target, GCDPriority.FifthStep); //Queue Blizzard III + //Step 10B - swap from AF to UI + if (Unlocked(AID.Blizzard3) && //if Blizzard III is unlocked + MP <= 400 && //and MP is less than 400 + AstralSoulStacks == 0) //and Astral Soul stacks are 0 + QueueGCD(AID.Blizzard3, target, GCDPriority.FifthStep); //Queue Blizzard III } } } - if (InUmbralIce) - { - //Step 1 - max stacks in UI - if (Unlocked(AID.Blizzard2) && - UmbralStacks < 3) - QueueGCD(AID.Blizzard2, target, GCDPriority.Step3); - //Step 2 - Freeze - if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && - (JustUsed(AID.Blizzard2, 5) || MP < 10000)) - QueueGCD(AID.Freeze, target, GCDPriority.Step2); - //Step 3 - swap from UI to AF - if (Unlocked(AID.Fire2) && - MP >= 10000 && - UmbralStacks == 3) - QueueGCD(AID.Fire2, target, GCDPriority.Step1); - } - if (InAstralFire) - { - //Step 1 - spam Fire 2 - if (MP >= 3000) - QueueGCD(AID.Fire2, target, GCDPriority.Step3); - //Step 2 - Flare - if (Unlocked(AID.Flare) && - MP < 3000) - QueueGCD(AID.Flare, target, GCDPriority.Step2); - //Step 3 - swap from AF to UI - if (Unlocked(AID.Blizzard2) && - (!Unlocked(AID.Flare) && MP < 3000) || //do your job quests, fool - (Unlocked(AID.Flare) && MP < 400)) - QueueGCD(AID.Blizzard2, target, MP < 400 ? GCDPriority.Step10 : GCDPriority.Step1); - } } - private void AOELv58toLv81(Actor? target) //Level 58-81 AOE rotation + private void BestAOE(Actor? target) //AOE rotation based on level { - if (NoStance) + if (In25y(target)) { - if (Unlocked(AID.Blizzard2)) + if (NoStance) { - if (MP >= 10000) - QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); - if (MP < 10000 && Player.InCombat) + if (Unlocked(AID.Blizzard2) && !Unlocked(AID.HighBlizzard2)) { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); - else + if (MP >= 10000) + QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); + if (MP < 10000 && Player.InCombat) QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); } - } - } - if (InUmbralIce) - { - //Step 1 - max stacks in UI - if (Unlocked(AID.Blizzard2) && - UmbralStacks < 3) - QueueGCD(AID.Blizzard2, target, GCDPriority.Step3); - //Step 2 - Freeze - if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && - (JustUsed(AID.Blizzard2, 5) || MP < 10000)) - QueueGCD(AID.Freeze, target, GCDPriority.Step2); - //Step 3 - swap from UI to AF - if (Unlocked(AID.Fire2) && - MP >= 10000 && - UmbralStacks == 3) - QueueGCD(AID.Fire2, target, GCDPriority.Step1); - } - if (InAstralFire) - { - //Step 1 - spam Fire 2 - if (UmbralHearts > 1) - QueueGCD(AID.Fire2, target, GCDPriority.Step4); - //Step 2 - Flare - if (Unlocked(AID.Flare)) - { - //first cast - if (UmbralHearts == 1) - QueueGCD(AID.Flare, target, GCDPriority.Step3); - //second cast - if (UmbralHearts == 0 && - MP >= 800) - QueueGCD(AID.Flare, target, GCDPriority.Step2); - } - //Step 3 - swap from AF to UI - if (Unlocked(AID.Blizzard2) && - MP < 400) - QueueGCD(AID.Blizzard2, target, GCDPriority.Step1); - } - } - private void AOELv82toLv99(Actor? target) //Level 82-99 AOE rotation - { - if (NoStance) - { - if (Unlocked(AID.HighBlizzard2)) - { - if (MP >= 10000) - QueueGCD(AID.HighBlizzard2, target, GCDPriority.NeedB3); - if (MP < 10000 && Player.InCombat) + if (Unlocked(AID.HighBlizzard2)) { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); - else + if (MP >= 10000) + QueueGCD(AID.HighBlizzard2, target, GCDPriority.NeedB3); + if (MP < 10000 && Player.InCombat) QueueGCD(AID.HighBlizzard2, target, GCDPriority.NeedB3); } } - } - if (InUmbralIce) - { - //Step 1 - max stacks in UI - if (Unlocked(AID.HighBlizzard2) && - UmbralStacks < 3) - QueueGCD(AID.HighBlizzard2, target, GCDPriority.Step3); - //Step 2 - Freeze - if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && - (JustUsed(AID.HighBlizzard2, 5) || MP < 10000)) - QueueGCD(AID.Freeze, target, GCDPriority.Step2); - //Step 3 - swap from UI to AF - if (Unlocked(AID.HighFire2) && - MP >= 10000 && - UmbralStacks == 3) - QueueGCD(AID.HighFire2, target, GCDPriority.Step1); - } - if (InAstralFire) - { - //Step 1 - spam Fire 2 - if (MP > 5500) - QueueGCD(AID.HighFire2, target, GCDPriority.Step4); - //Step 2 - Flare - if (Unlocked(AID.Flare)) + if (!Unlocked(AID.Blizzard3) || Player.Level is >= 12 and <= 35) { - //first cast - if (UmbralHearts == 1) - QueueGCD(AID.Flare, target, GCDPriority.Step3); - //second cast - if (UmbralHearts == 0 && - MP >= 800) - QueueGCD(AID.Flare, target, GCDPriority.Step2); + //Fire + if (Unlocked(AID.Fire2) && //if Fire is unlocked + InAstralFire && MP >= 3000) //or if Astral Fire is active and MP is 1600 or more + QueueGCD(AID.Fire2, target, GCDPriority.Standard); //Queue Fire II + //Ice + //TODO: MP tick is not fast enough before next cast, this will cause an extra unnecessary cast + if (InUmbralIce && + MP <= 9600) + QueueGCD(AID.Blizzard2, target, GCDPriority.Standard); //Queue Blizzard II + //Transpose + if (ActionReady(AID.Transpose) && //if Transpose is unlocked & off cooldown + (InAstralFire && MP < 3000 || //if Astral Fire is active and MP is less than 1600 + InUmbralIce && MP > 9600)) //or if Umbral Ice is active and MP is max + QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); //Queue Transpose + //if in AF but no F2 yet, TP back to UI for B2 spam + if (InAstralFire && !Unlocked(AID.Fire2)) + QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); } - //Step 3 - swap from AF to UI - if (Unlocked(AID.HighBlizzard2) && - MP < 400) - QueueGCD(AID.HighBlizzard2, target, GCDPriority.Step1); - } - } - private void AOELv100(Actor? target) //Level 100 AOE rotation - { - if (NoStance) - { - if (Unlocked(AID.HighBlizzard2)) + if (!Unlocked(AID.Freeze) || Player.Level is >= 35 and <= 39) { - if (MP >= 10000) - QueueGCD(AID.HighBlizzard2, target, GCDPriority.NeedB3); - if (MP < 10000 && Player.InCombat) + if (InUmbralIce) { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); - else - QueueGCD(AID.HighBlizzard2, target, GCDPriority.NeedB3); + //Step 1 - max stacks in UI + //TODO: MP tick is not fast enough before next cast, this will cause an extra unnecessary cast + if (Unlocked(AID.Blizzard2) && + MP < 9600) + QueueGCD(AID.Blizzard2, target, GCDPriority.FirstStep); + //Step 2 - swap from UI to AF + if (Unlocked(AID.Fire2) && + MP >= 9600 && + UmbralStacks == 3) + QueueGCD(AID.Fire2, target, GCDPriority.SecondStep); + } + if (InAstralFire) + { + if (MP >= 3000) + QueueGCD(AID.Fire2, target, GCDPriority.FirstStep); + if (Unlocked(AID.Blizzard2) && + MP < 3000) + QueueGCD(AID.Blizzard2, target, GCDPriority.SecondStep); } } - } - if (InUmbralIce) - { - //Step 1 - max stacks in UI - if (Unlocked(AID.HighBlizzard2) && - UmbralStacks < 3) - QueueGCD(AID.HighBlizzard2, target, GCDPriority.Step3); - //Step 2 - Freeze - if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && - (JustUsed(AID.HighBlizzard2, 5) || MP < 10000)) - QueueGCD(AID.Freeze, target, GCDPriority.Step2); - //Step 3 - swap from UI to AF - if (Unlocked(AID.HighFire2) && - MP >= 10000 && - UmbralStacks == 3) - QueueGCD(AID.HighFire2, target, GCDPriority.Step1); - } - if (InAstralFire) - { - //Step 1 - Flare - if (Unlocked(AID.Flare)) - { - //first cast - if (UmbralHearts == 1) - QueueGCD(AID.Flare, target, GCDPriority.Step3); - //second cast - if (UmbralHearts == 0 && - MP >= 800) - QueueGCD(AID.Flare, target, GCDPriority.Step2); - } - //Step 2 - Flare Star - if (AstralSoulStacks == 6) //if Astral Soul stacks are max - QueueGCD(AID.FlareStar, target, GCDPriority.Step2); //Queue Flare Star - //Step 3 - swap from AF to UI - if (Unlocked(AID.HighBlizzard2) && - MP < 400) - QueueGCD(AID.HighBlizzard2, target, GCDPriority.Step1); - } - } - private void BestAOE(Actor? target) //AOE rotation based on level - { - if (In25y(target)) - { - if (Player.Level is >= 12 and <= 34) - { - AOELv12toLv34(target); - } - if (Player.Level is >= 35 and <= 39) - { - AOELv35toLv39(target); - } - if (Player.Level is >= 40 and <= 49) + if (!Unlocked(AID.Flare) || Player.Level is >= 40 and <= 49) { - AOELv40toLv49(target); + if (InUmbralIce) + { + //Step 1 - max stacks in UI + if (Unlocked(AID.Blizzard2) && + UmbralStacks < 3) + QueueGCD(AID.Blizzard2, target, GCDPriority.FirstStep); + //Step 2 - Freeze + if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && + (JustUsed(AID.Blizzard2, 5) || MP < 10000)) + QueueGCD(AID.Freeze, target, GCDPriority.SecondStep); + //Step 3 - swap from UI to AF + if (Unlocked(AID.Fire2) && + MP >= 10000 && + UmbralStacks == 3) + QueueGCD(AID.Fire2, target, GCDPriority.ThirdStep); + } + if (InAstralFire) + { + if (MP >= 3000) + QueueGCD(AID.Fire2, target, GCDPriority.FirstStep); + if (Unlocked(AID.Blizzard2) && + MP < 3000) + QueueGCD(AID.Blizzard2, target, GCDPriority.SecondStep); + } } - if (Player.Level is >= 50 and <= 57) + if (!Unlocked(AID.Blizzard4) || Player.Level is >= 50 and <= 57) { - AOELv50toLv57(target); + if (InUmbralIce) + { + //Step 1 - max stacks in UI + if (Unlocked(AID.Blizzard2) && + UmbralStacks < 3) + QueueGCD(AID.Blizzard2, target, GCDPriority.FirstStep); + //Step 2 - Freeze + if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && + (JustUsed(AID.Blizzard2, 5) || MP < 10000)) + QueueGCD(AID.Freeze, target, GCDPriority.SecondStep); + //Step 3 - swap from UI to AF + if (Unlocked(AID.Fire2) && + MP >= 10000 && + UmbralStacks == 3) + QueueGCD(AID.Fire2, target, GCDPriority.ThirdStep); + } + if (InAstralFire) + { + //Step 1 - spam Fire 2 + if (MP >= 3000) + QueueGCD(AID.Fire2, target, GCDPriority.FirstStep); + //Step 2 - Flare + if (Unlocked(AID.Flare) && + MP < 3000) + QueueGCD(AID.Flare, target, GCDPriority.SecondStep); + //Step 3 - swap from AF to UI + if (Unlocked(AID.Blizzard2) && + (!Unlocked(AID.Flare) && MP < 3000) || //do your job quests, fool + (Unlocked(AID.Flare) && MP < 400)) + QueueGCD(AID.Blizzard2, target, MP < 400 ? GCDPriority.ForcedStep : GCDPriority.ThirdStep); + } } - if (Player.Level is >= 58 and <= 81) + if (!Unlocked(AID.HighBlizzard2) || Player.Level is >= 58 and <= 81) { - AOELv58toLv81(target); + if (InUmbralIce) + { + //Step 1 - max stacks in UI + if (Unlocked(AID.Blizzard2) && + UmbralStacks < 3) + QueueGCD(AID.Blizzard2, target, GCDPriority.FirstStep); + //Step 2 - Freeze + if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && + (JustUsed(AID.Blizzard2, 5) || MP < 10000)) + QueueGCD(AID.Freeze, target, GCDPriority.SecondStep); + //Step 3 - swap from UI to AF + if (Unlocked(AID.Fire2) && + MP >= 10000 && + UmbralStacks == 3) + QueueGCD(AID.Fire2, target, GCDPriority.ThirdStep); + } + if (InAstralFire) + { + //Step 1 - spam Fire 2 + if (UmbralHearts > 1) + QueueGCD(AID.Fire2, target, GCDPriority.FirstStep); + //Step 2 - Flare + if (Unlocked(AID.Flare)) + { + //first cast + if (UmbralHearts == 1) + QueueGCD(AID.Flare, target, GCDPriority.SecondStep); + //second cast + if (UmbralHearts == 0 && + MP >= 800) + QueueGCD(AID.Flare, target, GCDPriority.ThirdStep); + } + //Step 3 - swap from AF to UI + if (Unlocked(AID.Blizzard2) && + MP < 400) + QueueGCD(AID.Blizzard2, target, GCDPriority.FourthStep); + } } - if (Player.Level is >= 82 and <= 99) + if (!Unlocked(AID.FlareStar) || Player.Level is >= 82 and <= 99) { - AOELv82toLv99(target); + if (InUmbralIce) + { + //Step 1 - max stacks in UI + if (Unlocked(AID.HighBlizzard2) && + UmbralStacks < 3) + QueueGCD(AID.HighBlizzard2, target, GCDPriority.FirstStep); + //Step 2 - Freeze + if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && + (JustUsed(AID.HighBlizzard2, 5) || MP < 10000)) + QueueGCD(AID.Freeze, target, GCDPriority.SecondStep); + //Step 3 - swap from UI to AF + if (Unlocked(AID.HighFire2) && + MP >= 10000 && + UmbralStacks == 3) + QueueGCD(AID.HighFire2, target, GCDPriority.ThirdStep); + } + if (InAstralFire) + { + //Step 1 - spam Fire 2 + if (MP > 5500) + QueueGCD(AID.HighFire2, target, GCDPriority.FirstStep); + //Step 2 - Flare + if (Unlocked(AID.Flare)) + { + //first cast + if (UmbralHearts == 1) + QueueGCD(AID.Flare, target, GCDPriority.SecondStep); + //second cast + if (UmbralHearts == 0 && + MP >= 800) + QueueGCD(AID.Flare, target, GCDPriority.ThirdStep); + } + //Step 3 - swap from AF to UI + if (Unlocked(AID.HighBlizzard2) && + MP < 400) + QueueGCD(AID.HighBlizzard2, target, GCDPriority.ThirdStep); + } } - if (Player.Level is 100) + if (Unlocked(AID.FlareStar) || Player.Level is 100) { - AOELv100(target); + if (InUmbralIce) + { + //Step 1 - max stacks in UI + if (Unlocked(AID.HighBlizzard2) && + UmbralStacks < 3) + QueueGCD(AID.HighBlizzard2, target, GCDPriority.FirstStep); + //Step 2 - Freeze + if (Unlocked(AID.Freeze) && !JustUsed(AID.Freeze, 5f) && + (JustUsed(AID.HighBlizzard2, 5) || MP < 10000)) + QueueGCD(AID.Freeze, target, GCDPriority.SecondStep); + //Step 3 - swap from UI to AF + if (Unlocked(AID.HighFire2) && + MP >= 10000 && + UmbralStacks == 3) + QueueGCD(AID.HighFire2, target, GCDPriority.ThirdStep); + } + if (InAstralFire) + { + //Step 1 - Flare + if (Unlocked(AID.Flare)) + { + //first cast + if (UmbralHearts == 1) + QueueGCD(AID.Flare, target, GCDPriority.FirstStep); + //second cast + if (UmbralHearts == 0 && + MP >= 800) + QueueGCD(AID.Flare, target, GCDPriority.SecondStep); + } + //Step 2 - Flare Star + if (AstralSoulStacks == 6) //if Astral Soul stacks are max + QueueGCD(AID.FlareStar, target, GCDPriority.ThirdStep); //Queue Flare Star + //Step 3 - swap from AF to UI + if (Unlocked(AID.HighBlizzard2) && + MP < 400) + QueueGCD(AID.HighBlizzard2, target, GCDPriority.FourthStep); + } } } } From f7e711c9b7ccf924311e5c926a255e74ec1fff0c Mon Sep 17 00:00:00 2001 From: AceAkechi123 Date: Sun, 19 Jan 2025 05:25:36 -0800 Subject: [PATCH 02/33] forgot this --- BossMod/Autorotation/akechi/AkechiBLM.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index dc0449f057..25c8f7ef89 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -658,7 +658,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa TargetChoice(thunder) ?? primaryTarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); - if (AOEStrategy is AOEStrategy.ForceAOE) + if (forceAOE) QueueGCD(BestThunderAOE, TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : From 104f9b115d074f3429b36dadc277ed05a9cbcdf3 Mon Sep 17 00:00:00 2001 From: AceAkechi123 Date: Sun, 19 Jan 2025 05:29:02 -0800 Subject: [PATCH 03/33] good to go for now, more opti later --- BossMod/Autorotation/akechi/AkechiBLM.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index 25c8f7ef89..94f914d4e2 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -655,7 +655,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa GCDPriority.DOT); if (forceST) QueueGCD(BestThunderST, - TargetChoice(thunder) ?? primaryTarget, + TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); if (forceAOE) @@ -681,7 +681,7 @@ or PolyglotStrategy.XenoHold1 or PolyglotStrategy.XenoHold2 or PolyglotStrategy.XenoHold3) QueueGCD(BestXenoglossy, - TargetChoice(polyglot) ?? primaryTarget, + TargetChoice(polyglot) ?? primaryTarget ?? BestAOETarget, polyglotStrat is PolyglotStrategy.ForceXeno ? GCDPriority.ForcedGCD : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot : GCDPriority.Polyglot); @@ -736,11 +736,13 @@ or ManafontStrategy.ForceWeaveEX ? OGCDPriority.ForcedOGCD : OGCDPriority.Manafont); //Retrace + //TODO: more options? if (ShouldUseRetrace(retraceStrat)) QueueOGCD(AID.Retrace, Player, OGCDPriority.ForcedOGCD); //Between the Lines + //TODO: Utility maybe? if (ShouldUseBTL(btlStrat)) QueueOGCD(AID.BetweenTheLines, Player, From 30c2105a5a625f78a58ea35479a5d2befd247cc6 Mon Sep 17 00:00:00 2001 From: AceAkechi123 Date: Sun, 19 Jan 2025 05:37:25 -0800 Subject: [PATCH 04/33] this caused issues oops --- BossMod/Autorotation/akechi/AkechiBLM.cs | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index 94f914d4e2..7ab3b5f596 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -837,7 +837,7 @@ private void BestST(Actor? target) //Single-target rotation based on level QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III } } - if (!Unlocked(AID.Blizzard3) || Player.Level is >= 1 and <= 34) + if (Player.Level is >= 1 and <= 34) { //Fire if (Unlocked(AID.Fire1) && //if Fire is unlocked @@ -855,7 +855,7 @@ private void BestST(Actor? target) //Single-target rotation based on level QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); //Queue Transpose } - if (!Unlocked(AID.Fire4) || Player.Level is >= 35 and <= 59) + if (Player.Level is >= 35 and <= 59) { if (InUmbralIce) //if Umbral Ice is active { @@ -889,7 +889,7 @@ private void BestST(Actor? target) //Single-target rotation based on level QueueGCD(AID.Blizzard3, target, GCDPriority.SecondStep); //Queue Blizzard III } } - if (!Unlocked(AID.Despair) || Player.Level is >= 60 and <= 71) + if (Player.Level is >= 60 and <= 71) { if (InUmbralIce) //if Umbral Ice is active { @@ -921,7 +921,7 @@ private void BestST(Actor? target) //Single-target rotation based on level QueueGCD(AID.Blizzard3, target, GCDPriority.ThirdStep); //Queue Blizzard III } } - if (!Unlocked(AID.Paradox) || Player.Level is >= 72 and <= 89) + if (Player.Level is >= 72 and <= 89) { if (InUmbralIce) //if Umbral Ice is active { @@ -957,7 +957,7 @@ private void BestST(Actor? target) //Single-target rotation based on level QueueGCD(AID.Blizzard3, target, GCDPriority.FourthStep); //Queue Blizzard III } } - if (!Unlocked(AID.FlareStar) || Player.Level is >= 90 and <= 99) + if (Player.Level is >= 90 and <= 99) { if (InUmbralIce) //if Umbral Ice is active { @@ -999,7 +999,7 @@ private void BestST(Actor? target) //Single-target rotation based on level QueueGCD(AID.Blizzard3, target, GCDPriority.FourthStep); //Queue Blizzard III } } - if (!Unlocked(AID.FlareStar) || Player.Level is 100) + if (Player.Level is 100) { if (InUmbralIce) //if Umbral Ice is active { @@ -1073,7 +1073,7 @@ private void BestAOE(Actor? target) //AOE rotation based on level QueueGCD(AID.HighBlizzard2, target, GCDPriority.NeedB3); } } - if (!Unlocked(AID.Blizzard3) || Player.Level is >= 12 and <= 35) + if (Player.Level is >= 12 and <= 35) { //Fire if (Unlocked(AID.Fire2) && //if Fire is unlocked @@ -1093,7 +1093,7 @@ private void BestAOE(Actor? target) //AOE rotation based on level if (InAstralFire && !Unlocked(AID.Fire2)) QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); } - if (!Unlocked(AID.Freeze) || Player.Level is >= 35 and <= 39) + if (Player.Level is >= 35 and <= 39) { if (InUmbralIce) { @@ -1117,7 +1117,7 @@ private void BestAOE(Actor? target) //AOE rotation based on level QueueGCD(AID.Blizzard2, target, GCDPriority.SecondStep); } } - if (!Unlocked(AID.Flare) || Player.Level is >= 40 and <= 49) + if (Player.Level is >= 40 and <= 49) { if (InUmbralIce) { @@ -1144,7 +1144,7 @@ private void BestAOE(Actor? target) //AOE rotation based on level QueueGCD(AID.Blizzard2, target, GCDPriority.SecondStep); } } - if (!Unlocked(AID.Blizzard4) || Player.Level is >= 50 and <= 57) + if (Player.Level is >= 50 and <= 57) { if (InUmbralIce) { @@ -1178,7 +1178,7 @@ private void BestAOE(Actor? target) //AOE rotation based on level QueueGCD(AID.Blizzard2, target, MP < 400 ? GCDPriority.ForcedStep : GCDPriority.ThirdStep); } } - if (!Unlocked(AID.HighBlizzard2) || Player.Level is >= 58 and <= 81) + if (Player.Level is >= 58 and <= 81) { if (InUmbralIce) { @@ -1218,7 +1218,7 @@ private void BestAOE(Actor? target) //AOE rotation based on level QueueGCD(AID.Blizzard2, target, GCDPriority.FourthStep); } } - if (!Unlocked(AID.FlareStar) || Player.Level is >= 82 and <= 99) + if (Player.Level is >= 82 and <= 99) { if (InUmbralIce) { @@ -1258,7 +1258,7 @@ private void BestAOE(Actor? target) //AOE rotation based on level QueueGCD(AID.HighBlizzard2, target, GCDPriority.ThirdStep); } } - if (Unlocked(AID.FlareStar) || Player.Level is 100) + if (Player.Level is 100) { if (InUmbralIce) { From 1aaff43d7d3ed052890f1439eb0810f1ff4e52c8 Mon Sep 17 00:00:00 2001 From: AceAkechi123 Date: Sun, 19 Jan 2025 05:43:36 -0800 Subject: [PATCH 05/33] Scathe has too much prio here, adjusted this --- BossMod/Autorotation/akechi/AkechiBLM.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index 7ab3b5f596..6b1d151e32 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -560,12 +560,15 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa : PlayerHasEffect(SID.Firestarter, 30) ? AID.Fire3 : hasThunderhead ? (forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder) - : AID.Scathe, + : BestThunder, Polyglots > 0 ? TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget : hasThunderhead ? TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget : primaryTarget, GCDPriority.Moving1); + if (CD(AID.Swiftcast) > 2f && //if Swiftcast is on cooldown + CD(AID.Triplecast) > 62f) //and Triplecast is not active + QueueGCD(AID.Scathe, primaryTarget, GCDPriority.Moving1); //use Scathe //OGCDs if (ActionReady(AID.Swiftcast) && !PlayerHasEffect(SID.Triplecast, 15)) From de8e7020923980cb17370fcc5d3d13ac3c75ed4f Mon Sep 17 00:00:00 2001 From: AceAkechi123 Date: Sun, 19 Jan 2025 05:52:07 -0800 Subject: [PATCH 06/33] wait what? --- BossMod/Autorotation/akechi/AkechiBLM.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index 6b1d151e32..e8d9ea8b6e 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -558,9 +558,10 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? (forceST ? BestXenoglossy : forceAOE ? AID.Foul : BestPolyglot) : PlayerHasEffect(SID.Firestarter, 30) ? AID.Fire3 - : hasThunderhead ? - (forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder) - : BestThunder, + : hasThunderhead ? (forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder) + : ActionReady(AID.Swiftcast) && !PlayerHasEffect(SID.Triplecast, 15) ? AID.Swiftcast + : Unlocked(AID.Triplecast) && CD(AID.Triplecast) <= 60 && !PlayerHasEffect(SID.Triplecast, 15) && !PlayerHasEffect(SID.Swiftcast, 10) ? AID.Triplecast + : AID.Scathe, Polyglots > 0 ? TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget : hasThunderhead ? TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget From df275f4e005448fe34130fe0f5780dfd97c64210 Mon Sep 17 00:00:00 2001 From: ace Date: Sun, 19 Jan 2025 10:42:12 -0800 Subject: [PATCH 07/33] cooked --- BossMod/Autorotation/akechi/AkechiBLM.cs | 143 ++++++++++------------- 1 file changed, 60 insertions(+), 83 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index e8d9ea8b6e..4e06f8d6d4 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -1,4 +1,9 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using System; +using static BossMod.ActorState; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.DataCenterHelper; using AID = BossMod.BLM.AID; using SID = BossMod.BLM.SID; using TraitID = BossMod.BLM.TraitID; @@ -357,7 +362,8 @@ private AID BestXenoglossy private bool In25y(Actor? target) => Player.DistanceToHitbox(target) <= 24.99f; //Check if the target is within 25 yalms private bool ActionReady(AID aid) => Unlocked(aid) && CD(aid) < 0.6f; //Check if the desired action is unlocked and is ready (cooldown less than 0.6 seconds) private bool PlayerHasEffect(SID sid, float duration) => SelfStatusLeft(sid, duration) > GCD; //Checks if Status effect is on self - public float GetActualCastTime(AID aid) => ActionDefinitions.Instance.Spell(aid)!.CastTime * SpS / 2.5f; + private float GetCurrentCastTime(AID aid) => ActionDefinitions.Instance.Spell(aid)!.CastTime; //Get the current cast time for the specified action + public float GetActualCastTime(AID aid) => GetCurrentCastTime(aid) * SpS / 2.5f; public float GetCastTime(AID aid) { var aspect = ActionDefinitions.Instance.Spell(aid)!.Aspect; @@ -384,49 +390,15 @@ private bool JustUsed(AID aid, float variance) } #region Targeting - private bool ShouldUseAOE - { - get - { - var bestTarget = BestAOETarget; - if (bestTarget != null) - { - var minimumTargetsForAOE = 2; - float splashPriorityFunc(Actor actor) - { - var distanceToPlayer = actor.DistanceToHitbox(Player); - if (distanceToPlayer <= 24.99f) - { - var targetsInSplashRadius = 0; - foreach (var enemy in Hints.PriorityTargets) - { - var targetActor = enemy.Actor; - if (targetActor != actor && targetActor.Position.InCircle(actor.Position, 5f)) - { - targetsInSplashRadius++; - } - } - return targetsInSplashRadius; - } - return float.MinValue; - } - - var (_, bestPrio) = FindBetterTargetBy(null, 25f, splashPriorityFunc); - - return bestPrio >= minimumTargetsForAOE; - } - - return false; - } - } - private int TargetsInRange() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 25); //Returns the number of targets hit by AOE within a 25-yalm radius around the player + private int TargetsInRange() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 26); //Returns the number of targets within 26-yalm radius around the player + private bool ShouldUseAOE => TargetsInRange() >= 3; //Check if we should use AOE private Actor? TargetChoice(StrategyValues.OptionRef strategy) => ResolveTargetOverride(strategy.Value); //Resolves the target choice based on the strategy - private Actor? FindBestSplashTarget() + private Actor? FindBestTarget() { - float splashPriorityFunc(Actor actor) + float AOEPriorityFunc(Actor actor) { var distanceToPlayer = actor.DistanceToHitbox(Player); - if (distanceToPlayer <= 24f) + if (distanceToPlayer <= 24.99f) { var targetsInSplashRadius = 0; foreach (var enemy in Hints.PriorityTargets) @@ -437,16 +409,17 @@ float splashPriorityFunc(Actor actor) targetsInSplashRadius++; } } - return targetsInSplashRadius; + return targetsInSplashRadius * 10 - actor.HPMP.CurHP * 0.01f; } return float.MinValue; } + float STPriorityFunc(Actor actor) => actor.HPMP.CurHP > 0 ? 1f / actor.HPMP.CurHP : float.MinValue; - var (bestTarget, bestPrio) = FindBetterTargetBy(null, 25f, splashPriorityFunc); - - return bestTarget; + var (bestAOETarget, bestAOEPrio) = FindBetterTargetBy(null, 25f, STPriorityFunc); + var (bestTarget, bestPrio) = FindBetterTargetBy(bestAOETarget, 25f, AOEPriorityFunc); + return ShouldUseAOE ? bestAOETarget : bestTarget; } - private Actor? BestAOETarget => FindBestSplashTarget(); // Find the best target for splash attack + private Actor? BestTarget => FindBestTarget(); // Find the best target for splash attack //TODO: BestDOTTarget #endregion @@ -536,11 +509,11 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa (Unlocked(TraitID.EnhancedAstralFire) && MP is < 1600 and not 0)))) //instant cast Despair { if (AOEStrategy is AOEStrategy.Auto) - BestRotation(TargetChoice(AOE) ?? primaryTarget ?? BestAOETarget); //target prio is user choice -> current target -> best AOE target + BestRotation(TargetChoice(AOE) ?? BestTarget ?? primaryTarget); //target prio is user choice -> current target -> best AOE target if (forceST) BestST(TargetChoice(AOE) ?? primaryTarget); //target prio is user choice -> current target if (forceAOE) - BestAOE(TargetChoice(AOE) ?? primaryTarget ?? BestAOETarget); //target prio is user choice -> best AOE target -> current target + BestAOE(TargetChoice(AOE) ?? primaryTarget); //target prio is user choice -> best AOE target -> current target } #endregion @@ -551,34 +524,38 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa { if (movementStrat is MovementStrategy.Allow) { - //GCDs - if (!PlayerHasEffect(SID.Swiftcast, 10) || - !PlayerHasEffect(SID.Triplecast, 15)) - QueueGCD( - Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? - (forceST ? BestXenoglossy : forceAOE ? AID.Foul : BestPolyglot) - : PlayerHasEffect(SID.Firestarter, 30) ? AID.Fire3 - : hasThunderhead ? (forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder) - : ActionReady(AID.Swiftcast) && !PlayerHasEffect(SID.Triplecast, 15) ? AID.Swiftcast - : Unlocked(AID.Triplecast) && CD(AID.Triplecast) <= 60 && !PlayerHasEffect(SID.Triplecast, 15) && !PlayerHasEffect(SID.Swiftcast, 10) ? AID.Triplecast - : AID.Scathe, - Polyglots > 0 ? TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget - : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget - : hasThunderhead ? TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget - : primaryTarget, - GCDPriority.Moving1); - if (CD(AID.Swiftcast) > 2f && //if Swiftcast is on cooldown - CD(AID.Triplecast) > 62f) //and Triplecast is not active - QueueGCD(AID.Scathe, primaryTarget, GCDPriority.Moving1); //use Scathe + // GCDs + if (!PlayerHasEffect(SID.Swiftcast, 10) || !PlayerHasEffect(SID.Triplecast, 15)) + { + if (Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0) + QueueGCD(forceST ? BestXenoglossy : forceAOE ? AID.Foul : BestPolyglot, + TargetChoice(polyglot) ?? primaryTarget ?? BestTarget, + GCDPriority.Moving1); + + if (PlayerHasEffect(SID.Firestarter, 30)) + QueueGCD(AID.Fire3, + TargetChoice(AOE) ?? primaryTarget ?? BestTarget, + GCDPriority.Moving1); + + if (hasThunderhead) + QueueGCD(forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder, + TargetChoice(thunder) ?? primaryTarget ?? BestTarget, + GCDPriority.Moving1); + + if (MP >= 800 && + CD(AID.Swiftcast) > 2f && //if Swiftcast is on cooldown + CD(AID.Triplecast) > 62f) //and Triplecast is not active + QueueGCD(AID.Scathe, TargetChoice(AOE) ?? primaryTarget ?? BestTarget, GCDPriority.SixthStep); //use Scathe + } //OGCDs if (ActionReady(AID.Swiftcast) && !PlayerHasEffect(SID.Triplecast, 15)) - QueueOGCD(AID.Swiftcast, Player, GCDPriority.Moving2); + QueueOGCD(AID.Swiftcast, Player, GCDPriority.Moving1); if (Unlocked(AID.Triplecast) && CD(AID.Triplecast) <= 60 && !PlayerHasEffect(SID.Triplecast, 15) && !PlayerHasEffect(SID.Swiftcast, 10)) - QueueOGCD(AID.Triplecast, Player, GCDPriority.Moving3); + QueueOGCD(AID.Triplecast, Player, GCDPriority.Moving1); } if (movementStrat is MovementStrategy.OnlyGCDs) { @@ -592,9 +569,9 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa : hasThunderhead ? (forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder) : AID.Scathe, - Polyglots > 0 ? TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget - : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget - : hasThunderhead ? TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget + Polyglots > 0 ? TargetChoice(polyglot) ?? primaryTarget ?? BestTarget + : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget ?? BestTarget + : hasThunderhead ? TargetChoice(thunder) ?? primaryTarget ?? BestTarget : primaryTarget, GCDPriority.Moving1); } @@ -611,7 +588,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa if (movementStrat is MovementStrategy.OnlyScathe) { if (Unlocked(AID.Scathe) && MP >= 800) - QueueGCD(AID.Scathe, primaryTarget, GCDPriority.Moving1); + QueueGCD(AID.Scathe, TargetChoice(AOE) ?? primaryTarget ?? BestTarget, GCDPriority.Moving1); } } #endregion @@ -654,17 +631,17 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa { if (AOEStrategy is AOEStrategy.Auto) QueueGCD(BestThunder, - TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, + TargetChoice(thunder) ?? primaryTarget ?? BestTarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); if (forceST) QueueGCD(BestThunderST, - TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, + TargetChoice(thunder) ?? primaryTarget ?? BestTarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); if (forceAOE) QueueGCD(BestThunderAOE, - TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, + TargetChoice(thunder) ?? primaryTarget ?? BestTarget, ThunderLeft < 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); } @@ -676,7 +653,7 @@ or PolyglotStrategy.AutoHold1 or PolyglotStrategy.AutoHold2 or PolyglotStrategy.AutoHold3) QueueGCD(BestPolyglot, - TargetChoice(polyglot) ?? primaryTarget ?? BestAOETarget, + TargetChoice(polyglot) ?? primaryTarget ?? BestTarget, polyglotStrat is PolyglotStrategy.ForceXeno ? GCDPriority.ForcedGCD : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot : GCDPriority.Polyglot); @@ -685,7 +662,7 @@ or PolyglotStrategy.XenoHold1 or PolyglotStrategy.XenoHold2 or PolyglotStrategy.XenoHold3) QueueGCD(BestXenoglossy, - TargetChoice(polyglot) ?? primaryTarget ?? BestAOETarget, + TargetChoice(polyglot) ?? primaryTarget ?? BestTarget, polyglotStrat is PolyglotStrategy.ForceXeno ? GCDPriority.ForcedGCD : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot : GCDPriority.Polyglot); @@ -694,7 +671,7 @@ or PolyglotStrategy.FoulHold1 or PolyglotStrategy.FoulHold2 or PolyglotStrategy.FoulHold3) QueueGCD(AID.Foul, - TargetChoice(polyglot) ?? primaryTarget ?? BestAOETarget, + TargetChoice(polyglot) ?? primaryTarget ?? BestTarget, polyglotStrat is PolyglotStrategy.ForceFoul ? GCDPriority.ForcedGCD : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot : GCDPriority.Polyglot); @@ -943,20 +920,20 @@ private void BestST(Actor? target) //Single-target rotation based on level //Step 1-3, 5-7 - Fire IV if (MP >= 1600) //and MP is 1600 or more QueueGCD(AID.Fire4, target, GCDPriority.FirstStep); //Queue Fire IV - //Step 4A - Fire 1 + //Step 4A - Fire 1 if (ElementTimer <= (GetCastTime(AID.Fire1) * 3) && //if time remaining on current element is less than 3x GCDs MP >= 4000) //and MP is 4000 or more QueueGCD(AID.Fire1, target, ElementTimer <= (GetCastTime(AID.Fire1) * 3) && MP >= 4000 ? GCDPriority.Paradox : GCDPriority.SecondStep); //Queue Fire I, increase priority if less than 3s left on element - //Step 4B - F3P + //Step 4B - F3P if (SelfStatusLeft(SID.Firestarter, 30) is < 25 and not 0 && //if Firestarter buff is active and not 0 AstralStacks == 3) //and Umbral Hearts are 0 QueueGCD(AID.Fire3, target, GCDPriority.ForcedStep); //Queue Fire III (AF3 F3P) - //Step 8 - Despair + //Step 8 - Despair if (Unlocked(AID.Despair) && //if Despair is unlocked ((MP is < 1600 and >= 800) || //if MP is less than 1600 and not 0 (MP is <= 4000 and >= 800 && ElementTimer <= (GetCastTime(AID.Despair) * 2)))) //or if we dont have enough time for last F4s QueueGCD(AID.Despair, target, ElementTimer <= (GetCastTime(AID.Despair) * 2) ? GCDPriority.ForcedGCD : GCDPriority.ThirdStep); //Queue Despair - //Step 9 - swap from AF to UI + //Step 9 - swap from AF to UI if (MP <= 400) //and MP is less than 400 QueueGCD(AID.Blizzard3, target, GCDPriority.FourthStep); //Queue Blizzard III } From fb8a08ac3481017f09c5cf315db56098435ee7ea Mon Sep 17 00:00:00 2001 From: ace Date: Sun, 19 Jan 2025 10:43:30 -0800 Subject: [PATCH 08/33] pesky little shits --- BossMod/Autorotation/akechi/AkechiBLM.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index 4e06f8d6d4..4d1659c7f7 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -1,9 +1,4 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; -using System; -using static BossMod.ActorState; -using static FFXIVClientStructs.FFXIV.Client.UI.Misc.DataCenterHelper; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using AID = BossMod.BLM.AID; using SID = BossMod.BLM.SID; using TraitID = BossMod.BLM.TraitID; From 1b3ab0f5b778cf5d742b194313b91c4fd9c5c987 Mon Sep 17 00:00:00 2001 From: ace Date: Sun, 19 Jan 2025 15:13:59 -0800 Subject: [PATCH 09/33] some opti stuff, Fire III opener --- BossMod/Autorotation/akechi/AkechiBLM.cs | 38 ++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index 4d1659c7f7..61ce511099 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -284,6 +284,9 @@ public static RotationModuleDefinition Definition() //Forced ForcedGCD = 900, //Forced GCDs + + //Opener + Opener = 1000, //Opener } public enum OGCDPriority //priorities for oGCDs (higher number = higher priority) { @@ -348,6 +351,7 @@ private AID BestXenoglossy public bool canWeaveLate; //Can late weave oGCDs public float SpS; //Current GCD length, adjusted by spell speed/haste (2.5s baseline) public AID NextGCD; //Next global cooldown action to be used + public bool canOpen; //Can use opener #endregion #region Module Helpers @@ -461,7 +465,10 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa canWeaveLate = GCD is <= 1.25f and >= 0.1f; //Can weave in oGCDs late SpS = ActionSpeed.GCDRounded(World.Client.PlayerStats.SpellSpeed, World.Client.PlayerStats.Haste, Player.Level); //GCD based on spell speed and haste NextGCD = AID.None; //Next global cooldown action to be used - + canOpen = CD(AID.LeyLines) <= 120 + && CD(AID.Triplecast) <= 0.1f + && CD(AID.Manafont) <= 0.1f + && CD(AID.Amplifier) <= 0.1f; #region Strategy Definitions var AOE = strategy.Option(Track.AOE); //AOE track var AOEStrategy = AOE.As(); //AOE strategy @@ -599,7 +606,8 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa (tpusStrat == TPUSStrategy.Allow && (!Player.InCombat || Player.InCombat && TargetsInRange() is 0)) || (tpusStrat == TPUSStrategy.OOConly && !Player.InCombat)) { - if (CD(AID.Transpose) < 0.6f && + if (Player.Level < 35 && + CD(AID.Transpose) < 0.6f && (InAstralFire || InUmbralIce)) QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); } @@ -788,7 +796,7 @@ public bool QueueAction(AID aid, Actor? target, float priority, float delay) #endregion #region Rotation Helpers - private void BestRotation(Actor? target) //Best rotation based on targets nearby + private void BestRotation(Actor? target) { if (ShouldUseAOE) { @@ -805,13 +813,13 @@ private void BestST(Actor? target) //Single-target rotation based on level { if (NoStance) //if no stance is active { - if (Unlocked(AID.Blizzard3)) //if Blizzard III is unlocked - { - if (MP >= 10000) //if no stance is active and MP is max (opener) - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III - if (MP < 10000 && Player.InCombat) //or if in combat and no stance is active and MP is less than max (died or stopped attacking) - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III - } + if (Unlocked(AID.Blizzard3) && + MP < 9600 && + Player.InCombat) //if Blizzard III is unlocked + QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III + if (Unlocked(AID.Fire3) && + (CD(AID.Manafont) < 5 && MP >= 10000)) + QueueGCD(AID.Fire3, target, canOpen ? GCDPriority.Opener : GCDPriority.NeedB3); } if (Player.Level is >= 1 and <= 34) { @@ -1008,9 +1016,9 @@ private void BestST(Actor? target) //Single-target rotation based on level AstralStacks == 3) //and Umbral Hearts are 0 QueueGCD(AID.Fire3, target, GCDPriority.ForcedStep); //Queue Fire III (AF3 F3P) //Step 8 - Despair - if (MP is < 1600 and not 0 && //if MP is less than 1600 and not 0 - Unlocked(AID.Despair)) //and Despair is unlocked - QueueGCD(AID.Despair, target, GCDPriority.ThirdStep); //Queue Despair + if (Unlocked(AID.Despair) && + ((MP is < 1600 and not 0) || (MP <= 1600 && ElementTimer <= 4))) //if MP is less than 1600 and not 0 + QueueGCD(AID.Despair, target, (MP <= 1600 && ElementTimer <= 4) ? GCDPriority.NeedPolyglot : GCDPriority.ThirdStep); //Queue Despair //Step 9 - Flare Star if (AstralSoulStacks == 6) //if Astral Soul stacks are max QueueGCD(AID.FlareStar, target, GCDPriority.FourthStep); //Queue Flare Star @@ -1398,8 +1406,8 @@ private void BestAOE(Actor? target) //AOE rotation based on level => Player.InCombat && target != null && canMF && - InAstralFire && - (JustUsed(BestXenoglossy, 5) && MP < 1600), + canWeaveIn && + MP == 0, ManafontStrategy.Force => canMF, ManafontStrategy.ForceWeave => canMF && canWeaveIn, ManafontStrategy.ForceEX => canMF, From b72f6452633d9ae367802056db3627ee8fec183a Mon Sep 17 00:00:00 2001 From: ace Date: Sun, 19 Jan 2025 16:21:46 -0800 Subject: [PATCH 10/33] more de-spaghetting --- BossMod/Autorotation/akechi/AkechiBLM.cs | 87 ++++++++---------------- 1 file changed, 28 insertions(+), 59 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index 61ce511099..607a9ce5f7 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -36,6 +36,7 @@ public enum AOEStrategy public enum MovementStrategy { Allow, //Allow the use of all abilities for movement, regardless of any setting or condition set by the user in other options + AllowNoScathe, //Allow the use of all abilities for movement, except Scathe OnlyGCDs, //Only use instant cast GCDs for movement (Polyglots->Firestarter->Thunder->Scathe if nothing left), regardless of any setting or condition set by the user in other options OnlyOGCDs, //Only use OGCDs for movement, (Swiftcast->Triplecast) regardless of any setting or condition set by the user in other options OnlyScathe, //Only use Scathe for movement @@ -141,6 +142,7 @@ public static RotationModuleDefinition Definition() .AddOption(AOEStrategy.ForceAOE, "Force AOE", "Force use of AOE abilities only", supportedTargets: ActionTargets.Hostile); res.Define(Track.Movement).As("Movement", uiPriority: 195) .AddOption(MovementStrategy.Allow, "Allow", "Allow the use of all appropriate abilities for movement") + .AddOption(MovementStrategy.AllowNoScathe, "AllowNoScathe", "Allow the use of all appropriate abilities for movement except for Scathe") .AddOption(MovementStrategy.OnlyGCDs, "OnlyGCDs", "Only use instant cast GCDs for movement; Polyglots->Firestarter->Thunder->Scathe if nothing left") .AddOption(MovementStrategy.OnlyOGCDs, "OnlyOGCDs", "Only use OGCDs for movement; Swiftcast->Triplecast") .AddOption(MovementStrategy.OnlyScathe, "OnlyScathe", "Only use Scathe for movement") @@ -524,7 +526,9 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa primaryTarget != null && isMoving) { - if (movementStrat is MovementStrategy.Allow) + if (movementStrat is MovementStrategy.Allow + or MovementStrategy.AllowNoScathe + or MovementStrategy.OnlyGCDs) { // GCDs if (!PlayerHasEffect(SID.Swiftcast, 10) || !PlayerHasEffect(SID.Triplecast, 15)) @@ -543,41 +547,11 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa QueueGCD(forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder, TargetChoice(thunder) ?? primaryTarget ?? BestTarget, GCDPriority.Moving1); - - if (MP >= 800 && - CD(AID.Swiftcast) > 2f && //if Swiftcast is on cooldown - CD(AID.Triplecast) > 62f) //and Triplecast is not active - QueueGCD(AID.Scathe, TargetChoice(AOE) ?? primaryTarget ?? BestTarget, GCDPriority.SixthStep); //use Scathe } - //OGCDs - if (ActionReady(AID.Swiftcast) && - !PlayerHasEffect(SID.Triplecast, 15)) - QueueOGCD(AID.Swiftcast, Player, GCDPriority.Moving1); - if (Unlocked(AID.Triplecast) && - CD(AID.Triplecast) <= 60 && - !PlayerHasEffect(SID.Triplecast, 15) && - !PlayerHasEffect(SID.Swiftcast, 10)) - QueueOGCD(AID.Triplecast, Player, GCDPriority.Moving1); - } - if (movementStrat is MovementStrategy.OnlyGCDs) - { - //GCDs - if (!PlayerHasEffect(SID.Swiftcast, 10) || - !PlayerHasEffect(SID.Triplecast, 15)) - QueueGCD( - Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? - (forceST ? BestXenoglossy : forceAOE ? AID.Foul : BestPolyglot) - : PlayerHasEffect(SID.Firestarter, 30) ? AID.Fire3 - : hasThunderhead ? - (forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder) - : AID.Scathe, - Polyglots > 0 ? TargetChoice(polyglot) ?? primaryTarget ?? BestTarget - : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget ?? BestTarget - : hasThunderhead ? TargetChoice(thunder) ?? primaryTarget ?? BestTarget - : primaryTarget, - GCDPriority.Moving1); } - if (movementStrat is MovementStrategy.OnlyOGCDs) + if (movementStrat is MovementStrategy.Allow + or MovementStrategy.AllowNoScathe + or MovementStrategy.OnlyOGCDs) { //OGCDs if (ActionReady(AID.Swiftcast) && @@ -587,7 +561,8 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa !PlayerHasEffect(SID.Swiftcast, 10)) QueueOGCD(AID.Triplecast, Player, GCDPriority.Moving3); } - if (movementStrat is MovementStrategy.OnlyScathe) + if (movementStrat is MovementStrategy.Allow + or MovementStrategy.OnlyScathe) { if (Unlocked(AID.Scathe) && MP >= 800) QueueGCD(AID.Scathe, TargetChoice(AOE) ?? primaryTarget ?? BestTarget, GCDPriority.Moving1); @@ -596,56 +571,48 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa #endregion #region Out of combat - if (tpusStrat != TPUSStrategy.Forbid) + if (primaryTarget == null && + (tpusStrat == TPUSStrategy.Allow && (!Player.InCombat || Player.InCombat && TargetsInRange() is 0)) || + (tpusStrat == TPUSStrategy.OOConly && !Player.InCombat)) { if (Unlocked(AID.Transpose)) { if (!Unlocked(AID.UmbralSoul)) { - if (primaryTarget == null && - (tpusStrat == TPUSStrategy.Allow && (!Player.InCombat || Player.InCombat && TargetsInRange() is 0)) || - (tpusStrat == TPUSStrategy.OOConly && !Player.InCombat)) - { - if (Player.Level < 35 && - CD(AID.Transpose) < 0.6f && - (InAstralFire || InUmbralIce)) - QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); - } + if (CD(AID.Transpose) < 0.6f && + (InAstralFire || InUmbralIce)) + QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); } if (Unlocked(AID.UmbralSoul)) { - if (primaryTarget == null && - (tpusStrat == TPUSStrategy.Allow && (!Player.InCombat || Player.InCombat && TargetsInRange() is 0)) || - (tpusStrat == TPUSStrategy.OOConly && !Player.InCombat)) - { - if (InAstralFire) - QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); - if (InUmbralIce && - (ElementTimer <= 14 || UmbralStacks < 3 || UmbralHearts != MaxUmbralHearts)) - QueueGCD(AID.UmbralSoul, Player, GCDPriority.Standard); - } + if (InAstralFire) + QueueOGCD(AID.Transpose, Player, OGCDPriority.Transpose); + if (InUmbralIce && + (ElementTimer <= 14 || UmbralStacks < 3 || UmbralHearts != MaxUmbralHearts)) + QueueGCD(AID.UmbralSoul, Player, GCDPriority.Standard); } } } #endregion + #region Cooldowns //Thunder if (ShouldUseThunder(primaryTarget, thunderStrat)) //if Thunder should be used based on strategy { if (AOEStrategy is AOEStrategy.Auto) QueueGCD(BestThunder, TargetChoice(thunder) ?? primaryTarget ?? BestTarget, - ThunderLeft < 3 ? GCDPriority.NeedDOT : + ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); if (forceST) QueueGCD(BestThunderST, TargetChoice(thunder) ?? primaryTarget ?? BestTarget, - ThunderLeft < 3 ? GCDPriority.NeedDOT : + ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); if (forceAOE) QueueGCD(BestThunderAOE, TargetChoice(thunder) ?? primaryTarget ?? BestTarget, - ThunderLeft < 3 ? GCDPriority.NeedDOT : + ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); } //Polyglots @@ -736,6 +703,8 @@ or ManafontStrategy.ForceWeaveEX potionStrat is PotionStrategy.Immediate) Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionInt, Player, ActionQueue.Priority.VeryHigh + (int)OGCDPriority.Potion, 0, GCD - 0.9f); #endregion + + #endregion } #region Core Execution Helpers @@ -818,7 +787,7 @@ private void BestST(Actor? target) //Single-target rotation based on level Player.InCombat) //if Blizzard III is unlocked QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III if (Unlocked(AID.Fire3) && - (CD(AID.Manafont) < 5 && MP >= 10000)) + ((CD(AID.Manafont) < 5 && CD(AID.LeyLines) <= 121 && MP >= 10000)) || (!Player.InCombat && World.Client.CountdownRemaining <= 4)) //F3 opener QueueGCD(AID.Fire3, target, canOpen ? GCDPriority.Opener : GCDPriority.NeedB3); } if (Player.Level is >= 1 and <= 34) From a9f4e5f1fc9568c36d7bdc5660cbe624ef0c3f1b Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:06:19 -0500 Subject: [PATCH 11/33] more changes haha yayyyy --- BossMod/ActionQueue/ActionDefinition.cs | 28 ++- BossMod/ActionQueue/Casters/SMN.cs | 1 + BossMod/ActionQueue/ClassShared.cs | 12 ++ BossMod/Autorotation/RotationModule.cs | 5 +- BossMod/Autorotation/xan/AI/AIBase.cs | 17 +- BossMod/Autorotation/xan/AI/Healer.cs | 98 ++++++++- BossMod/Autorotation/xan/AI/Melee.cs | 16 +- BossMod/Autorotation/xan/AI/Ranged.cs | 6 +- BossMod/Autorotation/xan/AI/Tank.cs | 148 ++++++++----- .../Autorotation/xan/AI/TrackPartyHealth.cs | 43 +++- BossMod/Autorotation/xan/BLU/Basic.cs | 20 +- BossMod/Autorotation/xan/Basexan.cs | 198 +++++++++++++----- BossMod/Autorotation/xan/Casters/BLM.cs | 40 ++-- BossMod/Autorotation/xan/Casters/PCT.cs | 119 ++++++----- BossMod/Autorotation/xan/Casters/RDM.cs | 21 +- BossMod/Autorotation/xan/Casters/SMN.cs | 35 ++-- BossMod/Autorotation/xan/Healers/AST.cs | 18 +- BossMod/Autorotation/xan/Healers/SCH.cs | 11 +- BossMod/Autorotation/xan/Healers/SGE.cs | 29 ++- BossMod/Autorotation/xan/Healers/WHM.cs | 18 +- BossMod/Autorotation/xan/Melee/DRG.cs | 37 ++-- BossMod/Autorotation/xan/Melee/MNK.cs | 133 ++++++++---- BossMod/Autorotation/xan/Melee/NIN.cs | 37 ++-- BossMod/Autorotation/xan/Melee/RPR.cs | 86 +++++--- BossMod/Autorotation/xan/Melee/SAM.cs | 65 +++--- BossMod/Autorotation/xan/Melee/VPR.cs | 31 ++- BossMod/Autorotation/xan/Ranged/BRD.cs | 16 +- BossMod/Autorotation/xan/Ranged/DNC.cs | 39 ++-- BossMod/Autorotation/xan/Ranged/MCH.cs | 137 +++++++----- BossMod/Autorotation/xan/Tanks/DRK.cs | 13 +- BossMod/Autorotation/xan/Tanks/GNB.cs | 38 +++- BossMod/Autorotation/xan/Tanks/PLD.cs | 185 +++++++++++----- BossMod/BossModule/AIHints.cs | 2 +- BossMod/BossModule/RaidCooldowns.cs | 4 +- BossMod/Data/Actor.cs | 6 +- 35 files changed, 1158 insertions(+), 554 deletions(-) diff --git a/BossMod/ActionQueue/ActionDefinition.cs b/BossMod/ActionQueue/ActionDefinition.cs index 949b454609..77154c58c1 100644 --- a/BossMod/ActionQueue/ActionDefinition.cs +++ b/BossMod/ActionQueue/ActionDefinition.cs @@ -18,6 +18,30 @@ public enum ActionTargets All = (1 << 9) - 1, } +// some debuffs prevent specific categories of action - amnesia, silence, pacification, etc +public enum ActionCategory : byte +{ + None, + Autoattack, + Spell, + Weaponskill, + Ability, + Item, + DoLAbility, + DoHAbility, + Event, + LimitBreak9, + System10, + System11, + Mount, + Special, + ItemManipulation, + LimitBreak15, + Unk1, + Artillery, + Unk2 +} + // used for BLM calculations and possibly BLU optimization public enum ActionAspect : byte { @@ -47,6 +71,7 @@ public sealed record class ActionDefinition(ActionID ID) public ActionTargets AllowedTargets; public float Range; // 0 for self-targeted abilities public float CastTime; // 0 for instant-cast; can be adjusted by a number of factors (TODO: add functor) + public ActionCategory Category; public int MainCooldownGroup = -1; public int ExtraCooldownGroup = -1; public float Cooldown; // for single charge (if multi-charge action); can be adjusted by a number of factors (TODO: add functor) @@ -340,7 +365,8 @@ public void RegisterSpell(ActionID aid, bool isPhysRanged = false, float instant MaxChargesBase = SpellBaseMaxCharges(data), InstantAnimLock = instantAnimLock, CastAnimLock = castAnimLock, - IsRoleAction = data.IsRoleAction + IsRoleAction = data.IsRoleAction, + Category = (ActionCategory)data.ActionCategory.RowId }; Register(aid, def); } diff --git a/BossMod/ActionQueue/Casters/SMN.cs b/BossMod/ActionQueue/Casters/SMN.cs index 1c28bf3e98..f08e4ccc9d 100644 --- a/BossMod/ActionQueue/Casters/SMN.cs +++ b/BossMod/ActionQueue/Casters/SMN.cs @@ -136,6 +136,7 @@ public enum SID : uint GarudasFavor = 2725, // applied by Summon Garuda II to self RubysGlimmer = 3873, // applied by Searing Light to self RefulgentLux = 3874, // applied by Summon Solar Bahamut to self + CrimsonStrikeReady = 4403, // applied by Crimson Cyclone to self //Shared Addle = ClassShared.SID.Addle, // applied by Addle to target diff --git a/BossMod/ActionQueue/ClassShared.cs b/BossMod/ActionQueue/ClassShared.cs index 67df2742c6..7f19ef5fc0 100644 --- a/BossMod/ActionQueue/ClassShared.cs +++ b/BossMod/ActionQueue/ClassShared.cs @@ -92,6 +92,18 @@ public enum SID : uint Addle = 1203, // applied by Addle to target Swiftcast = 167, // applied by Swiftcast to self Raise = 148, // applied by Raise to target + + // Bozja + LostChainspell = 2560, // instant cast + + MagicBurst = 1652, // magic damage buff + BannerOfNobleEnds = 2326, // damage buff + healing disable + BannerOfHonoredSacrifice = 2327, // damage buff + hp drain + LostFontOfPower = 2346, // damage/crit buff + ClericStance = 2484, // damage buff (from seraph strike) + LostExcellence = 2564, // damage buff + invincibility + Memorable = 2565, // damage buff + BloodRush = 2567, // damage buff + ability haste #endregion #region PvP diff --git a/BossMod/Autorotation/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index f0ae56acb3..53fdd97291 100644 --- a/BossMod/Autorotation/RotationModule.cs +++ b/BossMod/Autorotation/RotationModule.cs @@ -1,4 +1,6 @@ -namespace BossMod.Autorotation; +using static BossMod.AIHints; + +namespace BossMod.Autorotation; public enum RotationModuleQuality { @@ -119,6 +121,7 @@ public bool TraitUnlocked(uint id) return status != null ? (StatusDuration(status.Value.ExpireAt), status.Value.Extra & 0xFF) : (0, 0); } protected (float Left, int Stacks) StatusDetails(Actor? actor, SID sid, ulong sourceID, float pendingDuration = 1000) where SID : Enum => StatusDetails(actor, (uint)(object)sid, sourceID, pendingDuration); + protected (float Left, int Stacks) StatusDetails(Enemy? enemy, SID sid, ulong sourceID, float pendingDuration = 1000) where SID : Enum => StatusDetails(enemy?.Actor, (uint)(object)sid, sourceID, pendingDuration); protected (float Left, int Stacks) SelfStatusDetails(uint sid, float pendingDuration = 1000) => StatusDetails(Player, sid, Player.InstanceID, pendingDuration); protected (float Left, int Stacks) SelfStatusDetails(SID sid, float pendingDuration = 1000) where SID : Enum => StatusDetails(Player, sid, Player.InstanceID, pendingDuration); diff --git a/BossMod/Autorotation/xan/AI/AIBase.cs b/BossMod/Autorotation/xan/AI/AIBase.cs index f06e0c1b8b..b9a9813a19 100644 --- a/BossMod/Autorotation/xan/AI/AIBase.cs +++ b/BossMod/Autorotation/xan/AI/AIBase.cs @@ -1,6 +1,4 @@ -using Lumina.Excel.Sheets; - -namespace BossMod.Autorotation.xan; +namespace BossMod.Autorotation.xan; public abstract class AIBase(RotationModuleManager manager, Actor player) : RotationModule(manager, player) { @@ -10,10 +8,9 @@ public abstract class AIBase(RotationModuleManager manager, Actor player) : Rota internal static ActionID Spell(AID aid) where AID : Enum => ActionID.MakeSpell(aid); - internal bool ShouldInterrupt(Actor act) => IsCastReactable(act) && act.CastInfo!.Interruptible; - internal bool ShouldStun(Actor act) => IsCastReactable(act) && !act.CastInfo!.Interruptible && !IsBossFromIcon(act.OID); - - private static bool IsBossFromIcon(uint oid) => Service.LuminaRow(oid)?.Rank is 1 or 2 or 6; + // note "in combat" check here, as deep dungeon enemies can randomly cast interruptible spells out of combat - interjecting causes aggro + internal bool ShouldInterrupt(AIHints.Enemy e) => e.Actor.InCombat && e.ShouldBeInterrupted && (e.Actor.CastInfo?.Interruptible ?? false); + internal bool ShouldStun(AIHints.Enemy e) => e.Actor.InCombat && e.ShouldBeStunned; internal bool IsCastReactable(Actor act) { @@ -23,12 +20,6 @@ internal bool IsCastReactable(Actor act) internal IEnumerable EnemiesAutoingMe => Hints.PriorityTargets.Where(x => x.Actor.CastInfo == null && x.Actor.TargetID == Player.InstanceID && Player.DistanceToHitbox(x.Actor) <= 6); - internal float HPRatio(Actor actor) => (float)actor.HPMP.CurHP / Player.HPMP.MaxHP; - internal float HPRatio() => HPRatio(Player); - - internal uint PredictedHP(Actor actor) => (uint)actor.PredictedHPClamped; - internal float PredictedHPRatio(Actor actor) => (float)PredictedHP(actor) / actor.HPMP.MaxHP; - internal IEnumerable Raidwides => Hints.PredictedDamage.Where(d => World.Party.WithSlot(excludeAlliance: true).IncludedInMask(d.players).Count() >= 2).Select(t => t.activation); internal IEnumerable<(Actor, DateTime)> Tankbusters { diff --git a/BossMod/Autorotation/xan/AI/Healer.cs b/BossMod/Autorotation/xan/AI/Healer.cs index 8085e1bb8f..8a6b552561 100644 --- a/BossMod/Autorotation/xan/AI/Healer.cs +++ b/BossMod/Autorotation/xan/AI/Healer.cs @@ -1,5 +1,6 @@ using BossMod.Autorotation.xan.AI; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.Autorotation.xan.AI.TrackPartyHealth; namespace BossMod.Autorotation.xan; @@ -7,7 +8,7 @@ public class HealerAI(RotationModuleManager manager, Actor player) : AIBase(mana { private readonly TrackPartyHealth Health = new(manager.WorldState); - public enum Track { Raise, RaiseTarget, Heal, Esuna } + public enum Track { Raise, RaiseTarget, Heal, Esuna, StayNearParty } public enum RaiseStrategy { None, @@ -33,7 +34,7 @@ public enum RaiseTarget public static RotationModuleDefinition Definition() { - var def = new RotationModuleDefinition("Healer AI", "Auto-healer", "AI (xan)", "xan", RotationModuleQuality.WIP, BitMask.Build(Class.CNJ, Class.WHM, Class.ACN, Class.SCH, Class.SGE, Class.AST), 100); + var def = new RotationModuleDefinition("Healer AI", "Auto-healer", "AI (xan)", "xan", RotationModuleQuality.WIP, BitMask.Build(Class.CNJ, Class.WHM, Class.SCH, Class.SGE, Class.AST), 100); def.Define(Track.Raise).As("Raise") .AddOption(RaiseStrategy.None, "Don't automatically raise") @@ -48,16 +49,39 @@ public static RotationModuleDefinition Definition() def.AbilityTrack(Track.Heal, "Heal"); def.AbilityTrack(Track.Esuna, "Esuna"); + def.AbilityTrack(Track.StayNearParty, "Stay near party"); return def; } - private void HealSingle(Action healFun) + private void HealSingle(Action healFun) { if (Health.BestSTHealTarget is (var a, var b)) healFun(a, b); } + /// + /// Run the given Action if the party has exactly one tank, otherwise do nothing + /// + /// + private void RunForTank(Action tankFun) + { + var tankSlot = -1; + foreach (var (slot, actor) in World.Party.WithSlot(excludeAlliance: true)) + if (actor.ClassCategory == ClassCategory.Tank) + { + if (tankSlot >= 0) + return; + else + tankSlot = slot; + } + + if (tankSlot >= 0) + tankFun(World.Party[tankSlot]!, Health.PartyMemberStates[tankSlot]!); + } + + private IEnumerable LightParty => World.Party.WithoutSlot(excludeAlliance: true, excludeNPCs: Health.HaveRealPartyMembers); + public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Player.MountId > 0) @@ -65,6 +89,12 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa Health.Update(Hints); + if (strategy.Enabled(Track.StayNearParty) && Player.InCombat) + { + List<(WPos pos, float radius)> allies = [.. LightParty.Exclude(Player).Select(e => (e.Position, e.HitboxRadius))]; + Hints.GoalZones.Add(p => allies.Count(a => a.pos.InCircle(p, a.radius + Player.HitboxRadius + 15))); + } + AutoRaise(strategy); if (strategy.Enabled(Track.Esuna)) @@ -160,8 +190,8 @@ void UseThinAir() var candidates = strategy.Option(Track.RaiseTarget).As() switch { RaiseTarget.Everyone => World.Actors.Where(x => x.Type is ActorType.Player or ActorType.DutySupport && x.IsAlly), - RaiseTarget.Alliance => World.Party.WithoutSlot(true, false), - _ => World.Party.WithoutSlot(true, true) + RaiseTarget.Alliance => World.Party.WithoutSlot(true, false, true), + _ => World.Party.WithoutSlot(true, true, true) }; return candidates.Where(x => x.IsDead && Player.DistanceToHitbox(x) <= 30 && !BeingRaised(x)).MaxBy(actor => actor.Class.GetRole() switch @@ -235,8 +265,30 @@ private void AutoAST(StrategyValues strategy) Hints.ActionsToExecute.Push(ActionID.MakeSpell(BossMod.AST.AID.EarthlyStar), Player, ActionQueue.Priority.Medium, targetPos: Player.PosRot.XYZ()); } + private Vector3? GetArenaCenter() + { + if (Bossmods.ActiveModule is BossModule m) + { + var center = m.Arena.Center; + return new Vector3(center.X, Player.PosRot.Y, center.Z); + } + return null; + } + private void AutoSCH(StrategyValues strategy, Actor? primaryTarget) { + void UseSoil(Vector3? location = null) + { + if (World.Client.GetGauge().Aetherflow == 0) + return; + location ??= GetArenaCenter() ?? Player.PosRot.XYZ(); + Hints.ActionsToExecute.Push(ActionID.MakeSpell(BossMod.SCH.AID.SacredSoil), null, ActionQueue.Priority.Medium + 5, targetPos: location.Value); + } + + // TODO make this configurable + if (primaryTarget != null) + UseOGCD(BossMod.SCH.AID.ChainStratagem, primaryTarget); + var gauge = World.Client.GetGauge(); var pet = World.Client.ActivePet.InstanceID == 0xE0000000 ? null : World.Actors.Find(World.Client.ActivePet.InstanceID); @@ -250,8 +302,15 @@ private void AutoSCH(StrategyValues strategy, Actor? primaryTarget) if (pet != null) { - if (haveEos && ShouldHealInArea(pet.Position, 20, 0.5f)) - UseOGCD(BossMod.SCH.AID.FeyBlessing, Player); + if (ShouldHealInArea(pet.Position, 30, 0.5f)) + { + if (haveSeraph) + UseOGCD(BossMod.SCH.AID.Consolation, Player); + else if (NextChargeIn(BossMod.SCH.AID.SummonSeraph) == 0) + UseOGCD(BossMod.SCH.AID.SummonSeraph, Player); + else + UseOGCD(BossMod.SCH.AID.FeyBlessing, Player); + } if (ShouldHealInArea(pet.Position, 15, 0.8f)) UseOGCD(BossMod.SCH.AID.WhisperingDawn, Player); @@ -268,9 +327,30 @@ private void AutoSCH(StrategyValues strategy, Actor? primaryTarget) UseOGCD(BossMod.SCH.AID.Lustrate, target); } else - UseGCD(BossMod.SCH.AID.Physick, target); + UseGCD(BossMod.SCH.AID.Adloquium, target); } }); + + RunForTank((tank, tankState) => + { + if (!Player.InCombat && (World.CurrentTime - tankState.LastCombat).TotalSeconds > 1) + { + if (NextChargeIn(BossMod.SCH.AID.Excogitation) == 0) + UseOGCD(BossMod.SCH.AID.Recitation, Player, 5); + UseOGCD(BossMod.SCH.AID.Excogitation, tank); + } + + if (tank.InCombat && Bossmods.ActiveModule is null && tankState.MoveDelta < 0.75f) + UseSoil(tank.PosRot.XYZ()); + }); + + foreach (var rw in Raidwides) + if ((rw - World.CurrentTime).TotalSeconds < 5) + { + var allies = LightParty.ToList(); + var centroid = allies.Aggregate(allies[0].PosRot.XYZ(), (pos, actor) => (pos + actor.PosRot.XYZ()) / 2f); + UseSoil(centroid); + } } private void AutoSGE(StrategyValues strategy, Actor? primaryTarget) @@ -302,9 +382,7 @@ private void AutoSGE(StrategyValues strategy, Actor? primaryTarget) }); foreach (var rw in Raidwides) - { if ((rw - World.CurrentTime).TotalSeconds < 15 && haveBalls) UseOGCD(BossMod.SGE.AID.Kerachole, Player); - } } } diff --git a/BossMod/Autorotation/xan/AI/Melee.cs b/BossMod/Autorotation/xan/AI/Melee.cs index bd89864cca..8a88715aed 100644 --- a/BossMod/Autorotation/xan/AI/Melee.cs +++ b/BossMod/Autorotation/xan/AI/Melee.cs @@ -17,21 +17,21 @@ public static RotationModuleDefinition Definition() public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { - if (Player.Statuses.Any(x => x.ID is (uint)BossMod.NIN.SID.TenChiJin or (uint)BossMod.NIN.SID.Mudra)) + if (Player.Statuses.Any(x => x.ID is (uint)BossMod.NIN.SID.TenChiJin or (uint)BossMod.NIN.SID.Mudra or 1092)) return; // second wind - if (strategy.Enabled(Track.SecondWind) && Player.InCombat && HPRatio() <= 0.5) + if (strategy.Enabled(Track.SecondWind) && Player.InCombat && Player.PredictedHPRatio <= 0.5) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.SecondWind), Player, ActionQueue.Priority.Medium); // bloodbath - if (strategy.Enabled(Track.Bloodbath) && Player.InCombat && HPRatio() <= 0.75) + if (strategy.Enabled(Track.Bloodbath) && Player.InCombat && Player.PredictedHPRatio <= 0.75) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Bloodbath), Player, ActionQueue.Priority.Medium); // low blow if (strategy.Enabled(Track.Stun) && NextChargeIn(ClassShared.AID.LegSweep) == 0) { - var stunnableEnemy = Hints.PotentialTargets.FirstOrDefault(e => ShouldStun(e.Actor) && Player.DistanceToHitbox(e.Actor) <= 3); + var stunnableEnemy = Hints.PotentialTargets.FirstOrDefault(e => ShouldStun(e) && Player.DistanceToHitbox(e.Actor) <= 3); if (stunnableEnemy != null) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.LegSweep), stunnableEnemy.Actor, ActionQueue.Priority.Minimal); } @@ -39,6 +39,14 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa if (Player.Class == Class.SAM) AISAM(); + if (Player.FindStatus(2324) != null && Bossmods.ActiveModule?.Info?.GroupType is BossModuleInfo.GroupType.BozjaDuel) + { + var gcdLength = ActionSpeed.GCDRounded(World.Client.PlayerStats.SkillSpeed, World.Client.PlayerStats.Haste, Player.Level); + var fopLeft = Player.FindStatus(2346) is ActorStatus st ? StatusDuration(st.ExpireAt) : 0; + if (GCD + gcdLength < fopLeft) + Hints.ActionsToExecute.Push(BozjaActionID.GetNormal(BozjaHolsterID.LostAssassination), primaryTarget, ActionQueue.Priority.Low); + } + ExecLB(strategy, primaryTarget); } diff --git a/BossMod/Autorotation/xan/AI/Ranged.cs b/BossMod/Autorotation/xan/AI/Ranged.cs index 03e480ddb8..953cd8b8b4 100644 --- a/BossMod/Autorotation/xan/AI/Ranged.cs +++ b/BossMod/Autorotation/xan/AI/Ranged.cs @@ -19,13 +19,13 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa // interrupt if (strategy.Enabled(Track.Interrupt) && NextChargeIn(ClassShared.AID.HeadGraze) == 0) { - var interruptibleEnemy = Hints.PotentialTargets.FirstOrDefault(e => ShouldInterrupt(e.Actor) && Player.DistanceToHitbox(e.Actor) <= 25); + var interruptibleEnemy = Hints.PotentialTargets.FirstOrDefault(e => ShouldInterrupt(e) && Player.DistanceToHitbox(e.Actor) <= 25); if (interruptibleEnemy != null) - Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.HeadGraze), interruptibleEnemy.Actor, ActionQueue.Priority.Minimal); + Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.HeadGraze), interruptibleEnemy.Actor, ActionQueue.Priority.High); } // second wind - if (strategy.Enabled(Track.SecondWind) && Player.InCombat && HPRatio() <= 0.5) + if (strategy.Enabled(Track.SecondWind) && Player.InCombat && Player.PredictedHPRatio <= 0.5) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.SecondWind), Player, ActionQueue.Priority.Medium); ExecLB(strategy, primaryTarget); diff --git a/BossMod/Autorotation/xan/AI/Tank.cs b/BossMod/Autorotation/xan/AI/Tank.cs index 708896f1b1..5da2685787 100644 --- a/BossMod/Autorotation/xan/AI/Tank.cs +++ b/BossMod/Autorotation/xan/AI/Tank.cs @@ -21,70 +21,101 @@ public static RotationModuleDefinition Definition() return def; } - public record struct TankActions(ActionID Ranged, ActionID Stance, uint StanceBuff, ActionID PartyMit, ActionID LongMit, ActionID ShortMit, float ShortMitDuration, ActionID AllyMit, ActionID SmallMit = default, Func? ShortMitCheck = null); + public record struct Buff(ActionID ID, float Duration, float ApplicationDelay = 0, Func? CanUse = null) + { + public Buff(object ID, float Duration, float ApplicationDelay = 0, Func? CanUse = null) : this(ActionID.MakeSpell((ClassShared.AID)ID), Duration, ApplicationDelay, CanUse) { } + } + + public record struct TankActions( + ActionID Ranged, + ActionID Stance, + uint StanceBuff, + Buff Invuln, + Buff PartyMit, + Buff LongMit, + Buff ShortMit, + Buff AllyMit, + Buff SmallMit = default + ); - private static TankActions WARActions = new( + // 120s mit application delays are guessed here, the DT sheet doesn't show them (but Rampart is 0.62s) + + public static readonly TankActions WARActions = new( Ranged: Spell(WAR.AID.Tomahawk), Stance: Spell(WAR.AID.Defiance), StanceBuff: (uint)WAR.SID.Defiance, - PartyMit: Spell(WAR.AID.ShakeItOff), - LongMit: Spell(WAR.AID.Vengeance), - ShortMit: Spell(WAR.AID.RawIntuition), - ShortMitDuration: 8, - AllyMit: Spell(WAR.AID.NascentFlash) + Invuln: new(WAR.AID.Holmgang, 10, 0), + PartyMit: new(WAR.AID.ShakeItOff, 30), + LongMit: new(WAR.AID.Vengeance, 15, 0.62f), + + // 8s lifesteal, 4s damage reduction, 20s shield + // before upgrade: 6s lifesteal, 6s damage reduction + ShortMit: new(WAR.AID.RawIntuition, 4, 0.62f), + // 8s lifesteal, 4s damage reduction, 20s shield + AllyMit: new(WAR.AID.NascentFlash, 4, 0.62f) ); - private static TankActions PLDActions = new( + + public static readonly TankActions PLDActions = new( Ranged: Spell(BossMod.PLD.AID.ShieldLob), Stance: Spell(BossMod.PLD.AID.IronWill), StanceBuff: (uint)BossMod.PLD.SID.IronWill, - PartyMit: Spell(BossMod.PLD.AID.DivineVeil), - LongMit: Spell(BossMod.PLD.AID.Sentinel), - ShortMit: Spell(BossMod.PLD.AID.Sheltron), - ShortMitDuration: 8, - AllyMit: Spell(BossMod.PLD.AID.Intervention), - SmallMit: Spell(BossMod.PLD.AID.Bulwark), - ShortMitCheck: (mod) => mod.World.Client.GetGauge().OathGauge >= 50 + Invuln: new(BossMod.PLD.AID.HallowedGround, 10, 0), + PartyMit: new(BossMod.PLD.AID.DivineVeil, 30), + LongMit: new(BossMod.PLD.AID.Sentinel, 15, 0.62f), + SmallMit: new(BossMod.PLD.AID.Bulwark, 10, 0.62f), + + // 8s 15% mit, 4s of an additional 15% mit, 12s regen + // before upgrade: 6s 15% + ShortMit: new(BossMod.PLD.AID.Sheltron, 8, 0, mod => mod.World.Client.GetGauge().OathGauge >= 50), + // same as above, no pre-upgrade version + AllyMit: new(BossMod.PLD.AID.Intervention, 8, 0.80f, mod => mod.World.Client.GetGauge().OathGauge >= 50) ); - private static TankActions DRKActions = new( + + public static readonly TankActions DRKActions = new( Ranged: Spell(BossMod.DRK.AID.Unmend), Stance: Spell(BossMod.DRK.AID.Grit), StanceBuff: (uint)BossMod.DRK.SID.Grit, - PartyMit: Spell(BossMod.DRK.AID.DarkMissionary), - LongMit: Spell(BossMod.DRK.AID.ShadowWall), - ShortMit: Spell(BossMod.DRK.AID.TheBlackestNight), - ShortMitDuration: 7, - AllyMit: Spell(BossMod.DRK.AID.TheBlackestNight), - SmallMit: Spell(BossMod.DRK.AID.DarkMind), - ShortMitCheck: (mod) => mod.Player.HPMP.CurMP >= 3000 + Invuln: new(BossMod.DRK.AID.LivingDead, 10, 0), + PartyMit: new(BossMod.DRK.AID.DarkMissionary, 15, 0.62f), + LongMit: new(BossMod.DRK.AID.ShadowWall, 15, 0.62f), + SmallMit: new(BossMod.DRK.AID.DarkMind, 10, 0.62f), + + ShortMit: new(BossMod.DRK.AID.TheBlackestNight, 7, 0.62f, mod => mod.Player.HPMP.CurMP >= 3000), + AllyMit: new(BossMod.DRK.AID.TheBlackestNight, 7, 0.62f, mod => mod.Player.HPMP.CurMP >= 3000) ); - private static TankActions GNBActions = new( + + public static readonly TankActions GNBActions = new( Ranged: Spell(BossMod.GNB.AID.LightningShot), Stance: Spell(BossMod.GNB.AID.RoyalGuard), StanceBuff: (uint)BossMod.GNB.SID.RoyalGuard, - PartyMit: Spell(BossMod.GNB.AID.HeartOfLight), - LongMit: Spell(BossMod.GNB.AID.Nebula), - ShortMit: Spell(BossMod.GNB.AID.HeartOfCorundum), - ShortMitDuration: 8, - AllyMit: Spell(BossMod.GNB.AID.HeartOfCorundum), - SmallMit: Spell(BossMod.GNB.AID.Camouflage) + Invuln: new(BossMod.GNB.AID.Superbolide, 10, 0), + PartyMit: new(BossMod.GNB.AID.HeartOfLight, 15, 0.62f), + LongMit: new(BossMod.GNB.AID.Nebula, 15, 0.54f), + SmallMit: new(BossMod.GNB.AID.Camouflage, 20, 0.62f), + + // 8s 15% mit, 4s of an additional 15% mit, 20s excog + ShortMit: new(BossMod.GNB.AID.HeartOfStone, 8, 0.62f), + AllyMit: new(BossMod.GNB.AID.HeartOfStone, 8, 0.62f) ); - private TankActions JobActions => Player.Class switch + public static TankActions ActionsForJob(Class c) => c switch { - Class.GLA or Class.PLD => PLDActions, - Class.MRD or Class.WAR => WARActions, + Class.PLD => PLDActions, + Class.WAR => WARActions, Class.DRK => DRKActions, Class.GNB => GNBActions, - _ => default + _ => throw new InvalidOperationException($"{c} is not a tank class") }; + private TankActions JobActions => ActionsForJob(Player.Class); + public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Player.MountId > 0) return; // ranged - if (strategy.Enabled(Track.Ranged) && Player.DistanceToHitbox(primaryTarget) is > 5 and <= 20 && primaryTarget!.Type is ActorType.Enemy && !primaryTarget.IsAlly) + if (ShouldRanged(strategy, primaryTarget)) Hints.ActionsToExecute.Push(JobActions.Ranged, primaryTarget, ActionQueue.Priority.Low); // stance @@ -94,7 +125,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa // interrupt if (strategy.Enabled(Track.Interject) && NextChargeIn(ClassShared.AID.Interject) == 0) { - var interruptibleEnemy = Hints.PotentialTargets.FirstOrDefault(e => ShouldInterrupt(e.Actor) && Player.DistanceToHitbox(e.Actor) <= 3); + var interruptibleEnemy = Hints.PotentialTargets.FirstOrDefault(e => ShouldInterrupt(e) && Player.DistanceToHitbox(e.Actor) <= 3); if (interruptibleEnemy != null) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Interject), interruptibleEnemy.Actor, ActionQueue.Priority.Minimal); } @@ -102,7 +133,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa // low blow if (strategy.Enabled(Track.Stun) && NextChargeIn(ClassShared.AID.LowBlow) == 0) { - var stunnableEnemy = Hints.PotentialTargets.Find(e => ShouldStun(e.Actor) && Player.DistanceToHitbox(e.Actor) <= 3); + var stunnableEnemy = Hints.PotentialTargets.Find(e => ShouldInterrupt(e) && Player.DistanceToHitbox(e.Actor) <= 3); if (stunnableEnemy != null) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.LowBlow), stunnableEnemy.Actor, ActionQueue.Priority.Minimal); } @@ -128,6 +159,14 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa } } + private bool ShouldRanged(StrategyValues strategy, Actor? primaryTarget) + { + return strategy.Enabled(Track.Ranged) + && Player.DistanceToHitbox(primaryTarget) is > 5 and <= 20 + && !primaryTarget!.IsAlly + && !Player.Statuses.Any(x => x.ID is (uint)WAR.SID.Berserk or (uint)WAR.SID.InnerRelease); + } + private void AutoProtect() { var threat = Hints.PriorityTargets.FirstOrDefault(x => @@ -151,28 +190,37 @@ private void AutoProtect() foreach (var rw in Raidwides) if ((rw - World.CurrentTime).TotalSeconds < 5) { - Hints.ActionsToExecute.Push(JobActions.PartyMit, Player, ActionQueue.Priority.Medium); + Hints.ActionsToExecute.Push(JobActions.PartyMit.ID, Player, ActionQueue.Priority.Medium); if (Player.DistanceToHitbox(Bossmods.ActiveModule?.PrimaryActor) <= 5) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Reprisal), Player, ActionQueue.Priority.Low); } foreach (var (ally, t) in Tankbusters) if (ally != Player && (t - World.CurrentTime).TotalSeconds < 4) - Hints.ActionsToExecute.Push(JobActions.AllyMit, ally, ActionQueue.Priority.Low); + Hints.ActionsToExecute.Push(JobActions.AllyMit.ID, ally, ActionQueue.Priority.Low); } private void AutoMit() { - if (EnemiesAutoingMe.Count() > 1) + if (EnemiesAutoingMe.Any()) { - if (HPRatio() < 0.8) - Hints.ActionsToExecute.Push(JobActions.ShortMit, Player, ActionQueue.Priority.Minimal); + if (Player.PredictedHPRatio < 0.8) + { + var delay = 0f; + if (JobActions.ShortMit.ID == ActionID.MakeSpell(WAR.AID.RawIntuition)) + delay = GCD - 0.8f; + Hints.ActionsToExecute.Push(JobActions.ShortMit.ID, Player, ActionQueue.Priority.Minimal, delay: delay); + } - if (HPRatio() < 0.6) + if (Player.PredictedHPRatio < 0.6) // set arbitrary deadline to 1 second in the future UseOneMit(1); } + // TODO figure out how consistent this is or if we should use predictively instead + if (Player.PredictedHPRaw <= 0) + Hints.ActionsToExecute.Push(JobActions.Invuln.ID, Player, ActionQueue.Priority.VeryHigh); + foreach (var t in Tankbusters) if (t.Item1 == Player) UseOneMit((float)(t.Item2 - World.CurrentTime).TotalSeconds); @@ -182,7 +230,7 @@ private void ExecuteGNB(StrategyValues strategy) { if (strategy.Enabled(Track.Mit) && EnemiesAutoingMe.Any()) { - if (HPRatio() < 0.8 && Player.FindStatus(BossMod.GNB.SID.Aurora) == null) + if (Player.PredictedHPRatio < 0.8 && Player.FindStatus(BossMod.GNB.SID.Aurora) == null) Hints.ActionsToExecute.Push(ActionID.MakeSpell(BossMod.GNB.AID.Aurora), Player, ActionQueue.Priority.Minimal); } } @@ -191,10 +239,10 @@ private void ExecuteWAR(StrategyValues strategy) { if (strategy.Enabled(Track.Mit) && EnemiesAutoingMe.Any()) { - if (HPRatio() < 0.75) + if (Player.PredictedHPRatio < 0.75) Hints.ActionsToExecute.Push(ActionID.MakeSpell(WAR.AID.Bloodwhetting), Player, ActionQueue.Priority.Low, delay: GCD - 1f); - if (HPRatio() < 0.5) + if (Player.PredictedHPRatio < 0.5) { Hints.ActionsToExecute.Push(ActionID.MakeSpell(WAR.AID.ThrillOfBattle), Player, ActionQueue.Priority.Low); Hints.ActionsToExecute.Push(ActionID.MakeSpell(WAR.AID.Equilibrium), Player, ActionQueue.Priority.Low); @@ -204,16 +252,16 @@ private void ExecuteWAR(StrategyValues strategy) private void UseOneMit(float deadline) { - var longmit = GetMitStatus(JobActions.LongMit, 15, deadline); + var longmit = GetMitStatus(JobActions.LongMit.ID, 15, deadline); var rampart = GetMitStatus(ActionID.MakeSpell(ClassShared.AID.Rampart), 20, deadline); - var shortmit = GetMitStatus(JobActions.ShortMit, JobActions.ShortMitDuration, deadline, JobActions.ShortMitCheck); + var shortmit = GetMitStatus(JobActions.ShortMit.ID, JobActions.ShortMit.Duration, deadline, JobActions.ShortMit.CanUse); if (longmit.Active || rampart.Active && shortmit.Active) return; if (longmit.Usable) { - Hints.ActionsToExecute.Push(JobActions.LongMit, Player, ActionQueue.Priority.Low); + Hints.ActionsToExecute.Push(JobActions.LongMit.ID, Player, ActionQueue.Priority.Low); return; } @@ -221,7 +269,7 @@ private void UseOneMit(float deadline) Hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Rampart), Player, ActionQueue.Priority.Low); if (shortmit.Usable) - Hints.ActionsToExecute.Push(JobActions.ShortMit, Player, ActionQueue.Priority.Low); + Hints.ActionsToExecute.Push(JobActions.ShortMit.ID, Player, ActionQueue.Priority.Low); } private (bool Ready, bool Active, bool Usable) GetMitStatus(ActionID action, float actionDuration, float deadline, Func? resourceCheck = null) diff --git a/BossMod/Autorotation/xan/AI/TrackPartyHealth.cs b/BossMod/Autorotation/xan/AI/TrackPartyHealth.cs index 011058b342..7b79e04f82 100644 --- a/BossMod/Autorotation/xan/AI/TrackPartyHealth.cs +++ b/BossMod/Autorotation/xan/AI/TrackPartyHealth.cs @@ -19,6 +19,9 @@ public record struct PartyMemberState public float NoHealStatusRemaining; // Doom (1769 and possibly other statuses) is only removed once a player reaches full HP, must be healed asap public float DoomRemaining; + public Vector2 AveragePosition; + public float MoveDelta; + public DateTime LastCombat; } public record PartyHealthState @@ -34,15 +37,15 @@ public record PartyHealthState public readonly PartyMemberState[] PartyMemberStates = new PartyMemberState[PartyState.MaxAllies]; public PartyHealthState PartyHealth { get; private set; } = new(); + public bool HaveRealPartyMembers { get; private set; } + // looking up this field in sheets is noticeably expensive somehow private static readonly Dictionary _esunaCache = []; private static bool StatusIsRemovable(uint statusID) { if (_esunaCache.TryGetValue(statusID, out var value)) return value; - var check = Utils.StatusIsRemovable(statusID); - _esunaCache[statusID] = check; - return check; + return _esunaCache[statusID] = Utils.StatusIsRemovable(statusID); } private static readonly uint[] NoHealStatuses = [ @@ -115,12 +118,26 @@ public void Update(AIHints Hints) foreach (var caster in World.Party.WithoutSlot(excludeAlliance: true).Where(a => a.CastInfo?.IsSpell(BossMod.WHM.AID.Esuna) ?? false)) esunas.Set(World.Party.FindSlot(caster.CastInfo!.TargetID)); + HaveRealPartyMembers = false; + for (var i = 0; i < PartyState.MaxAllies; i++) { + var shouldSkip = false; + if (i >= PartyState.MaxPartySize) + { + // if we are running content with normal party, either duty support or human players, NPC allies should be ignored entirely + if (HaveRealPartyMembers) + shouldSkip = true; + + // otherwise alliance should be skipped since healing actions generally can't target them + if (i < PartyState.MaxAllianceSize) + shouldSkip = true; + } + var actor = World.Party[i]; ref var state = ref PartyMemberStates[i]; state.Slot = i; - if (actor == null || actor.IsDead || actor.HPMP.MaxHP == 0) + if (actor == null || actor.IsDead || actor.HPMP.MaxHP == 0 || shouldSkip) { state.PredictedHP = state.PredictedHPMissing = 0; state.PredictedHPRatio = state.PendingHPRatio = 1; @@ -146,6 +163,19 @@ public void Update(AIHints Hints) if (s.ID == 1769) state.DoomRemaining = StatusDuration(s.ExpireAt); } + + if (actor.InCombat) + state.LastCombat = World.CurrentTime; + + var pos = actor.Position.ToVec2(); + if (state.AveragePosition == default) + state.AveragePosition = pos; + else + { + state.AveragePosition -= state.AveragePosition * World.Frame.Duration; + state.AveragePosition += pos * World.Frame.Duration; + } + state.MoveDelta = (state.AveragePosition - pos).Length(); } } @@ -160,6 +190,11 @@ public void Update(AIHints Hints) state.PredictedHPRatio -= enemy.AttackStrength; } } + + foreach (var predicted in Hints.PredictedDamage) + foreach (var bit in predicted.players.SetBits()) + PartyMemberStates[bit].PredictedHPRatio -= 0.30f; + PartyHealth = CalculatePartyHealthState(_ => true); } } diff --git a/BossMod/Autorotation/xan/BLU/Basic.cs b/BossMod/Autorotation/xan/BLU/Basic.cs index d7f46757fc..f2e17949df 100644 --- a/BossMod/Autorotation/xan/BLU/Basic.cs +++ b/BossMod/Autorotation/xan/BLU/Basic.cs @@ -1,4 +1,5 @@ using BossMod.BLU; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -45,7 +46,7 @@ public enum GCDPriority : int _ => World.Client.BlueMageSpells.Contains((uint)aid) }; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 25); @@ -73,7 +74,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) var haveModule = Bossmods.ActiveModule?.StateMachine.ActiveState != null; // mortal flame - if (primaryTarget is Actor p && StatusDetails(p, 3643, Player.InstanceID).Left == 0 && Hints.PriorityTargets.Count() == 1 && haveModule) + if (primaryTarget is { } p && StatusDetails(p.Actor, 3643, Player.InstanceID).Left == 0 && Hints.PriorityTargets.Count() == 1 && haveModule) PushGCD(AID.MortalFlame, p, GCDPriority.GCDWithCooldown); if (haveModule && currentHP * 2 < Player.HPMP.MaxHP) @@ -99,8 +100,8 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) // standard filler spells if (HaveSpell(AID.GoblinPunch)) { - if (primaryTarget is Actor t) - Hints.GoalZones.Add(Hints.GoalSingleTarget(t, Positional.Front, 3)); + if (primaryTarget is { } t) + Hints.GoalZones.Add(Hints.GoalSingleTarget(t.Actor, Positional.Front, 3)); PushGCD(AID.GoblinPunch, primaryTarget, GCDPriority.FillerST); } PushGCD(AID.SonicBoom, primaryTarget, GCDPriority.FillerST); @@ -110,7 +111,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) var (poopTarget, poopNum) = SelectTarget(strategy, primaryTarget, 25, (primary, other) => Hints.TargetInAOECircle(other, primary.Position, 6)); if (poopTarget != null && poopNum > 2) { - var scoopNum = Hints.NumPriorityTargetsInAOE(act => StatusDetails(act.Actor, 3636, Player.InstanceID).Left > SpellGCDLength && Hints.TargetInAOECircle(act.Actor, poopTarget.Position, 6)); + var scoopNum = Hints.NumPriorityTargetsInAOE(act => StatusDetails(act.Actor, 3636, Player.InstanceID).Left > SpellGCDLength && Hints.TargetInAOECircle(act.Actor, poopTarget.Actor.Position, 6)); if (scoopNum > 2) PushGCD(AID.DeepClean, poopTarget, GCDPriority.Scoop); PushGCD(AID.PeatPelt, poopTarget, GCDPriority.Poop); @@ -154,12 +155,12 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushOGCD(AID.JKick, primaryTarget); } - private void TankSpecific(Actor? primaryTarget) + private void TankSpecific(Enemy? primaryTarget) { if (HaveSpell(AID.Devour) && !CanFitGCD(StatusLeft(SID.HPBoost), 1)) { - if (primaryTarget is Actor t) - Hints.GoalZones.Add(Hints.GoalSingleTarget(t, 3)); + if (primaryTarget is { } t) + Hints.GoalZones.Add(Hints.GoalSingleTarget(t.Actor, 3)); PushGCD(AID.Devour, primaryTarget, GCDPriority.BuffRefresh); } @@ -168,10 +169,7 @@ private void TankSpecific(Actor? primaryTarget) PushGCD(AID.ChelonianGate, Player, GCDPriority.BuffRefresh); if (Player.FindStatus(2497u) != null) - { - Service.Log($"executing Divine Cataract"); PushGCD(AID.DivineCataract, Player, GCDPriority.SurpanakhaRepeat); - } } public Mimicry CurrentMimic() diff --git a/BossMod/Autorotation/xan/Basexan.cs b/BossMod/Autorotation/xan/Basexan.cs index 10c1cce5f8..5d6a650423 100644 --- a/BossMod/Autorotation/xan/Basexan.cs +++ b/BossMod/Autorotation/xan/Basexan.cs @@ -1,8 +1,10 @@ -namespace BossMod.Autorotation.xan; +using static BossMod.AIHints; + +namespace BossMod.Autorotation.xan; public enum Targeting { Manual, Auto, AutoPrimary, AutoTryPri } public enum OffensiveStrategy { Automatic, Delay, Force } -public enum AOEStrategy { ST, AOE, ForceAOE, ForceST } +public enum AOEStrategy { AOE, ST, ForceAOE, ForceST } public enum SharedTrack { Targeting, AOE, Buffs, Count } @@ -30,12 +32,10 @@ public abstract class Basexan(RotationModuleManager manager, Actor protected float RaidBuffsLeft { get; private set; } protected float DowntimeIn { get; private set; } protected float? UptimeIn { get; private set; } - /// - /// Player's "actual" target. Is guaranteed to be an enemy. - /// - protected Actor? PlayerTarget { get; private set; } + protected Enemy? PlayerTarget { get; private set; } protected float? CountdownRemaining => World.Client.CountdownRemaining; + protected float AnimLock => World.Client.AnimationLock; protected float AttackGCDLength => ActionSpeed.GCDRounded(World.Client.PlayerStats.SkillSpeed, World.Client.PlayerStats.Haste, Player.Level); protected float SpellGCDLength => ActionSpeed.GCDRounded(World.Client.PlayerStats.SpellSpeed, World.Client.PlayerStats.Haste, Player.Level); @@ -50,7 +50,7 @@ public abstract class Basexan(RotationModuleManager manager, Actor protected bool OnCooldown(AID aid) => MaxChargesIn(aid) > 0; public bool CanWeave(float cooldown, float actionLock, int extraGCDs = 0, float extraFixedDelay = 0) - => MathF.Max(cooldown, World.Client.AnimationLock) + actionLock + AnimationLockDelay <= GCD + GCDLength * extraGCDs + extraFixedDelay; + => MathF.Max(cooldown, AnimLock) + actionLock + AnimationLockDelay <= GCD + GCDLength * extraGCDs + extraFixedDelay; public bool CanWeave(AID aid, int extraGCDs = 0, float extraFixedDelay = 0) { @@ -59,6 +59,11 @@ public bool CanWeave(AID aid, int extraGCDs = 0, float extraFixedDelay = 0) return false; var def = ActionDefinitions.Instance[ActionID.MakeSpell(aid)]!; + + // amnesia check + if (def.Category == ActionCategory.Ability && Player.FindStatus(1092) != null) + return false; + return CanWeave(ReadyIn(aid), def.InstantAnimLock, extraGCDs, extraFixedDelay); } @@ -71,6 +76,11 @@ public bool CanWeave(AID aid, int extraGCDs = 0, float extraFixedDelay = 0) protected void PushGCD

(AID aid, Actor? target, P priority, float delay = 0) where P : Enum => PushGCD(aid, target, (int)(object)priority, delay); + protected void PushGCD

(AID aid, Enemy? target, P priority, float delay = 0) where P : Enum + => PushGCD(aid, target?.Actor, (int)(object)priority, delay); + + protected void PushGCD(AID aid, Enemy? target, int priority = 2, float delay = 0) => PushGCD(aid, target?.Actor, priority, delay); + protected void PushGCD(AID aid, Actor? target, int priority = 2, float delay = 0) { if (priority == 0) @@ -86,6 +96,11 @@ protected void PushGCD(AID aid, Actor? target, int priority = 2, float delay = 0 protected void PushOGCD

(AID aid, Actor? target, P priority, float delay = 0) where P : Enum => PushOGCD(aid, target, (int)(object)priority, delay); + protected void PushOGCD

(AID aid, Enemy? target, P priority, float delay = 0) where P : Enum + => PushOGCD(aid, target?.Actor, (int)(object)priority, delay); + + protected void PushOGCD(AID aid, Enemy? target, int priority = 1, float delay = 0) => PushOGCD(aid, target?.Actor, priority, delay); + protected void PushOGCD(AID aid, Actor? target, int priority = 1, float delay = 0) { if (priority == 0) @@ -134,20 +149,15 @@ protected bool PushAction(AID aid, Actor? target, float priority, float delay) /// Targeting strategy /// Player's current target - may be null /// Maximum distance from the player to search for a candidate target - protected void SelectPrimaryTarget(StrategyValues strategy, ref Actor? primaryTarget, float range) + protected void SelectPrimaryTarget(StrategyValues strategy, ref Enemy? primaryTarget, float range) { var t = strategy.Option(SharedTrack.Targeting).As(); - if (!IsValidEnemy(primaryTarget)) - primaryTarget = null; - - PlayerTarget = primaryTarget; - if (t is Targeting.Auto or Targeting.AutoTryPri) { if (Player.DistanceToHitbox(primaryTarget) > range) { - var newTarget = Hints.PriorityTargets.FirstOrDefault(x => Player.DistanceToHitbox(x.Actor) <= range)?.Actor; + var newTarget = Hints.PriorityTargets.FirstOrDefault(x => Player.DistanceToHitbox(x.Actor) <= range); if (newTarget != null) primaryTarget = newTarget; } @@ -157,19 +167,19 @@ protected void SelectPrimaryTarget(StrategyValues strategy, ref Actor? primaryTa protected delegate bool PositionCheck(Actor playerTarget, Actor targetToTest); protected delegate P PriorityFunc

(int totalTargets, Actor primaryTarget); - protected (Actor? Best, int Targets) SelectTarget( + protected (Enemy? Best, int Targets) SelectTarget( StrategyValues strategy, - Actor? primaryTarget, + Enemy? primaryTarget, float range, PositionCheck isInAOE ) => SelectTarget(strategy, primaryTarget, range, isInAOE, (numTargets, _) => numTargets, a => a); - protected (Actor? Best, int Targets) SelectTargetByHP(StrategyValues strategy, Actor? primaryTarget, float range, PositionCheck isInAOE) + protected (Enemy? Best, int Targets) SelectTargetByHP(StrategyValues strategy, Enemy? primaryTarget, float range, PositionCheck isInAOE) => SelectTarget(strategy, primaryTarget, range, isInAOE, (numTargets, actor) => (numTargets, numTargets > 2 ? actor.HPMP.CurHP : 0), args => args.numTargets); - protected (Actor? Best, int Priority) SelectTarget

( + protected (Enemy? Best, int Priority) SelectTarget

( StrategyValues strategy, - Actor? primaryTarget, + Enemy? primaryTarget, float range, PositionCheck isInAOE, PriorityFunc

prioritize, @@ -195,17 +205,17 @@ P targetPrio(Actor potentialTarget) var (newtarget, newprio) = targeting switch { - Targeting.Auto => FindBetterTargetBy(primaryTarget, range, targetPrio), + Targeting.Auto => FindBetterTargetBy(primaryTarget?.Actor, range, targetPrio), Targeting.AutoPrimary => primaryTarget == null ? (null, default) : FindBetterTargetBy( - primaryTarget, + primaryTarget.Actor, range, targetPrio, - enemy => isInAOE(enemy.Actor, primaryTarget) + enemy => isInAOE(enemy.Actor, primaryTarget.Actor) ), - _ => (primaryTarget, primaryTarget == null ? default : targetPrio(primaryTarget)) + _ => (primaryTarget?.Actor, primaryTarget == null ? default : targetPrio(primaryTarget.Actor)) }; var newnewprio = simplify(newprio); - return (newnewprio > 0 ? newtarget : null, newnewprio); + return (newnewprio > 0 ? Hints.FindEnemy(newtarget) : null, newnewprio); } ///

@@ -218,21 +228,22 @@ P targetPrio(Actor potentialTarget) /// /// /// - protected (Actor? Target, P Timer) SelectDotTarget

(StrategyValues strategy, Actor? initial, Func getTimer, int maxAllowedTargets) where P : struct, IComparable + protected (Enemy? Target, P Timer) SelectDotTarget

(StrategyValues strategy, Enemy? initial, Func getTimer, int maxAllowedTargets) where P : struct, IComparable { + var forbidden = initial?.ForbidDOTs ?? false; switch (strategy.Targeting()) { case Targeting.Manual: case Targeting.AutoPrimary: - return (initial, getTimer(initial)); + return forbidden ? (null, getTimer(null)) : (initial, getTimer(initial?.Actor)); case Targeting.AutoTryPri: if (initial != null) - return (initial, getTimer(initial)); + return forbidden ? (null, getTimer(null)) : (initial, getTimer(initial?.Actor)); break; } var newTarget = initial; - var initialTimer = getTimer(initial); + var initialTimer = getTimer(initial?.Actor); var newTimer = initialTimer; var numTargets = 0; @@ -248,7 +259,7 @@ P targetPrio(Actor potentialTarget) var thisTimer = getTimer(dotTarget.Actor); if (thisTimer.CompareTo(newTimer) < 0) { - newTarget = dotTarget.Actor; + newTarget = dotTarget; newTimer = thisTimer; } } @@ -256,12 +267,29 @@ P targetPrio(Actor potentialTarget) return (newTarget, newTimer); } - protected void GoalZoneCombined(float range, Func fAoe, int minAoe, Positional pos = Positional.Any) + // used for casters that don't have a separate maximize-AOE function + protected void GoalZoneSingle(float range) { + if (PlayerTarget != null) + Hints.GoalZones.Add(Hints.GoalSingleTarget(PlayerTarget.Actor, range)); + } + + protected void GoalZoneCombined(StrategyValues strategy, float range, Func fAoe, AID firstUnlockedAoeAction, int minAoe, Positional positional = Positional.Any, float? maximumActionRange = null) + { + if (!strategy.AOEOk() || !Unlocked(firstUnlockedAoeAction)) + minAoe = 50; + if (PlayerTarget == null) - Hints.GoalZones.Add(fAoe); + { + if (minAoe < 50) + Hints.GoalZones.Add(fAoe); + } else - Hints.GoalZones.Add(Hints.GoalCombined(Hints.GoalSingleTarget(PlayerTarget, pos, range), fAoe, minAoe)); + { + Hints.GoalZones.Add(Hints.GoalCombined(Hints.GoalSingleTarget(PlayerTarget.Actor, positional, range), fAoe, minAoe)); + if (maximumActionRange is float r) + Hints.GoalZones.Add(Hints.GoalSingleTarget(PlayerTarget.Actor, r, 0.5f)); + } } protected int NumMeleeAOETargets(StrategyValues strategy) => NumNearbyTargets(strategy, 5); @@ -290,7 +318,7 @@ protected int AdjustNumTargets(StrategyValues strategy, int reported) /// protected virtual float GetCastTime(AID aid) => SwiftcastLeft > GCD ? 0 : ActionDefinitions.Instance.Spell(aid)!.CastTime * GCDLength / 2.5f; - protected float NextCastStart => World.Client.AnimationLock > GCD ? World.Client.AnimationLock + AnimationLockDelay : GCD; + protected float NextCastStart => AnimLock > GCD ? AnimLock + AnimationLockDelay : GCD; protected float GetSlidecastTime(AID aid) => MathF.Max(0, GetCastTime(aid) - 0.5f); protected float GetSlidecastEnd(AID aid) => NextCastStart + GetSlidecastTime(aid); @@ -301,16 +329,14 @@ protected virtual bool CanCast(AID aid) if (t == 0) return true; - return NextCastStart + t <= ForceMovementIn; + return NextCastStart + t <= MaxCastTime; } - protected float ForceMovementIn; + protected float MaxCastTime; protected bool Unlocked(AID aid) => ActionUnlocked(ActionID.MakeSpell(aid)); protected bool Unlocked(TraitID tid) => TraitUnlocked((uint)(object)tid); - private static bool IsValidEnemy(Actor? actor) => actor != null && !actor.IsAlly; - protected Positional GetCurrentPositional(Actor target) => (Player.Position - target.Position).Normalized().Dot(target.Rotation.ToDirection()) switch { < -0.7071068f => Positional.Rear, @@ -321,8 +347,9 @@ protected virtual bool CanCast(AID aid) protected bool NextPositionalImminent; protected bool NextPositionalCorrect; - protected void UpdatePositionals(Actor? target, ref (Positional pos, bool imm) positional, bool trueNorth) + protected void UpdatePositionals(Enemy? enemy, ref (Positional pos, bool imm) positional, bool trueNorth) { + var target = enemy?.Actor; if ((target?.Omnidirectional ?? true) || target?.TargetID == Player.InstanceID && target?.CastInfo == null && positional.pos != Positional.Front && target?.NameID != 541) positional = (Positional.Any, false); @@ -333,20 +360,42 @@ protected void UpdatePositionals(Actor? target, ref (Positional pos, bool imm) p Positional.Rear => target.Rotation.ToDirection().Dot((Player.Position - target.Position).Normalized()) < -0.7071068f, _ => true }; - Manager.Hints.RecommendedPositional = (target, positional.pos, NextPositionalImminent, NextPositionalCorrect); + Hints.RecommendedPositional = (target, positional.pos, NextPositionalImminent, NextPositionalCorrect); + } + + private readonly SmartRotationConfig _smartrot = Service.Config.Get(); + + private void EstimateCastTime() + { + MaxCastTime = Hints.MaxCastTimeEstimate; + + if (Player.PendingKnockbacks.Count > 0) + { + MaxCastTime = 0f; + return; + } + + var forbiddenDir = Hints.ForbiddenDirections.Where(d => Player.Rotation.AlmostEqual(d.center, d.halfWidth.Rad)).Select(d => d.activation).DefaultIfEmpty(DateTime.MinValue).Min(); + if (forbiddenDir > World.CurrentTime) + { + var cushion = _smartrot.MinTimeToAvoid; + var gazeIn = MathF.Max(0, (float)(forbiddenDir - World.CurrentTime).TotalSeconds - cushion); + MaxCastTime = MathF.Min(MaxCastTime, gazeIn); + } } public sealed override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { NextGCD = default; NextGCDPrio = 0; + PlayerTarget = Hints.FindEnemy(primaryTarget); - var pelo = Player.FindStatus(BossMod.BRD.SID.Peloton); + var pelo = Player.FindStatus(ClassShared.SID.Peloton); PelotonLeft = pelo != null ? StatusDuration(pelo.Value.ExpireAt) : 0; - SwiftcastLeft = StatusLeft(BossMod.WHM.SID.Swiftcast); - TrueNorthLeft = StatusLeft(BossMod.DRG.SID.TrueNorth); + SwiftcastLeft = MathF.Max(StatusLeft(ClassShared.SID.Swiftcast), StatusLeft(ClassShared.SID.LostChainspell)); + TrueNorthLeft = StatusLeft(ClassShared.SID.TrueNorth); - ForceMovementIn = Hints.MaxCastTimeEstimate; + EstimateCastTime(); AnimationLockDelay = estimatedAnimLockDelay; CombatTimer = (float)(World.CurrentTime - Manager.CombatStart).TotalSeconds; @@ -367,25 +416,63 @@ public sealed override void Execute(StrategyValues strategy, Actor? primaryTarge MP = (uint)Math.Clamp(Player.PredictedMPRaw, 0, 10000); if (Player.MountId is not (103 or 117 or 128)) - Exec(strategy, primaryTarget); + Exec(strategy, PlayerTarget); } + // other classes have timed personal buffs to plan around, like blm leylines, mch overheat, gnb nomercy + // war could also be here but i dont have a war rotation + private bool IsSelfish(Class cls) => cls is Class.VPR or Class.SAM or Class.WHM or Class.SGE; + private new (float Left, float In) EstimateRaidBuffTimings(Actor? primaryTarget) { - // striking dummy that spawns in Explorer Mode - if (primaryTarget?.OID != 0x2DE0) - return (Bossmods.RaidCooldowns.DamageBuffLeft(Player), Bossmods.RaidCooldowns.NextDamageBuffIn2()); + if (Bossmods.ActiveModule?.Info?.GroupType is BossModuleInfo.GroupType.BozjaDuel && IsSelfish(Player.Class)) + return (float.MaxValue, 0); + + // level 100 stone sky sea + if (primaryTarget?.OID == 0x41CD) + { + // hack for a dummy: expect that raidbuffs appear at 7.8s and then every 120s + var cycleTime = CombatTimer - 7.8f; + if (cycleTime < 0) + return (0, 7.8f - CombatTimer); // very beginning of a fight + + cycleTime %= 120; + return cycleTime < 20 ? (20 - cycleTime, 0) : (0, 120 - cycleTime); + } - // hack for a dummy: expect that raidbuffs appear at 7.8s and then every 120s - var cycleTime = CombatTimer - 7.8f; - if (cycleTime < 0) - return (0, 7.8f - CombatTimer); // very beginning of a fight + var buffsIn = Bossmods.RaidCooldowns.NextDamageBuffIn2(); + if (buffsIn == null) + { + if (CombatTimer < 7.8f && World.Party.WithoutSlot(false, true, true).Skip(1).Any(HavePartyBuff)) + buffsIn = 7.8f - CombatTimer; + else + buffsIn = float.MaxValue; + } - cycleTime %= 120; - return cycleTime < 20 ? (20 - cycleTime, 0) : (0, 120 - cycleTime); + return (Bossmods.RaidCooldowns.DamageBuffLeft(Player), buffsIn.Value); } - public abstract void Exec(StrategyValues strategy, Actor? primaryTarget); + private bool HavePartyBuff(Actor player) => player.Class switch + { + Class.MNK => player.Level >= 70, // brotherhood + Class.DRG => player.Level >= 52, // battle litany + Class.NIN => player.Level >= 45, // mug/dokumori - level check is for suiton/huton, which grant Shadow Walker + Class.RPR => player.Level >= 72, // arcane circle + + Class.SMN => player.Level >= 66, // searing light + Class.RDM => player.Level >= 58, // embolden + Class.PCT => player.Level >= 70, // starry muse + + Class.BRD => player.Level >= 50, // battle voice - not counting songs since they are permanent kinda + Class.DNC => player.Level >= 70, // tech finish + + Class.SCH => player.Level >= 66, // chain + Class.AST => player.Level >= 50, // divination + + _ => false + }; + + public abstract void Exec(StrategyValues strategy, Enemy? primaryTarget); protected (float Left, int Stacks) Status(SID status) where SID : Enum => Player.FindStatus(status) is ActorStatus s ? (StatusDuration(s.ExpireAt), s.Extra & 0xFF) : (0, 0); protected float StatusLeft(SID status) where SID : Enum => Status(status).Left; @@ -414,8 +501,8 @@ public static RotationModuleDefinition DefineSharedTA(this RotationModuleDefinit .AddOption(xan.Targeting.AutoTryPri, "AutoTryPri", "Automatically select best target for AOE actions - if player has a target, ensure that target is hit"); def.Define(SharedTrack.AOE).As("AOE") - .AddOption(AOEStrategy.ST, "ST", "Use single-target actions") .AddOption(AOEStrategy.AOE, "AOE", "Use AOE actions if beneficial") + .AddOption(AOEStrategy.ST, "ST", "Use single-target actions") .AddOption(AOEStrategy.ForceAOE, "ForceAOE", "Always use AOE actions, even on one target") .AddOption(AOEStrategy.ForceST, "ForceST", "Forbid any action that can hit multiple targets"); @@ -434,4 +521,5 @@ public static RotationModuleDefinition.ConfigRef DefineSimple public static Targeting Targeting(this StrategyValues strategy) => strategy.Option(SharedTrack.Targeting).As(); public static OffensiveStrategy Simple(this StrategyValues strategy, Index track) where Index : Enum => strategy.Option(track).As(); public static bool BuffsOk(this StrategyValues strategy) => strategy.Option(SharedTrack.Buffs).As() != OffensiveStrategy.Delay; + public static bool AOEOk(this StrategyValues strategy) => strategy.AOE() is AOEStrategy.AOE or AOEStrategy.ForceAOE; } diff --git a/BossMod/Autorotation/xan/Casters/BLM.cs b/BossMod/Autorotation/xan/Casters/BLM.cs index c28aa8f015..db7a66fa48 100644 --- a/BossMod/Autorotation/xan/Casters/BLM.cs +++ b/BossMod/Autorotation/xan/Casters/BLM.cs @@ -1,5 +1,6 @@ using BossMod.BLM; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -47,7 +48,7 @@ public static RotationModuleDefinition Definition() public int MaxPolyglot => Unlocked(TraitID.EnhancedPolyglotII) ? 3 : Unlocked(TraitID.EnhancedPolyglot) ? 2 : 1; public int MaxHearts => Unlocked(TraitID.UmbralHeart) ? 3 : 0; - private Actor? BestAOETarget; + private Enemy? BestAOETarget; private int NumAOETargets; protected override float GetCastTime(AID aid) @@ -55,6 +56,9 @@ protected override float GetCastTime(AID aid) if (TriplecastLeft > GCD) return 0; + if (aid == AID.Despair && Unlocked(TraitID.EnhancedAstralFire)) + return 0; + var aspect = ActionDefinitions.Instance.Spell(aid)!.Aspect; if (aid == AID.Fire3 && Firestarter > GCD @@ -72,7 +76,7 @@ protected override float GetCastTime(AID aid) return castTime; } - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, range: 25); @@ -121,12 +125,10 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) return; } - if (PlayerTarget != null) - Hints.GoalZones.Add(Hints.GoalSingleTarget(PlayerTarget, 25)); + GoalZoneSingle(25); - var ll = World.Actors.FirstOrDefault(x => x.OID == 0x179 && x.OwnerID == Player.InstanceID); - if (ll != null) - Hints.GoalZones.Add(p => (p - ll.Position).Length() <= 3 ? 0.5f : 0); + if (Player.InCombat && World.Actors.FirstOrDefault(x => x.OID == 0x179 && x.OwnerID == Player.InstanceID) is Actor ll) + Hints.GoalZones.Add(p => p.InCircle(ll.Position, 3) ? 0.5f : 0); if (Unlocked(AID.Swiftcast)) PushOGCD(AID.Swiftcast, Player); @@ -167,7 +169,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.Scathe, primaryTarget); } - private void FirePhase(StrategyValues strategy, Actor? primaryTarget) + private void FirePhase(StrategyValues strategy, Enemy? primaryTarget) { if (NumAOETargets > 2) { @@ -182,7 +184,7 @@ private void FirePhase(StrategyValues strategy, Actor? primaryTarget) FirePhaseST(strategy, primaryTarget); } - private void FirePhaseST(StrategyValues strategy, Actor? primaryTarget) + private void FirePhaseST(StrategyValues strategy, Enemy? primaryTarget) { if (Thunderhead > GCD && TargetThunderLeft < 5 && ElementLeft > GCDLength + AnimationLockDelay) PushGCD(AID.Thunder1, primaryTarget); @@ -292,7 +294,7 @@ private void FirePhaseAOE(StrategyValues strategy) TryInstantCast(strategy, BestAOETarget); } - private void FireAOELowLevel(StrategyValues strategy, Actor? primaryTarget) + private void FireAOELowLevel(StrategyValues strategy, Enemy? primaryTarget) { if (Thunderhead > GCD && TargetThunderLeft < 5) { @@ -313,7 +315,7 @@ private void FireAOELowLevel(StrategyValues strategy, Actor? primaryTarget) } } - private void IcePhase(StrategyValues strategy, Actor? primaryTarget) + private void IcePhase(StrategyValues strategy, Enemy? primaryTarget) { if (NumAOETargets > 2 && Unlocked(AID.Blizzard2)) { @@ -326,7 +328,7 @@ private void IcePhase(StrategyValues strategy, Actor? primaryTarget) IcePhaseST(strategy, primaryTarget); } - private void IcePhaseST(StrategyValues strategy, Actor? primaryTarget) + private void IcePhaseST(StrategyValues strategy, Enemy? primaryTarget) { if (Thunderhead > GCD && TargetThunderLeft < 5 && ElementLeft > GCDLength + AnimationLockDelay) PushGCD(AID.Thunder1, primaryTarget); @@ -358,7 +360,7 @@ private void IcePhaseST(StrategyValues strategy, Actor? primaryTarget) } - private void IcePhaseAOE(StrategyValues strategy, Actor? primaryTarget) + private void IcePhaseAOE(StrategyValues strategy, Enemy? primaryTarget) { if (Ice == 0) { @@ -373,7 +375,7 @@ private void IcePhaseAOE(StrategyValues strategy, Actor? primaryTarget) TryInstantCast(strategy, primaryTarget); } - private void IceAOELowLevel(StrategyValues strategy, Actor? primaryTarget) + private void IceAOELowLevel(StrategyValues strategy, Enemy? primaryTarget) { if (Thunderhead > GCD && TargetThunderLeft < 5) { @@ -394,7 +396,7 @@ private void IceAOELowLevel(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.Blizzard2, BestAOETarget); } - private void Choose(AID st, AID aoe, Actor? primaryTarget, int additionalPrio = 0) + private void Choose(AID st, AID aoe, Enemy? primaryTarget, int additionalPrio = 0) { if (NumAOETargets > 2 && Unlocked(aoe)) PushGCD(aoe, BestAOETarget, additionalPrio + 1); @@ -402,7 +404,7 @@ private void Choose(AID st, AID aoe, Actor? primaryTarget, int additionalPrio = PushGCD(st, primaryTarget, additionalPrio + 1); } - private void TryInstantCast(StrategyValues strategy, Actor? primaryTarget, bool useFirestarter = true, bool useThunderhead = true, bool usePolyglot = true) + private void TryInstantCast(StrategyValues strategy, Enemy? primaryTarget, bool useFirestarter = true, bool useThunderhead = true, bool usePolyglot = true) { var tp = useThunderhead && Thunderhead > GCD; @@ -419,7 +421,7 @@ private void TryInstantCast(StrategyValues strategy, Actor? primaryTarget, bool PushGCD(AID.Fire3, primaryTarget); } - private void TryInstantOrTranspose(StrategyValues strategy, Actor? primaryTarget, bool useThunderhead = true) + private void TryInstantOrTranspose(StrategyValues strategy, Enemy? primaryTarget, bool useThunderhead = true) { if (useThunderhead && Thunderhead > GCD) Choose(AID.Thunder1, AID.Thunder2, primaryTarget); @@ -434,8 +436,8 @@ private void TryInstantOrTranspose(StrategyValues strategy, Actor? primaryTarget private bool ShouldTriplecast(StrategyValues strategy) => TriplecastLeft == 0 && (ShouldUseLeylines(strategy) || InLeyLines); private bool ShouldUseLeylines(StrategyValues strategy, int extraGCDs = 0) - => CanWeave(AID.LeyLines, extraGCDs) - && ForceMovementIn >= 30 + => CanWeave(MaxChargesIn(AID.LeyLines), extraGCDs) + && MaxCastTime >= 30 && strategy.Option(SharedTrack.Buffs).As() != OffensiveStrategy.Delay; private bool ShouldTranspose(StrategyValues strategy) diff --git a/BossMod/Autorotation/xan/Casters/PCT.cs b/BossMod/Autorotation/xan/Casters/PCT.cs index 514b8f9526..7456a3fe4a 100644 --- a/BossMod/Autorotation/xan/Casters/PCT.cs +++ b/BossMod/Autorotation/xan/Casters/PCT.cs @@ -1,5 +1,6 @@ using BossMod.PCT; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -27,11 +28,14 @@ public static RotationModuleDefinition Definition() public int Palette; // 0-100 public int Paint; // 0-5 - public bool Creature; - public bool Weapon; - public bool Landscape; - public bool Moogle; - public bool Madeen; + + public bool PomClawMuse => CanvasFlags.HasFlag(CanvasFlags.Pom) || CanvasFlags.HasFlag(CanvasFlags.Claw); + public bool WingFangMuse => CanvasFlags.HasFlag(CanvasFlags.Wing) || CanvasFlags.HasFlag(CanvasFlags.Maw); + public bool Portrait => CreatureFlags.HasFlag(CreatureFlags.MooglePortait) || CreatureFlags.HasFlag(CreatureFlags.MadeenPortrait); + + public bool CreaturePainted => PomClawMuse || WingFangMuse; + public bool WeaponPainted => CanvasFlags.HasFlag(CanvasFlags.Weapon); + public bool LandscapePainted => CanvasFlags.HasFlag(CanvasFlags.Landscape); public bool Monochrome; public CreatureFlags CreatureFlags; public CanvasFlags CanvasFlags; @@ -55,8 +59,8 @@ public enum AetherHues : uint public int NumAOETargets; public int NumLineTargets; - private Actor? BestAOETarget; - private Actor? BestLineTarget; + private Enemy? BestAOETarget; + private Enemy? BestLineTarget; public enum GCDPriority : int { @@ -69,23 +73,51 @@ public enum GCDPriority : int private float GetApplicationDelay(AID action) => action switch { AID.RainbowDrip => 1.24f, + AID.FireInRed => 0.84f, AID.ClawedMuse => 0.98f, AID.FangedMuse => 1.16f, + AID.MogOfTheAges => 1.15f, + AID.RetributionOfTheMadeen => 1.30f, _ => 0 }; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public const uint LeylinesOID = 0x6DF; + + private AID BestLivingMuse + { + get + { + if (CanvasFlags.HasFlag(CanvasFlags.Pom)) + return AID.PomMuse; + if (CanvasFlags.HasFlag(CanvasFlags.Wing)) + return AID.WingedMuse; + if (CanvasFlags.HasFlag(CanvasFlags.Claw)) + return AID.ClawedMuse; + if (CanvasFlags.HasFlag(CanvasFlags.Maw)) + return AID.FangedMuse; + return AID.None; + } + } + + private AID BestPortrait + { + get + { + if (CreatureFlags.HasFlag(CreatureFlags.MooglePortait)) + return AID.MogOfTheAges; + if (CreatureFlags.HasFlag(CreatureFlags.MadeenPortrait)) + return AID.RetributionOfTheMadeen; + return AID.None; + } + } + + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 25); var gauge = World.Client.GetGauge(); Palette = gauge.PalleteGauge; Paint = gauge.Paint; - Creature = gauge.CreatureMotifDrawn; - Weapon = gauge.WeaponMotifDrawn; - Landscape = gauge.LandscapeMotifDrawn; - Moogle = gauge.MooglePortraitReady; - Madeen = gauge.MadeenPortraitReady; CreatureFlags = gauge.CreatureFlags; CanvasFlags = gauge.CanvasFlags; @@ -111,27 +143,32 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (motifOk) { - if (!Creature && Unlocked(AID.CreatureMotif)) + if (!CreaturePainted && Unlocked(AID.CreatureMotif)) PushGCD(AID.CreatureMotif, Player, GCDPriority.Standard); - if (!Weapon && Unlocked(AID.WeaponMotif) && HammerTime.Left == 0) + if (!WeaponPainted && Unlocked(AID.WeaponMotif) && HammerTime.Left == 0) PushGCD(AID.WeaponMotif, Player, GCDPriority.Standard); - if (!Landscape && Unlocked(AID.LandscapeMotif) && StarryMuseLeft == 0) + if (!LandscapePainted && Unlocked(AID.LandscapeMotif) && StarryMuseLeft == 0) PushGCD(AID.LandscapeMotif, Player, GCDPriority.Standard); } if (CountdownRemaining > 0) { - if (CountdownRemaining <= GetCastTime(AID.RainbowDrip)) + if (CountdownRemaining <= GetCastTime(AID.RainbowDrip) + GetApplicationDelay(AID.RainbowDrip)) PushGCD(AID.RainbowDrip, primaryTarget, GCDPriority.Standard); - if (CountdownRemaining <= GetCastTime(AID.FireInRed)) + if (CountdownRemaining <= GetCastTime(AID.FireInRed) + GetApplicationDelay(AID.FireInRed)) PushGCD(AID.FireInRed, primaryTarget, GCDPriority.Standard); return; } + GoalZoneSingle(25); + + if (Player.InCombat && World.Actors.FirstOrDefault(x => x.OID is LeylinesOID && x.OwnerID == Player.InstanceID) is Actor ll) + Hints.GoalZones.Add(p => p.InCircle(ll.Position, 8) ? 0.5f : 0); + if (!Player.InCombat && primaryTarget != null && Paint == 0) PushGCD(AID.RainbowDrip, primaryTarget, GCDPriority.Standard); @@ -140,8 +177,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (ShouldWeapon(strategy)) PushOGCD(AID.StrikingMuse, Player); - if (CanvasFlags.HasFlag(CanvasFlags.Pom)) - PushOGCD(AID.PomMuse, BestAOETarget); + PushOGCD(BestLivingMuse, BestAOETarget); if (ShouldLandscape(strategy)) PushOGCD(AID.StarryMuse, Player, 2); @@ -149,14 +185,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (ShouldSubtract(strategy)) PushOGCD(AID.SubtractivePalette, Player); - if (ShouldCreature(strategy)) - PushOGCD(AID.LivingMuse, BestAOETarget); - - if (ShouldMog(strategy)) - PushOGCD(AID.MogOfTheAges, BestLineTarget); - - if (Madeen) - PushOGCD(AID.RetributionOfTheMadeen, BestLineTarget); + PushOGCD(BestPortrait, BestLineTarget); if (Player.HPMP.CurMP <= 7000) PushOGCD(AID.LucidDreaming, Player); @@ -176,7 +205,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (RainbowBright > GCD) PushGCD(AID.RainbowDrip, BestLineTarget, GCDPriority.Standard); - var shouldWing = WingPlanned(strategy); + var shouldWing = ShouldPaintInOpener(strategy); // hardcasting wing motif is #1 prio in opener if (shouldWing) @@ -231,12 +260,12 @@ private bool IsMotifOk(StrategyValues strategy) } // only relevant during opener - private bool WingPlanned(StrategyValues strategy) + private bool ShouldPaintInOpener(StrategyValues strategy) { if (strategy.Option(Track.Motif).As() != MotifStrategy.Combat) return false; - return PomOnly && !Creature && CanWeave(AID.LivingMuse, 0, extraFixedDelay: 4); + return !WingFangMuse && BestPortrait == AID.None && (CreatureFlags.HasFlag(CreatureFlags.Pom) || CreatureFlags.HasFlag(CreatureFlags.Claw)) && CanWeave(AID.LivingMuse, 0, extraFixedDelay: 4) && CanWeave(AID.MogOfTheAges, 5); } protected override float GetCastTime(AID aid) => aid switch @@ -266,6 +295,8 @@ private void Hammer(StrategyValues strategy) PushGCD(AID.HammerStamp, BestAOETarget, prio); } + private bool PaintOvercap => Paint == 5 && Hues == AetherHues.Two; + private void Holy(StrategyValues strategy) { if (Paint == 0) @@ -276,12 +307,12 @@ private void Holy(StrategyValues strategy) // use to weave in opener if (ShouldSubtract(strategy, 1)) prio = GCDPriority.Standard; - if (CombatTimer < 10 && !CreatureFlags.HasFlag(CreatureFlags.Pom)) + if (CombatTimer < 10 && !CreatureFlags.HasFlag(CreatureFlags.Pom) && CanvasFlags.HasFlag(CanvasFlags.Pom) && CanWeave(AID.LivingMuse, 1)) prio = GCDPriority.Standard; // use comet to prevent overcap or during buffs // regular holy can be overcapped without losing dps - if (Monochrome && (Paint == 5 || RaidBuffsLeft > GCD)) + if (Monochrome && (PaintOvercap || RaidBuffsLeft > GCD)) prio = GCDPriority.Standard; // holy always a gain in aoe @@ -291,27 +322,11 @@ private void Holy(StrategyValues strategy) PushGCD(Monochrome ? AID.CometInBlack : AID.HolyInWhite, BestAOETarget, prio); } - private bool PomOnly => CreatureFlags.HasFlag(CreatureFlags.Pom) && !CreatureFlags.HasFlag(CreatureFlags.Wings); - private bool ShouldWeapon(StrategyValues strategy) { // ensure muse alignment // ReadyIn will return float.max if not unlocked so no additional check needed - return Weapon && ReadyIn(AID.StarryMuse) is < 10 or > 60; - } - - private bool ShouldCreature(StrategyValues strategy) - { - // triggers native autotarget if BestAOETarget is null because LivingMuse is self targeted and all the actual muse actions are not - // TODO figure out buff timing, this code always just sends it - return Creature && BestAOETarget != null; - } - - private bool ShouldMog(StrategyValues strategy) - { - // ensure muse alignment - moogle takes two 40s charges to rebuild - // TODO fix this for madeen, i think we swap between mog/madeen every 2min? - return Moogle && (RaidBuffsLeft > 0 || ReadyIn(AID.StarryMuse) > 80); + return WeaponPainted && ReadyIn(AID.StarryMuse) is < 10 or > 60; } private bool ShouldLandscape(StrategyValues strategy, int gcdsAhead = 0) @@ -319,10 +334,10 @@ private bool ShouldLandscape(StrategyValues strategy, int gcdsAhead = 0) if (!strategy.BuffsOk()) return false; - if (CombatTimer < 10 && !CanvasFlags.HasFlag(CanvasFlags.Wing)) + if (CombatTimer < 10 && !WingFangMuse) return false; - return Landscape && CanWeave(AID.StarryMuse, gcdsAhead); + return LandscapePainted && CanWeave(AID.StarryMuse, gcdsAhead); } private bool ShouldSubtract(StrategyValues strategy, int gcdsAhead = 0) diff --git a/BossMod/Autorotation/xan/Casters/RDM.cs b/BossMod/Autorotation/xan/Casters/RDM.cs index de9dbddd7b..6ae9ec890e 100644 --- a/BossMod/Autorotation/xan/Casters/RDM.cs +++ b/BossMod/Autorotation/xan/Casters/RDM.cs @@ -1,5 +1,6 @@ using BossMod.RDM; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -57,9 +58,9 @@ public static RotationModuleDefinition Definition() public int NumConeTargets; public int NumLineTargets; - private Actor? BestAOETarget; - private Actor? BestConeTarget; - private Actor? BestLineTarget; + private Enemy? BestAOETarget; + private Enemy? BestConeTarget; + private Enemy? BestLineTarget; private bool InCombo => ComboLastMove == AID.Riposte && Unlocked(AID.Zwerchhau) @@ -88,7 +89,7 @@ protected override float GetCastTime(AID aid) return base.GetCastTime(aid); } - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 25); @@ -122,8 +123,8 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) : Unlocked(AID.Zwerchhau) ? 35 : 20; - if (primaryTarget is Actor tar && (Swordplay > 0 || LowestMana >= comboMana || InCombo)) - Hints.GoalZones.Add(Hints.GoalSingleTarget(tar, 3)); + if (primaryTarget is { } tar && (Swordplay > 0 || LowestMana >= comboMana || InCombo)) + Hints.GoalZones.Add(Hints.GoalSingleTarget(tar.Actor, 3)); OGCD(strategy, primaryTarget); @@ -201,7 +202,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.Jolt, primaryTarget); } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (!Player.InCombat || primaryTarget == null) return; @@ -239,12 +240,12 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) PushOGCD(AID.LucidDreaming, Player); } - private bool DashOk(StrategyValues strategy, Actor? primaryTarget) => strategy.Option(Track.Dash).As() switch + private bool DashOk(StrategyValues strategy, Enemy? primaryTarget) => strategy.Option(Track.Dash).As() switch { DashStrategy.Any => true, - DashStrategy.Move => ForceMovementIn > 30, + DashStrategy.Move => MaxCastTime > 30, DashStrategy.Close => Player.DistanceToHitbox(primaryTarget) < 3, - DashStrategy.CloseMove => Player.DistanceToHitbox(primaryTarget) < 3 && ForceMovementIn > 30, + DashStrategy.CloseMove => Player.DistanceToHitbox(primaryTarget) < 3 && MaxCastTime > 30, _ => false }; } diff --git a/BossMod/Autorotation/xan/Casters/SMN.cs b/BossMod/Autorotation/xan/Casters/SMN.cs index 86d0345fe6..c5c81f1fd9 100644 --- a/BossMod/Autorotation/xan/Casters/SMN.cs +++ b/BossMod/Autorotation/xan/Casters/SMN.cs @@ -1,5 +1,6 @@ using BossMod.SMN; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -77,6 +78,7 @@ public static RotationModuleDefinition Definition() public float SearingLightLeft; public float SearingFlash; public float RefulgentLux; + public bool CrimsonStrikeReady; public int Aetherflow => TranceFlags.HasFlag(SmnFlags.Aetherflow2) ? 2 : TranceFlags.HasFlag(SmnFlags.Aetherflow) ? 1 : 0; @@ -84,8 +86,8 @@ public static RotationModuleDefinition Definition() public int NumMeleeTargets; private Actor? Carbuncle; - private Actor? BestAOETarget; - private Actor? BestMeleeTarget; + private Enemy? BestAOETarget; + private Enemy? BestMeleeTarget; public Trance Trance { @@ -208,7 +210,7 @@ public AID BestAethercharge } } - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 25); @@ -218,6 +220,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) AttunementType = (AttunementType)(gauge.Attunement & 3); Attunement = gauge.Attunement >> 2; + // intentionally not using activepet as it is cleared when current summon's duration expires, even though the actor still exists, causing autorot to constantly do redundant summons Carbuncle = World.Actors.FirstOrDefault(x => x.Type == ActorType.Pet && x.OwnerID == Player.InstanceID); var favor = Player.Statuses.FirstOrDefault(x => (SID)x.ID is SID.GarudasFavor or SID.IfritsFavor or SID.TitansFavor); @@ -233,6 +236,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) SearingFlash = StatusLeft(SID.RubysGlimmer); SearingLightLeft = Player.FindStatus(SID.SearingLight) is ActorStatus s ? StatusDuration(s.ExpireAt) : 0; RefulgentLux = StatusLeft(SID.RefulgentLux); + CrimsonStrikeReady = Player.FindStatus(SID.CrimsonStrikeReady) != null; (BestAOETarget, NumAOETargets) = SelectTargetByHP(strategy, primaryTarget, 25, IsSplashTarget); (BestMeleeTarget, NumMeleeTargets) = SelectTarget(strategy, primaryTarget, 3, IsSplashTarget); @@ -251,11 +255,13 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) return; } + GoalZoneSingle(25); + OGCDs(strategy, primaryTarget); - if (ComboLastMove == AID.CrimsonCyclone) + if (CrimsonStrikeReady) { - Hints.GoalZones.Add(Hints.GoalSingleTarget(primaryTarget, 3)); + Hints.GoalZones.Add(Hints.GoalSingleTarget(primaryTarget.Actor, 3)); PushGCD(AID.CrimsonStrike, BestMeleeTarget); } @@ -280,13 +286,13 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) case CycloneUse.Delay: // do nothing, pause rotation return; case CycloneUse.DelayMove: - if (ForceMovementIn == 0) + if (MaxCastTime == 0) return; else PushGCD(AID.CrimsonCyclone, BestAOETarget); break; case CycloneUse.SkipMove: - if (ForceMovementIn > 0) + if (MaxCastTime > 0) PushGCD(AID.CrimsonCyclone, BestAOETarget); break; case CycloneUse.Skip: @@ -300,18 +306,19 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) // balance says to default to summons if you don't know whether you will lose a usage or not if (ReadyIn(AID.Aethercharge) <= GCD && Player.InCombat) { - // scarlet flame and wyrmwave are both single target, this is ok - PushGCD(BestAethercharge, primaryTarget); + if (!Unlocked(AID.DreadwyrmTrance) || DowntimeIn > GCD + 15) + // scarlet flame and wyrmwave are both single target, this is ok + PushGCD(BestAethercharge, primaryTarget); } if (TranceFlags.HasFlag(SmnFlags.Topaz)) - PushGCD(AID.SummonTopaz, primaryTarget); + PushGCD(AID.SummonTopaz, Unlocked(TraitID.TopazSummoningMastery) ? BestAOETarget : primaryTarget); if (TranceFlags.HasFlag(SmnFlags.Emerald)) - PushGCD(AID.SummonEmerald, primaryTarget); + PushGCD(AID.SummonEmerald, Unlocked(TraitID.EmeraldSummoningMastery) ? BestAOETarget : primaryTarget); if (TranceFlags.HasFlag(SmnFlags.Ruby)) - PushGCD(AID.SummonRuby, primaryTarget); + PushGCD(AID.SummonRuby, Unlocked(TraitID.RubySummoningMastery) ? BestAOETarget : primaryTarget); } if (FurtherRuin > GCD && SummonLeft == 0) @@ -324,9 +331,9 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) } - private void OGCDs(StrategyValues strategy, Actor? primaryTarget) + private void OGCDs(StrategyValues strategy, Enemy? primaryTarget) { - if (!Player.InCombat) + if (!Player.InCombat || primaryTarget == null) return; if (Favor == Favor.Titan) diff --git a/BossMod/Autorotation/xan/Healers/AST.cs b/BossMod/Autorotation/xan/Healers/AST.cs index 83755266ba..3b60ca8b6c 100644 --- a/BossMod/Autorotation/xan/Healers/AST.cs +++ b/BossMod/Autorotation/xan/Healers/AST.cs @@ -1,5 +1,6 @@ using BossMod.AST; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; public sealed class AST(RotationModuleManager manager, Actor player) : Castxan(manager, player) { @@ -24,8 +25,8 @@ public static RotationModuleDefinition Definition() public int NumCrownTargets; public int NumAOETargets; - private Actor? BestAOETarget; - private Actor? BestDotTarget; + private Enemy? BestAOETarget; + private Enemy? BestDotTarget; protected override float GetCastTime(AID aid) { @@ -37,7 +38,7 @@ protected override float GetCastTime(AID aid) return b; } - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 25); @@ -50,7 +51,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) DivinationLeft = StatusDetails(Player, SID.Divination, Player.InstanceID, 20).Left; Divining = StatusLeft(SID.Divining); - (BestAOETarget, NumAOETargets) = SelectTarget(strategy, primaryTarget, 25, IsSplashTarget); + (BestAOETarget, NumAOETargets) = SelectTarget(strategy, primaryTarget, 25, (primary, other) => Hints.TargetInAOECircle(other, primary.Position, 8)); NumCrownTargets = NumNearbyTargets(strategy, 20); (BestDotTarget, TargetDotLeft) = SelectDotTarget(strategy, primaryTarget, CombustLeft, 2); @@ -73,7 +74,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.Malefic, primaryTarget); } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (!Player.InCombat || primaryTarget == null) return; @@ -90,7 +91,10 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) if (UseCards) { if (HaveBuffCard) - PushOGCD(AID.PlayI, FindBestCardTarget(strategy, isRanged: Cards[0] == AstrologianCard.Spear)); + { + var isRanged = Cards[0] == AstrologianCard.Spear; + PushOGCD(isRanged ? AID.TheSpear : AID.TheBalance, FindBestCardTarget(strategy, isRanged: isRanged)); + } if (HaveLord && NumCrownTargets > 0) PushOGCD(AID.LordOfCrowns, Player); @@ -163,6 +167,6 @@ int Prio(Actor actor) return def; } - return World.Party.WithoutSlot().Where(actor => Player.DistanceToHitbox(actor) <= 30 && !HasCard(actor)).MaxBy(Prio) ?? Player; + return World.Party.WithoutSlot(excludeAlliance: true, excludeNPCs: true).Where(actor => Player.DistanceToHitbox(actor) <= 30 && !HasCard(actor)).MaxBy(Prio) ?? Player; } } diff --git a/BossMod/Autorotation/xan/Healers/SCH.cs b/BossMod/Autorotation/xan/Healers/SCH.cs index f0d8bdac0c..8e3780fe6b 100644 --- a/BossMod/Autorotation/xan/Healers/SCH.cs +++ b/BossMod/Autorotation/xan/Healers/SCH.cs @@ -1,5 +1,6 @@ using BossMod.SCH; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; public sealed class SCH(RotationModuleManager manager, Actor player) : Castxan(manager, player) @@ -46,12 +47,12 @@ public enum PetOrder public PetOrder FairyOrder; private Actor? Eos; - private Actor? BestDotTarget; - private Actor? BestRangedAOETarget; + private Enemy? BestDotTarget; + private Enemy? BestRangedAOETarget; private DateTime _summonWait; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 25); @@ -106,7 +107,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) var needAOETargets = Unlocked(AID.Broil1) ? 2 : 1; - GoalZoneCombined(25, Hints.GoalAOECircle(5), needAOETargets); + GoalZoneCombined(strategy, 25, Hints.GoalAOECircle(5), AID.ArtOfWar1, needAOETargets); if (NumAOETargets >= needAOETargets) PushGCD(AID.ArtOfWar1, Player); @@ -117,7 +118,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.Ruin2, primaryTarget); } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (primaryTarget == null || !Player.InCombat) return; diff --git a/BossMod/Autorotation/xan/Healers/SGE.cs b/BossMod/Autorotation/xan/Healers/SGE.cs index 60831c16ca..45d9d01738 100644 --- a/BossMod/Autorotation/xan/Healers/SGE.cs +++ b/BossMod/Autorotation/xan/Healers/SGE.cs @@ -1,5 +1,6 @@ using BossMod.SGE; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -38,15 +39,15 @@ public static RotationModuleDefinition Definition() public float TargetDotLeft; - private Actor? BestPhlegmaTarget; // 6y/5y - private Actor? BestRangedAOETarget; // 25y/5y toxikon, psyche - private Actor? BestPneumaTarget; // 25y/4y rect + private Enemy? BestPhlegmaTarget; // 6y/5y + private Enemy? BestRangedAOETarget; // 25y/5y toxikon, psyche + private Enemy? BestPneumaTarget; // 25y/4y rect - private Actor? BestDotTarget; + private Enemy? BestDotTarget; protected override float GetCastTime(AID aid) => Eukrasia ? 0 : base.GetCastTime(aid); - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, range: 25); @@ -69,7 +70,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) DoOGCD(strategy, primaryTarget); } - private void DoGCD(StrategyValues strategy, Actor? primaryTarget) + private void DoGCD(StrategyValues strategy, Enemy? primaryTarget) { if (strategy.Option(Track.Kardia).As() == KardiaStrategy.Auto && Unlocked(AID.Kardia) @@ -78,6 +79,16 @@ private void DoGCD(StrategyValues strategy, Actor? primaryTarget) && !World.Party.Members[World.Party.FindSlot(kardiaTarget.InstanceID)].InCutscene) PushGCD(AID.Kardia, kardiaTarget); + if (CountdownRemaining > 0) + { + if (CountdownRemaining < GetCastTime(AID.Dosis)) + PushGCD(AID.Dosis, primaryTarget); + + return; + } + + GoalZoneCombined(strategy, 25, Hints.GoalAOECircle(5), AID.Dyskrasia, 2); + if (!Player.InCombat && Unlocked(AID.Eukrasia) && !Eukrasia && Player.MountId == 0) PushGCD(AID.Eukrasia, Player); @@ -94,8 +105,8 @@ private void DoGCD(StrategyValues strategy, Actor? primaryTarget) if (ShouldPhlegma(strategy)) { - if (ReadyIn(AID.Phlegma) <= GCD && primaryTarget is Actor t) - Hints.GoalZones.Add(Hints.GoalSingleTarget(t, 6)); + if (ReadyIn(AID.Phlegma) <= GCD && primaryTarget is { } t) + Hints.GoalZones.Add(Hints.GoalSingleTarget(t.Actor, 6)); PushGCD(AID.Phlegma, BestPhlegmaTarget); } @@ -125,7 +136,7 @@ private bool ShouldPhlegma(StrategyValues strategy) return NumPhlegmaTargets > 2 || RaidBuffsLeft > GCD || RaidBuffsIn > 9000; } - private void DoOGCD(StrategyValues strategy, Actor? primaryTarget) + private void DoOGCD(StrategyValues strategy, Enemy? primaryTarget) { if (!Player.InCombat) return; diff --git a/BossMod/Autorotation/xan/Healers/WHM.cs b/BossMod/Autorotation/xan/Healers/WHM.cs index 07c0e70926..bb6f7c8c62 100644 --- a/BossMod/Autorotation/xan/Healers/WHM.cs +++ b/BossMod/Autorotation/xan/Healers/WHM.cs @@ -1,5 +1,6 @@ using BossMod.WHM; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -38,10 +39,10 @@ public static RotationModuleDefinition Definition() public int NumMiseryTargets; public int NumSolaceTargets; - private Actor? BestDotTarget; - private Actor? BestMiseryTarget; + private Enemy? BestDotTarget; + private Enemy? BestMiseryTarget; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 25); @@ -62,12 +63,14 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (CountdownRemaining > 0) { - if (CountdownRemaining < 1.7) + if (CountdownRemaining < GetCastTime(AID.Stone1)) PushGCD(AID.Stone1, primaryTarget); return; } + GoalZoneCombined(strategy, 25, Hints.GoalAOECircle(8), AID.Holy1, 3); + if (!CanFitGCD(TargetDotLeft, 1)) PushGCD(AID.Aero1, BestDotTarget); @@ -90,12 +93,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) // TODO make a track for this if (Lily == 3 || !CanFitGCD(NextLily, 2) && Lily == 2) - { - if (World.Party.WithoutSlot(excludeAlliance: true).Average(PredictedHPRatio) < 0.8 && NumSolaceTargets == World.Party.WithoutSlot(excludeAlliance: true).Count()) - PushGCD(AID.AfflatusRapture, Player, 1); - - PushGCD(AID.AfflatusSolace, World.Party.WithoutSlot(excludeAlliance: true).MinBy(PredictedHPRatio), 1); - } + PushGCD(AID.AfflatusSolace, World.Party.WithoutSlot(excludeAlliance: true).Where(m => Player.DistanceToHitbox(m) <= 30).MinBy(PredictedHPRatio)); if (SacredSight > 0) PushGCD(AID.GlareIV, primaryTarget); diff --git a/BossMod/Autorotation/xan/Melee/DRG.cs b/BossMod/Autorotation/xan/Melee/DRG.cs index 1bde962c0a..7afa7f8bfb 100644 --- a/BossMod/Autorotation/xan/Melee/DRG.cs +++ b/BossMod/Autorotation/xan/Melee/DRG.cs @@ -1,5 +1,6 @@ using BossMod.DRG; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -39,6 +40,7 @@ public static RotationModuleDefinition Definition() public float DraconianFire; public float DragonsFlight; public float StarcrossReady; + public float EnhancedTalon; public float TargetDotLeft; @@ -46,11 +48,11 @@ public static RotationModuleDefinition Definition() public int NumLongAOETargets; // GSK, nastrond (15x4 rect) public int NumDiveTargets; // dragonfire, stardiver, etc - private Actor? BestAOETarget; - private Actor? BestLongAOETarget; - private Actor? BestDiveTarget; + private Enemy? BestAOETarget; + private Enemy? BestLongAOETarget; + private Enemy? BestDiveTarget; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 3); @@ -68,6 +70,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) DraconianFire = StatusLeft(SID.DraconianFire); DragonsFlight = StatusLeft(SID.DragonsFlight); StarcrossReady = StatusLeft(SID.StarcrossReady); + EnhancedTalon = StatusLeft(SID.EnhancedPiercingTalon); TargetDotLeft = MathF.Max( StatusDetails(primaryTarget, SID.ChaosThrust, Player.InstanceID).Left, StatusDetails(primaryTarget, SID.ChaoticSpring, Player.InstanceID).Left @@ -80,12 +83,18 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) var pos = GetPositional(strategy, primaryTarget); UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); - OGCD(strategy, primaryTarget); - if (primaryTarget == null) return; - GoalZoneCombined(3, Hints.GoalAOERect(primaryTarget, 10, 2), 3, pos.Item1); + if (CountdownRemaining > 0) + { + if (CountdownRemaining < 0.7f) + PushGCD(AID.WingedGlide, primaryTarget); + + return; + } + + GoalZoneCombined(strategy, 3, Hints.GoalAOERect(primaryTarget.Actor, 10, 2), AID.DoomSpike, minAoe: 3, positional: pos.Item1, maximumActionRange: 20); if (NumAOETargets > 2) { @@ -145,9 +154,13 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) } PushGCD(DraconianFire > GCD ? AID.RaidenThrust : AID.TrueThrust, primaryTarget); + if (EnhancedTalon > GCD) + PushGCD(AID.PiercingTalon, primaryTarget); + + OGCD(strategy, primaryTarget); } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (primaryTarget == null || !Player.InCombat || PowerSurge == 0) return; @@ -174,11 +187,9 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) PushOGCD(AID.LifeSurge, Player); if (StarcrossReady > 0) - // TODO we *technically* should select a specific target for starcross because it's a 3y range 5y radius circle... - // but it's always gonna get used immediately after stardiver and we'll be melee range...so fuck it PushOGCD(AID.Starcross, primaryTarget); - if (LotD > 0 && moveOk) + if (LotD > AnimLock && moveOk) PushOGCD(AID.Stardiver, BestDiveTarget); if (NastrondReady == 0) @@ -187,7 +198,7 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) if (DiveReady == 0 && posOk) PushOGCD(AID.Jump, primaryTarget); - if (moveOk) + if (moveOk && strategy.BuffsOk()) PushOGCD(AID.DragonfireDive, BestDiveTarget); if (NastrondReady > 0) @@ -246,7 +257,7 @@ private bool ShouldLifeSurge() private bool MoveOk(StrategyValues strategy) => strategy.Option(Track.Dive).As() == DiveStrategy.Allow; private bool PosLockOk(StrategyValues strategy) => strategy.Option(Track.Dive).As() != DiveStrategy.NoLock; - private (Positional, bool) GetPositional(StrategyValues strategy, Actor? primaryTarget) + private (Positional, bool) GetPositional(StrategyValues strategy, Enemy? primaryTarget) { // no positional if (NumAOETargets > 2 && Unlocked(AID.DoomSpike) || !Unlocked(AID.ChaosThrust) || primaryTarget == null) diff --git a/BossMod/Autorotation/xan/Melee/MNK.cs b/BossMod/Autorotation/xan/Melee/MNK.cs index 32dd9516f6..99e33e370c 100644 --- a/BossMod/Autorotation/xan/Melee/MNK.cs +++ b/BossMod/Autorotation/xan/Melee/MNK.cs @@ -1,11 +1,12 @@ using BossMod.MNK; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; public sealed class MNK(RotationModuleManager manager, Actor player) : Attackxan(manager, player) { - public enum Track { Potion = SharedTrack.Buffs, SSS, Meditation, FormShift, FiresReply, Nadi, RoF, RoW, PB, BH, TC, Blitz, Engage } + public enum Track { Potion = SharedTrack.Buffs, SSS, Meditation, FormShift, FiresReply, Nadi, RoF, RoW, PB, BH, TC, Blitz, Engage, TN } public enum PotionStrategy { Manual, @@ -34,6 +35,13 @@ public enum NadiStrategy [PropertyDisplay("Solar", 0xFF8EE6FA)] Solar } + public enum RoFStrategy + { + Automatic, + Force, + ForceMidWeave, + Delay, + } public enum RoWStrategy { Automatic, @@ -103,7 +111,13 @@ public static RotationModuleDefinition Definition() .AddOption(NadiStrategy.Lunar, "Lunar", minLevel: 60) .AddOption(NadiStrategy.Solar, "Solar", minLevel: 60); - def.DefineSimple(Track.RoF, "RoF", minLevel: 68).AddAssociatedActions(AID.RiddleOfFire); + def.Define(Track.RoF).As("RoF") + .AddOption(RoFStrategy.Automatic, "Auto", "Automatically use RoF during burst window", minLevel: 68) + .AddOption(RoFStrategy.Force, "Force", "Use ASAP", minLevel: 68) + .AddOption(RoFStrategy.ForceMidWeave, "ForceMid", "Use ASAP, but retain late-weave to ensure maximum GCDs covered", minLevel: 68) + .AddOption(RoFStrategy.Delay, "Delay", "Do not use", minLevel: 68) + .AddAssociatedActions(AID.RiddleOfFire); + def.DefineSimple(Track.RoW, "RoW", minLevel: 72).AddAssociatedActions(AID.RiddleOfWind); def.Define(Track.PB).As("PB") @@ -135,6 +149,8 @@ public static RotationModuleDefinition Definition() .AddOption(EngageStrategy.FacepullDK, "Precast Dragon Kick from melee range") .AddOption(EngageStrategy.FacepullDemo, "Precast Demolish from melee range"); + def.DefineSimple(Track.TN, "TrueNorth", minLevel: 50).AddAssociatedActions(AID.TrueNorth); + return def; } @@ -165,9 +181,9 @@ public enum Form { None, OpoOpo, Raptor, Coeurl } public int NumAOETargets; public int NumLineTargets; - private Actor? BestBlitzTarget; - private Actor? BestRangedTarget; // fire's reply - private Actor? BestLineTarget; // enlightenment, wind's reply + private Enemy? BestBlitzTarget; + private Enemy? BestRangedTarget; // fire's reply + private Enemy? BestLineTarget; // enlightenment, wind's reply public bool HaveLunar => Nadi.HasFlag(NadiFlags.Lunar); public bool HaveSolar => Nadi.HasFlag(NadiFlags.Solar); @@ -198,7 +214,7 @@ public enum Form { None, OpoOpo, Raptor, Coeurl } public bool CanFormShift => Unlocked(AID.FormShift) && PerfectBalanceLeft == 0; // TODO incorporate crit calculation - rockbreaker is a gain on 3 at 22.1% crit - public int AOEBreakpoint => EffectiveForm == Form.OpoOpo ? 3 : 4; + public int AOEBreakpoint => Unlocked(AID.ShadowOfTheDestroyer) && EffectiveForm == Form.OpoOpo ? 3 : 4; public bool UseAOE => NumAOETargets >= AOEBreakpoint; public int BuffedGCDsLeft => FireLeft > GCD ? (int)MathF.Floor((FireLeft - GCD) / AttackGCDLength) + 1 : 0; @@ -262,7 +278,7 @@ public enum OGCDPriority public override string DescribeState() => $"F={BuffedGCDsLeft}, PB={PBGCDsLeft}"; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, range: 3); HaveTarget = primaryTarget != null && Player.InCombat; @@ -295,13 +311,13 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) (BestBlitzTarget, NumBlitzTargets) = SelectTarget(strategy, primaryTarget, 3, IsSplashTarget); else { - BestBlitzTarget = Player; + BestBlitzTarget = null; NumBlitzTargets = NumAOETargets; } } else { - BestBlitzTarget = Player; + BestBlitzTarget = null; NumBlitzTargets = 0; } @@ -329,9 +345,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) return; } - GoalZoneCombined(3, Hints.GoalAOECircle(5), AOEBreakpoint, pos.Item1); - - OGCD(strategy, primaryTarget); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.ArmOfTheDestroyer, AOEBreakpoint, positional: pos.Item1, maximumActionRange: 20); UseBlitz(strategy, currentBlitz); FiresReply(strategy); @@ -376,6 +390,9 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) } Prep(strategy); + + if (Player.InCombat) + OGCD(strategy, primaryTarget); } private void Prep(StrategyValues strategy) @@ -413,10 +430,21 @@ private void Prep(StrategyValues strategy) private Form GetEffectiveForm(StrategyValues strategy) { if (PerfectBalanceLeft == 0) - return CurrentForm; + { + if (Unlocked(AID.SnapPunch)) + return CurrentForm; + + if (Unlocked(AID.TrueStrike)) + return CurrentForm == Form.Raptor ? Form.Raptor : Form.OpoOpo; + + return Form.OpoOpo; + } var nadi = strategy.Option(Track.Nadi).As(); + if (ForcedLunar || nadi == NadiStrategy.Lunar) + return Form.OpoOpo; + // TODO throw away all this crap and fix odd lunar PB (it should not be used before rof) // force lunar PB iff we are in opener, have lunar nadi already, and this is our last PB charge, aka double lunar opener @@ -441,10 +469,19 @@ private Form GetEffectiveForm(StrategyValues strategy) canOpo &= chak != BeastChakraType.OpoOpo; } - return canRaptor ? Form.Raptor : canCoeurl ? Form.Coeurl : Form.OpoOpo; + // nice conditional + return canOpo && OpoStacks == 0 + ? Form.OpoOpo + : canRaptor && RaptorStacks == 0 + ? Form.Raptor + : canCoeurl + ? Form.Coeurl + : canRaptor + ? Form.Raptor + : Form.OpoOpo; } - private void QueuePB(StrategyValues strategy, Actor? primaryTarget) + private void QueuePB(StrategyValues strategy, Enemy? primaryTarget) { var pbstrat = strategy.Option(Track.PB).As(); @@ -471,7 +508,7 @@ private void QueuePB(StrategyValues strategy, Actor? primaryTarget) if (BrotherhoodLeft == 0 && MaxChargesIn(AID.PerfectBalance) > 30) return; - if (ShouldRoF(strategy, 3) || CanFitGCD(FireLeft, 3)) + if (ShouldRoF(strategy, 3).Use || CanFitGCD(FireLeft, 3)) { // in case of drift or whatever, if we end up wanting to triple weave after opo, delay PB in favor of using FR to get formless // check if BH cooldown is >118s. if we only checked CanWeave for both then autorotation would do BH -> PB because RoF is slightly delayed to get the optimal late weave @@ -484,7 +521,7 @@ private void QueuePB(StrategyValues strategy, Actor? primaryTarget) } } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { switch (strategy.Option(Track.Potion).As()) { @@ -500,35 +537,38 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) Brotherhood(strategy, primaryTarget); QueuePB(strategy, primaryTarget); - var useRof = ShouldRoF(strategy); + var (useRof, rofLate) = ShouldRoF(strategy); if (useRof) - PushOGCD(AID.RiddleOfFire, Player, OGCDPriority.RiddleOfFire, GCD - EarliestRoF(AnimationLockDelay)); + PushOGCD(AID.RiddleOfFire, Player, OGCDPriority.RiddleOfFire, rofLate ? GCD - EarliestRoF(AnimationLockDelay) : 0); + + if (strategy.Option(Track.RoF).As() == RoFStrategy.Force && !HaveTarget) + PushOGCD(AID.RiddleOfFire, Player, OGCDPriority.RiddleOfFire); if (ShouldRoW(strategy)) PushOGCD(AID.RiddleOfWind, Player, OGCDPriority.RiddleOfWind); - if (NextPositionalImminent && !NextPositionalCorrect) - PushOGCD(AID.TrueNorth, Player, OGCDPriority.TrueNorth, useRof ? 0 : GCD - 0.8f); + UseTN(strategy, primaryTarget, useRof); - if (HaveTarget && Chakra >= 5 && !CanWeave(AID.RiddleOfFire)) + if (HaveTarget && Chakra >= 5 && !useRof) { if (NumLineTargets >= 3) PushOGCD(AID.HowlingFist, BestLineTarget, OGCDPriority.TFC); - PushOGCD(AID.SteelPeak, primaryTarget, OGCDPriority.TFC); + if (primaryTarget?.Priority >= 0) + PushOGCD(AID.SteelPeak, primaryTarget, OGCDPriority.TFC); } if (strategy.Option(Track.TC).As() == TCStrategy.GapClose && Player.DistanceToHitbox(primaryTarget) is > 3 and < 25) PushOGCD(AID.Thunderclap, primaryTarget, OGCDPriority.TrueNorth); } - private void Brotherhood(StrategyValues strategy, Actor? primaryTarget) + private void Brotherhood(StrategyValues strategy, Enemy? primaryTarget) { switch (strategy.Simple(Track.BH)) { case OffensiveStrategy.Automatic: - if (HaveTarget && (CombatTimer > 10 || BeastCount == 2) && DowntimeIn > World.Client.AnimationLock + 20 && GCD > 0) + if (HaveTarget && (CombatTimer > 10 || BeastCount == 2) && DowntimeIn > AnimLock + 20 && GCD > 0) PushOGCD(AID.Brotherhood, Player, OGCDPriority.Brotherhood); break; case OffensiveStrategy.Force: @@ -539,7 +579,7 @@ private void Brotherhood(StrategyValues strategy, Actor? primaryTarget) } } - private void Meditate(StrategyValues strategy, Actor? primaryTarget) + private void Meditate(StrategyValues strategy, Enemy? primaryTarget) { if (Chakra >= 5 || !Unlocked(AID.SteeledMeditation) || Player.MountId > 0) return; @@ -568,7 +608,7 @@ private void Meditate(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.SteeledMeditation, Player, prio); } - private void FormShift(StrategyValues strategy, Actor? primaryTarget) + private void FormShift(StrategyValues strategy, Enemy? primaryTarget) { if (!Unlocked(AID.FormShift) || PerfectBalanceLeft > 0) return; @@ -618,6 +658,9 @@ private void FiresReply(StrategyValues strategy) _ => GCDPriority.None }; + if (!CanFitGCD(FiresReplyLeft, 1)) + prio = GCDPriority.FiresReply; + PushGCD(AID.FiresReply, BestRangedTarget, prio); } @@ -641,26 +684,42 @@ private void WindsReply() private void Potion() => Hints.ActionsToExecute.Push(ActionDefinitions.IDPotionStr, Player, ActionQueue.Priority.Low + 100 + (float)OGCDPriority.Potion); - private bool ShouldRoF(StrategyValues strategy, int extraGCDs = 0) + private (bool Use, bool LateWeave) ShouldRoF(StrategyValues strategy, int extraGCDs = 0) { if (!CanWeave(AID.RiddleOfFire, extraGCDs)) - return false; + return (false, false); - return strategy.Simple(Track.RoF) switch + return strategy.Option(Track.RoF).As() switch { - OffensiveStrategy.Automatic => HaveTarget && (extraGCDs > 0 || !CanWeave(AID.Brotherhood)) && DowntimeIn > World.Client.AnimationLock + 20, - OffensiveStrategy.Force => true, - _ => false + RoFStrategy.Automatic => (HaveTarget && (extraGCDs > 0 || !CanWeave(AID.Brotherhood)) && DowntimeIn > AnimLock + 20, true), + RoFStrategy.Force => (true, false), + RoFStrategy.ForceMidWeave => (true, true), + _ => (false, false) }; } private bool ShouldRoW(StrategyValues strategy) => strategy.Simple(Track.RoW) switch { - OffensiveStrategy.Automatic => HaveTarget && !CanWeave(AID.RiddleOfFire) && DowntimeIn > World.Client.AnimationLock + 15, + OffensiveStrategy.Automatic => HaveTarget && !CanWeave(AID.RiddleOfFire) && DowntimeIn > AnimLock + 15, OffensiveStrategy.Force => true, _ => false }; + private void UseTN(StrategyValues strategy, Enemy? primaryTarget, bool rofPlanned) + { + switch (strategy.Simple(Track.TN)) + { + case OffensiveStrategy.Automatic: + if (NextPositionalImminent && !NextPositionalCorrect && Player.DistanceToHitbox(primaryTarget) < 6) + PushOGCD(AID.TrueNorth, Player, OGCDPriority.TrueNorth, rofPlanned ? 0 : GCD - 0.8f); + break; + case OffensiveStrategy.Force: + if (TrueNorthLeft == 0) + PushOGCD(AID.TrueNorth, Player, OGCDPriority.TrueNorth); + break; + } + } + private bool IsEnlightenmentTarget(Actor primary, Actor other) => Hints.TargetInAOERect(other, Player.Position, Player.DirectionTo(primary), 10, 2); private (Form, float) DetermineForm() @@ -679,7 +738,7 @@ private bool ShouldRoF(StrategyValues strategy, int extraGCDs = 0) return s > 0 ? (Form.Coeurl, s) : (Form.None, 0); } - private void SmartEngage(StrategyValues strategy, Actor? primaryTarget) + private void SmartEngage(StrategyValues strategy, Enemy? primaryTarget) { if (primaryTarget == null) return; @@ -705,7 +764,7 @@ private void SmartEngage(StrategyValues strategy, Actor? primaryTarget) // TODO account for acceleration if (CountdownRemaining < secToMelee + 0.5f) { - Hints.ForcedMovement = Player.DirectionTo(primaryTarget).ToVec3(); + Hints.ForcedMovement = Player.DirectionTo(primaryTarget.Actor).ToVec3(); PushGCD(AID.DragonKick, primaryTarget); } @@ -723,7 +782,7 @@ private void SmartEngage(StrategyValues strategy, Actor? primaryTarget) return; if (Player.DistanceToHitbox(primaryTarget) > 3) - Hints.ForcedMovement = Player.DirectionTo(primaryTarget).ToVec3(); + Hints.ForcedMovement = Player.DirectionTo(primaryTarget.Actor).ToVec3(); if (CountdownRemaining < GetApplicationDelay(facepullAction)) PushGCD(facepullAction, primaryTarget); diff --git a/BossMod/Autorotation/xan/Melee/NIN.cs b/BossMod/Autorotation/xan/Melee/NIN.cs index 7df3846102..41f8586d0c 100644 --- a/BossMod/Autorotation/xan/Melee/NIN.cs +++ b/BossMod/Autorotation/xan/Melee/NIN.cs @@ -1,6 +1,7 @@ using BossMod.NIN; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using System.Collections.ObjectModel; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -46,7 +47,7 @@ public static RotationModuleDefinition Definition() public int NumRangedAOETargets; // 25y for hellfrog - ninjutsu have a range of 20y - private Actor? BestRangedAOETarget; + private Enemy? BestRangedAOETarget; // these aren't the same cdgroup :( public float AssassinateCD => ReadyIn(Unlocked(AID.DreamWithinADream) ? AID.DreamWithinADream : AID.Assassinate); @@ -78,7 +79,7 @@ public static RotationModuleDefinition Definition() _ => AID.Ninjutsu }; - private bool Hidden => HiddenStatus || ShadowWalker > World.Client.AnimationLock; + private bool Hidden => HiddenStatus || ShadowWalker > AnimLock; private bool CanTrickInCombat => Unlocked(AID.Suiton); @@ -86,7 +87,7 @@ public static RotationModuleDefinition Definition() 452 ]; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, range: 3); @@ -119,7 +120,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) NumAOETargets = NumMeleeAOETargets(strategy); - var pos = GetNextPositional(primaryTarget); + var pos = GetNextPositional(primaryTarget?.Actor); UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); OGCD(strategy, primaryTarget); @@ -132,7 +133,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) return; } - GoalZoneCombined(3, Hints.GoalAOECircle(5), 3, pos.Item1); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.DeathBlossom, minAoe: 3, positional: pos.Item1, maximumActionRange: 20); if (TenChiJin.Left > GCD) { @@ -217,7 +218,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) else { if (ComboLastMove == AID.GustSlash && primaryTarget != null) - PushGCD(GetComboEnder(primaryTarget), primaryTarget); + PushGCD(GetComboEnder(primaryTarget.Actor), primaryTarget); if (ComboLastMove == AID.SpinningEdge) PushGCD(AID.GustSlash, primaryTarget); @@ -226,7 +227,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) } } - private bool ShouldPK(Actor? primaryTarget) + private bool ShouldPK(Enemy? primaryTarget) { if (RaidBuffsLeft > GCD || TargetTrickLeft > GCD || TargetMugLeft > GCD) return true; @@ -251,14 +252,14 @@ private AID GetComboEnder(Actor primaryTarget) return primaryTarget.Omnidirectional || GetCurrentPositional(primaryTarget) == Positional.Rear ? AID.AeolianEdge : AID.ArmorCrush; } - private void UseMudra(AID mudra, Actor? target, bool startCondition = true, bool endCondition = true) + private void UseMudra(AID mudra, Enemy? target, bool startCondition = true, bool endCondition = true) { (var aid, var tar) = PickMudra(mudra, target, startCondition, endCondition); if (aid != AID.None) PushGCD(aid == AID.Ninjutsu ? CurrentNinjutsu : aid, tar); } - private (AID action, Actor? target) PickMudra(AID mudra, Actor? target, bool startCondition, bool endCondition) + private (AID action, Enemy? target) PickMudra(AID mudra, Enemy? target, bool startCondition, bool endCondition) { if (!Unlocked(mudra) || target == null) return (AID.None, null); @@ -285,7 +286,7 @@ private void UseMudra(AID mudra, Actor? target, bool startCondition = true, bool if (len == 1) { if (Mudras[0] == 0) - return (ten1, Player); + return (ten1, null); else if (endCondition) return (AID.Ninjutsu, target); } @@ -297,10 +298,10 @@ private void UseMudra(AID mudra, Actor? target, bool startCondition = true, bool return (AID.Ninjutsu, target); if (Mudras[0] == 0) - return (last == 1 ? (Unlocked(jin1) ? jin1 : chi1) : ten1, Player); + return (last == 1 ? (Unlocked(jin1) ? jin1 : chi1) : ten1, null); if (Mudras[1] == 0) - return (last == 1 ? AID.Ten2 : last == 2 ? AID.Chi2 : AID.Jin2, Player); + return (last == 1 ? AID.Ten2 : last == 2 ? AID.Chi2 : AID.Jin2, null); else if (endCondition) return (AID.Ninjutsu, target); } @@ -312,7 +313,7 @@ private void UseMudra(AID mudra, Actor? target, bool startCondition = true, bool return (AID.Ninjutsu, target); if (Mudras[0] == 0) - return (last == 1 ? jin1 : ten1, Player); + return (last == 1 ? jin1 : ten1, null); if (Mudras[1] == 0) return (Mudras[0] switch @@ -321,10 +322,10 @@ private void UseMudra(AID mudra, Actor? target, bool startCondition = true, bool 2 => last == 3 ? AID.Ten2 : AID.Jin2, 3 => last == 1 ? AID.Chi2 : AID.Ten2, _ => AID.None - }, Player); + }, null); if (Mudras[2] == 0) - return (last == 1 ? AID.Ten2 : last == 2 ? AID.Chi2 : AID.Jin2, Player); + return (last == 1 ? AID.Ten2 : last == 2 ? AID.Chi2 : AID.Jin2, null); else if (endCondition) return (AID.Ninjutsu, target); } @@ -332,7 +333,7 @@ private void UseMudra(AID mudra, Actor? target, bool startCondition = true, bool return (AID.None, null); } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (!Player.InCombat) { @@ -365,7 +366,7 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) if (!Unlocked(TraitID.Shukiho) || Ninki >= 10) PushOGCD(AID.Mug, primaryTarget); - if (ReadyIn(AID.Ten1) > GCD && Mudra.Left == 0 && Kassatsu == 0 && ShadowWalker == 0 && ForceMovementIn > GCD + 2) + if (ReadyIn(AID.Ten1) > GCD && Mudra.Left == 0 && Kassatsu == 0 && ShadowWalker == 0) PushOGCD(AID.TenChiJin, Player); if (Ninki >= 50) @@ -389,7 +390,7 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) } private bool ShouldBhava(StrategyValues strategy) - => Ninki >= 50 && (Meisui > 0 || TargetTrickLeft > World.Client.AnimationLock || Ninki > 85); + => Ninki >= 50 && (Meisui > 0 || TargetTrickLeft > AnimLock || Ninki > 85); private (Positional, bool) GetNextPositional(Actor? primaryTarget) { diff --git a/BossMod/Autorotation/xan/Melee/RPR.cs b/BossMod/Autorotation/xan/Melee/RPR.cs index b424314442..c6209c0183 100644 --- a/BossMod/Autorotation/xan/Melee/RPR.cs +++ b/BossMod/Autorotation/xan/Melee/RPR.cs @@ -1,15 +1,29 @@ using BossMod.RPR; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; public sealed class RPR(RotationModuleManager manager, Actor player) : Attackxan(manager, player) { + public enum Track { Harpe = SharedTrack.Count } + + public enum HarpeStrategy + { + Automatic, + Forbid, + Ranged, + } + public static RotationModuleDefinition Definition() { var def = new RotationModuleDefinition("xan RPR", "Reaper", "Standard rotation (xan)|Melee", "xan", RotationModuleQuality.Basic, BitMask.Build(Class.RPR), 100); def.DefineShared().AddAssociatedActions(AID.ArcaneCircle); + def.Define(Track.Harpe).As("Harpe") + .AddOption(HarpeStrategy.Automatic, "Use out of melee range if Enhanced Harpe is active") + .AddOption(HarpeStrategy.Forbid, "Don't use") + .AddOption(HarpeStrategy.Ranged, "Use out of melee range"); return def; } @@ -41,9 +55,9 @@ public static RotationModuleDefinition Definition() public int NumConeTargets; // grim swathe, guillotine public int NumLineTargets; // plentiful harvest - private Actor? BestRangedAOETarget; - private Actor? BestConeTarget; - private Actor? BestLineTarget; + private Enemy? BestRangedAOETarget; + private Enemy? BestConeTarget; + private Enemy? BestLineTarget; public enum GCDPriority { @@ -64,7 +78,7 @@ public enum GCDPriority private bool Enshrouded => BlueSouls > 0; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 3); @@ -90,17 +104,15 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) Executioner = StatusLeft(SID.Executioner); PerfectioParata = StatusLeft(SID.PerfectioParata); - var primaryEnemy = Hints.FindEnemy(primaryTarget); - - TargetDDLeft = DDLeft(primaryEnemy); + TargetDDLeft = DDLeft(primaryTarget); ShortestNearbyDDLeft = float.MaxValue; switch (strategy.AOE()) { case AOEStrategy.AOE: case AOEStrategy.ForceAOE: - var nearbyDD = Hints.PriorityTargets.Where(x => Player.DistanceToHitbox(x.Actor) <= 5).Select(DDLeft); - var minNeeded = strategy.AOE() == AOEStrategy.ForceAOE ? 1 : 2; + var nearbyDD = Hints.PriorityTargets.Where(x => Hints.TargetInAOECircle(x.Actor, Player.Position, 5)).Select(DDLeft); + var minNeeded = strategy.AOE() == AOEStrategy.ForceAOE ? 1 : 3; if (MinIfEnoughElements(nearbyDD.Where(x => x < 30), minNeeded) is float m) ShortestNearbyDDLeft = m; break; @@ -111,30 +123,20 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) (BestConeTarget, NumConeTargets) = SelectTarget(strategy, primaryTarget, 8, (primary, other) => Hints.TargetInAOECone(other, Player.Position, 8, Player.DirectionTo(primary), 90.Degrees())); (BestRangedAOETarget, NumRangedAOETargets) = SelectTarget(strategy, primaryTarget, 25, IsSplashTarget); - var pos = GetNextPositional(primaryTarget); + var pos = GetNextPositional(primaryTarget?.Actor); UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); OGCD(strategy, primaryTarget); - if (Soulsow) - PushGCD(AID.HarvestMoon, BestRangedAOETarget, GCDPriority.HarvestMoon); - else if (!Player.InCombat && Player.MountId == 0) - PushGCD(AID.SoulSow, Player, GCDPriority.Soulsow); - if (CountdownRemaining > 0) { - if (CountdownRemaining < 1.7) + if (CountdownRemaining < GetCastTime(AID.Harpe)) PushGCD(AID.Harpe, primaryTarget); return; } - GoalZoneCombined(3, Hints.GoalAOECircle(5), 3, pos.Item1); - - if (EnhancedHarpe > GCD) - PushGCD(AID.Harpe, primaryTarget, GCDPriority.EnhancedHarpe); - - DDRefresh(primaryTarget); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.SpinningScythe, 3, pos.Item1, maximumActionRange: 25); if (SoulReaver > GCD || Executioner > GCD) { @@ -151,13 +153,34 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushGCD(gal, primaryTarget, GCDPriority.Reaver); else if (EnhancedGibbet > GCD) PushGCD(gib, primaryTarget, GCDPriority.Reaver); - else if (GetCurrentPositional(primaryTarget!) == Positional.Rear) + else if (GetCurrentPositional(primaryTarget.Actor) == Positional.Rear) PushGCD(gal, primaryTarget, GCDPriority.Reaver); else PushGCD(gib, primaryTarget, GCDPriority.Reaver); } + + return; // every other GCD breaks soul reaver } + if (!Player.InCombat && Player.MountId == 0 && !Soulsow) + PushGCD(AID.SoulSow, Player, GCDPriority.Soulsow); + + switch (strategy.Option(Track.Harpe).As()) + { + case HarpeStrategy.Automatic: + if (EnhancedHarpe > GCD) + PushGCD(AID.Harpe, primaryTarget, GCDPriority.EnhancedHarpe); + break; + case HarpeStrategy.Ranged: + PushOGCD(AID.Harpe, primaryTarget, 50); + break; + } + + if (Soulsow) + PushGCD(AID.HarvestMoon, BestRangedAOETarget, GCDPriority.HarvestMoon); + + DDRefresh(primaryTarget); + if (PerfectioParata > GCD) PushGCD(AID.Perfectio, BestRangedAOETarget, GCDPriority.Communio); @@ -196,7 +219,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.Slice, primaryTarget, GCDPriority.Filler); } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (primaryTarget == null || !Player.InCombat) return; @@ -231,9 +254,9 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) UseSoul(strategy, primaryTarget); } - private void DDRefresh(Actor? primaryTarget) + private void DDRefresh(Enemy? primaryTarget) { - void Extend(float timer, AID action, Actor? target) + void Extend(float timer, AID action, Enemy? target) { if (!CanFitGCD(timer, CanWeave(AID.Gluttony) ? 2 : 1)) PushGCD(action, target, GCDPriority.DDExpiring); @@ -242,7 +265,7 @@ void Extend(float timer, AID action, Actor? target) PushGCD(action, target, GCDPriority.DDExtend); } - Extend(ShortestNearbyDDLeft, AID.WhorlofDeath, Player); + Extend(ShortestNearbyDDLeft, AID.WhorlofDeath, null); Extend(TargetDDLeft, AID.ShadowofDeath, primaryTarget); } @@ -267,7 +290,7 @@ private bool ShouldEnshroud(StrategyValues strategy) return ReadyIn(AID.ArcaneCircle) > 65; } - private void UseSoul(StrategyValues strategy, Actor? primaryTarget) + private void UseSoul(StrategyValues strategy, Enemy? primaryTarget) { // can't if (RedGauge < 50 || Enshrouded) @@ -300,11 +323,12 @@ private void UseSoul(StrategyValues strategy, Actor? primaryTarget) if (NumConeTargets > 2) PushOGCD(AID.GrimSwathe, BestConeTarget); - PushOGCD(AID.BloodStalk, primaryTarget); + if (primaryTarget?.Priority >= 0) + PushOGCD(AID.BloodStalk, primaryTarget); } } - private void EnshroudGCDs(StrategyValues strategy, Actor? primaryTarget) + private void EnshroudGCDs(StrategyValues strategy, Enemy? primaryTarget) { if (BlueSouls == 0) return; @@ -357,7 +381,7 @@ protected override float GetCastTime(AID aid) return (nextPos, SoulReaver > GCD || Executioner > GCD); } - private float DDLeft(AIHints.Enemy? target) + private float DDLeft(Enemy? target) => (target?.ForbidDOTs ?? false) ? float.MaxValue : StatusDetails(target?.Actor, SID.DeathsDesign, Player.InstanceID, 30).Left; diff --git a/BossMod/Autorotation/xan/Melee/SAM.cs b/BossMod/Autorotation/xan/Melee/SAM.cs index b263969df8..d23d860b46 100644 --- a/BossMod/Autorotation/xan/Melee/SAM.cs +++ b/BossMod/Autorotation/xan/Melee/SAM.cs @@ -1,5 +1,6 @@ using BossMod.SAM; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -67,10 +68,10 @@ public enum Kaeshi public AID AOEStarter => Unlocked(AID.Fuko) ? AID.Fuko : AID.Fuga; public AID STStarter => Unlocked(AID.Gyofu) ? AID.Gyofu : AID.Hakaze; - private Actor? BestAOETarget; // null if fuko is unlocked since it's self-targeted - private Actor? BestLineTarget; - private Actor? BestOgiTarget; - private Actor? BestDotTarget; + private Enemy? BestAOETarget; // null if fuko is unlocked since it's self-targeted + private Enemy? BestLineTarget; + private Enemy? BestOgiTarget; + private Enemy? BestDotTarget; private float TargetDotLeft; @@ -108,7 +109,7 @@ protected override float GetCastTime(AID aid) // TODO: fix GCD priorities - use kaeshi as fallback action (during forced movement, etc) // use kaeshi goken asap in aoe? we usually arent holding for buffs with 3 targets - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, range: 3); @@ -164,20 +165,23 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (TrueNorthLeft == 0 && Hints.PotentialTargets.Any(x => !x.Actor.Omnidirectional) && CountdownRemaining < 5) PushGCD(AID.TrueNorth, Player); + if (MeikyoLeft > CountdownRemaining && CountdownRemaining < 0.76f) + PushGCD(AID.Gekko, primaryTarget); + return; } - GoalZoneCombined(3, Hints.GoalAOECircle(NumStickers == 2 ? 8 : 5), 3, pos.Item1); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(NumStickers == 2 ? 8 : 5), AID.Fuga, 3, pos.Item1, 20); EmergencyMeikyo(strategy, primaryTarget); UseKaeshi(primaryTarget); UseIaijutsu(primaryTarget); - if (OgiLeft > GCD && TargetDotLeft > 10 && HaveFugetsu) + if (OgiLeft > GCD && TargetDotLeft > 10 && HaveFugetsu && (RaidBuffsLeft > GCD || RaidBuffsIn > 1000)) PushGCD(AID.OgiNamikiri, BestOgiTarget); if (MeikyoLeft > GCD) - PushGCD(MeikyoAction, NumAOECircleTargets > 2 ? Player : primaryTarget); + PushGCD(MeikyoAction, NumAOECircleTargets > 2 ? null : primaryTarget); if (ComboLastMove == AOEStarter && NumAOECircleTargets > 0) { @@ -269,7 +273,7 @@ private AID MeikyoAction } } - private void UseKaeshi(Actor? primaryTarget) + private void UseKaeshi(Enemy? primaryTarget) { // namikiri combo is broken by other gcds, other followups are not if (KaeshiNamikiri) @@ -283,16 +287,16 @@ private void UseKaeshi(Actor? primaryTarget) PushGCD(aid, target); } - private (AID, Actor?) KaeshiToAID(Actor? primaryTarget, Kaeshi k) => k switch + private (AID, Enemy?) KaeshiToAID(Enemy? primaryTarget, Kaeshi k) => k switch { Kaeshi.Setsugekka => (AID.KaeshiSetsugekka, primaryTarget), Kaeshi.TendoSetsugekka => (AID.TendoKaeshiSetsugekka, primaryTarget), - Kaeshi.Goken => (AID.KaeshiGoken, Player), - Kaeshi.TendoGoken => (AID.TendoKaeshiGoken, Player), + Kaeshi.Goken => (AID.KaeshiGoken, null), + Kaeshi.TendoGoken => (AID.TendoKaeshiGoken, null), _ => (default, null) }; - private void UseIaijutsu(Actor? primaryTarget) + private void UseIaijutsu(Enemy? primaryTarget) { if (!HaveFugetsu || NumStickers == 0) return; @@ -322,7 +326,7 @@ void kaeshi() } } - private void EmergencyMeikyo(StrategyValues strategy, Actor? primaryTarget) + private void EmergencyMeikyo(StrategyValues strategy, Enemy? primaryTarget) { // special case for if we got thrust into combat with no prep if (MeikyoLeft == 0 && !HaveFugetsu && CombatTimer < 5 && primaryTarget != null) @@ -362,34 +366,35 @@ private void EmergencyMeikyo(StrategyValues strategy, Actor? primaryTarget) return (Positional.Any, false); } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { - if (primaryTarget == null || !HaveFugetsu) + if (primaryTarget == null || !HaveFugetsu || !Player.InCombat) return; if (strategy.BuffsOk()) - { PushOGCD(AID.Ikishoten, Player); - if (Zanshin > World.Client.AnimationLock && Kenki >= 50) - PushOGCD(AID.Zanshin, BestOgiTarget); - - if (Kenki >= 25 && Zanshin == 0) - { - if (NumLineTargets > 1) - PushOGCD(AID.HissatsuGuren, BestLineTarget); - - // queue senei since guren may not be unlocked (gated by job quest) - PushOGCD(AID.HissatsuSenei, primaryTarget); - // queue guren since senei may not be unlocked (unlocks at level 72) + if (Kenki >= 25 && (RaidBuffsLeft > AnimLock || RaidBuffsIn > (Unlocked(TraitID.EnhancedHissatsu) ? 40 : 100))) + { + if (NumLineTargets > 1) PushOGCD(AID.HissatsuGuren, BestLineTarget); - } + + // queue senei since guren may not be unlocked (gated by job quest) + PushOGCD(AID.HissatsuSenei, primaryTarget); + // queue guren since senei may not be unlocked (unlocks at level 72) + PushOGCD(AID.HissatsuGuren, BestLineTarget); } + if (Kenki >= 50 && Zanshin > 0 && ReadyIn(AID.HissatsuSenei) > 30) + PushOGCD(AID.Zanshin, BestOgiTarget); + if (Meditation == 3) PushOGCD(AID.Shoha, BestLineTarget); - if (Kenki >= 25 && ReadyIn(AID.HissatsuGuren) > 10 && Zanshin == 0) + var saveKenki = RaidBuffsLeft <= AnimLock || Zanshin > 0 || ReadyIn(AID.HissatsuSenei) < 10; + var maxKenki = ReadyIn(AID.Ikishoten) < 15 ? 50 : 90; + + if (Kenki >= (saveKenki ? maxKenki : 25)) { if (NumAOECircleTargets > 2) PushOGCD(AID.HissatsuKyuten, Player); diff --git a/BossMod/Autorotation/xan/Melee/VPR.cs b/BossMod/Autorotation/xan/Melee/VPR.cs index a281c92d94..eef5107de1 100644 --- a/BossMod/Autorotation/xan/Melee/VPR.cs +++ b/BossMod/Autorotation/xan/Melee/VPR.cs @@ -1,6 +1,7 @@ using BossMod.VPR; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using System.Runtime.InteropServices; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -53,12 +54,12 @@ public enum TwinType public int NumAOETargets; public int NumRangedAOETargets; - private Actor? BestRangedAOETarget; - private Actor? BestGenerationTarget; + private Enemy? BestRangedAOETarget; + private Enemy? BestGenerationTarget; private int CoilMax => Unlocked(TraitID.EnhancedVipersRattle) ? 3 : 2; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 3); @@ -116,7 +117,19 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) OGCD(strategy, primaryTarget); - if (CombatTimer < 1 && Player.DistanceToHitbox(primaryTarget) is > 3 and < 20) + if (CountdownRemaining > 0 || primaryTarget == null) + return; + + var aoeBreakpoint = DreadCombo switch + { + DreadCombo.Dreadwinder or DreadCombo.HuntersCoil or DreadCombo.SwiftskinsCoil => 50, + DreadCombo.HuntersDen or DreadCombo.SwiftskinsDen or DreadCombo.PitOfDread => 1, + _ => Anguine > 0 ? 50 : 3 + }; + + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.SteelMaw, aoeBreakpoint, pos.Item1, 20); + + if (CombatTimer < 0.5f && Player.DistanceToHitbox(primaryTarget) > 3) PushGCD(AID.Slither, primaryTarget); if (ShouldReawaken(strategy)) @@ -275,9 +288,9 @@ private bool ShouldReawaken(StrategyValues strategy) private bool ShouldVice(StrategyValues strategy) => Swiftscaled > GCD && DreadCombo == 0 && ReadyIn(AID.Vicewinder) <= GCD; - private bool ShouldCoil(StrategyValues strategy) => Coil > 1 && Swiftscaled > GCD && DreadCombo == 0; + private bool ShouldCoil(StrategyValues strategy) => Coil == CoilMax && Swiftscaled > GCD && DreadCombo == 0; - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (!Player.InCombat || primaryTarget == null) return; @@ -332,6 +345,12 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) if (DreadCombo == DreadCombo.SwiftskinsCoil) return (Positional.Flank, true); + if (DreadCombo is DreadCombo.HuntersDen or DreadCombo.SwiftskinsDen or DreadCombo.PitOfDread) + return (Positional.Any, false); + + if (NumAOETargets > 2) + return (Positional.Any, false); + return ComboLastMove switch { AID.HuntersSting => (Positional.Flank, true), diff --git a/BossMod/Autorotation/xan/Ranged/BRD.cs b/BossMod/Autorotation/xan/Ranged/BRD.cs index 223fb5b356..a891f45c33 100644 --- a/BossMod/Autorotation/xan/Ranged/BRD.cs +++ b/BossMod/Autorotation/xan/Ranged/BRD.cs @@ -1,5 +1,6 @@ using BossMod.BRD; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -54,14 +55,14 @@ public enum CodaSongs : byte public int NumConeTargets; // 12y/90(?)deg cone - regular aoe gcds public int NumLineTargets; // 25y/4y rect - apex arrow and stuff - private Actor? BestCircleTarget; - private Actor? BestConeTarget; - private Actor? BestLineTarget; - private Actor? BestDotTarget; + private Enemy? BestCircleTarget; + private Enemy? BestConeTarget; + private Enemy? BestLineTarget; + private Enemy? BestDotTarget; public int Codas => (Coda.HasFlag(CodaSongs.MagesBallad) ? 1 : 0) + (Coda.HasFlag(CodaSongs.ArmysPaeon) ? 1 : 0) + (Coda.HasFlag(CodaSongs.WanderersMinuet) ? 1 : 0); - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 25); @@ -100,6 +101,9 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) return; } + if (primaryTarget != null) + GoalZoneCombined(strategy, 25, Hints.GoalAOECone(primaryTarget.Actor, 12, 45.Degrees()), AID.QuickNock, minAoe: 2); + var ijDelay = EffectApplicationDelay(AID.IronJaws); if (CanFitGCD(TargetDotLeft.Min - ijDelay) && !CanFitGCD(TargetDotLeft.Min - ijDelay, 1)) @@ -149,7 +153,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) return (MathF.Min(wind, poison), wind, poison); } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (!Player.InCombat || primaryTarget == null) return; diff --git a/BossMod/Autorotation/xan/Ranged/DNC.cs b/BossMod/Autorotation/xan/Ranged/DNC.cs index 287f1ac937..095a310dc4 100644 --- a/BossMod/Autorotation/xan/Ranged/DNC.cs +++ b/BossMod/Autorotation/xan/Ranged/DNC.cs @@ -1,5 +1,6 @@ using BossMod.DNC; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -44,9 +45,9 @@ public static RotationModuleDefinition Definition() public float FinishingMoveLeft; // 30s max public float DanceOfTheDawnLeft; // 30s max - private Actor? BestFan4Target; - private Actor? BestRangedAOETarget; - private Actor? BestStarfallTarget; + private Enemy? BestFan4Target; + private Enemy? BestRangedAOETarget; + private Enemy? BestStarfallTarget; public int NumAOETargets; public int NumDanceTargets; @@ -59,9 +60,15 @@ public static RotationModuleDefinition Definition() protected override float GetCastTime(AID aid) => 0; - private bool HaveTarget(Actor? primaryTarget) => NumAOETargets > 1 || primaryTarget != null; + private bool HaveTarget(Enemy? primaryTarget) => NumAOETargets > 1 || primaryTarget != null; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + private static float GetApplicationDelay(AID aid) => aid switch + { + AID.StandardFinish or AID.SingleStandardFinish or AID.DoubleStandardFinish => 0.54f, + _ => 0 + }; + + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, range: 25); @@ -110,7 +117,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) var approach = IsDancing || ReadyIn(AID.StandardStep) <= GCD || ReadyIn(AID.TechnicalStep) <= GCD; - GoalZoneCombined(approach ? 15 : 25, Hints.GoalAOECircle(IsDancing ? 15 : 5), 2); + GoalZoneCombined(strategy, approach ? 15 : 25, Hints.GoalAOECircle(IsDancing ? 15 : 5), AID.StandardFinish, 2); if (IsDancing) { @@ -209,7 +216,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (CountdownRemaining > 0) { @@ -228,7 +235,7 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) if (ReadyIn(AID.Devilment) > 55) PushOGCD(AID.Flourish, Player); - if ((TechFinishLeft == 0 || OnCooldown(AID.Devilment)) && ThreefoldLeft > World.Client.AnimationLock && NumRangedAOETargets > 0) + if ((TechFinishLeft == 0 || OnCooldown(AID.Devilment)) && ThreefoldLeft > AnimLock && NumRangedAOETargets > 0) PushOGCD(AID.FanDanceIII, BestRangedAOETarget); var canF1 = ShouldSpendFeathers(strategy); @@ -237,7 +244,7 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) if (Feathers == 4 && canF1) PushOGCD(f1ToUse, primaryTarget); - if (OnCooldown(AID.Devilment) && FourfoldLeft > World.Client.AnimationLock && NumFan4Targets > 0) + if (OnCooldown(AID.Devilment) && FourfoldLeft > AnimLock && NumFan4Targets > 0) PushOGCD(AID.FanDanceIV, BestFan4Target); if (canF1) @@ -249,8 +256,12 @@ private bool ShouldStdStep(StrategyValues strategy) if (ReadyIn(AID.StandardStep) > GCD) return false; + var stdFinishCast = GCD + 3.5f; + var stdFinishDamage = stdFinishCast + GetApplicationDelay(AID.StandardFinish); + return NumDanceTargets > 0 && - (TechFinishLeft == 0 || TechFinishLeft > GCD + 3.5 || !Unlocked(AID.TechnicalStep)); + DowntimeIn > stdFinishDamage && + (TechFinishLeft == 0 || TechFinishLeft > stdFinishCast || !Unlocked(AID.TechnicalStep)); } private bool ShouldTechStep(StrategyValues strategy) @@ -265,7 +276,7 @@ private bool ShouldTechStep(StrategyValues strategy) return NumDanceTargets > 0 && StandardFinishLeft > GCD + TechStepDuration + TechFinishDuration; } - private bool CanFlow(Actor? primaryTarget, out AID action) + private bool CanFlow(Enemy? primaryTarget, out AID action) { var act = NumAOETargets > 1 ? AID.Bloodshower : AID.Fountainfall; if (Unlocked(act) && FlowLeft > GCD && HaveTarget(primaryTarget)) @@ -278,7 +289,7 @@ private bool CanFlow(Actor? primaryTarget, out AID action) return false; } - private bool CanSymmetry(Actor? primaryTarget, out AID action) + private bool CanSymmetry(Enemy? primaryTarget, out AID action) { var act = NumAOETargets > 1 ? AID.RisingWindmill : AID.ReverseCascade; if (Unlocked(act) && SymmetryLeft > GCD && HaveTarget(primaryTarget)) @@ -317,14 +328,14 @@ private bool ShouldSpendFeathers(StrategyValues strategy) if (Feathers == 4 || !Unlocked(AID.TechnicalStep)) return true; - return TechFinishLeft > World.Client.AnimationLock; + return TechFinishLeft > AnimLock; } private bool IsFan4Target(Actor primary, Actor other) => Hints.TargetInAOECone(other, Player.Position, 15, Player.DirectionTo(primary), 60.Degrees()); private Actor? FindDancePartner() { - var partner = World.Party.WithoutSlot(excludeAlliance: true).Exclude(Player).Where(x => Player.DistanceToHitbox(x) <= 30).MaxBy(p => p.Class switch + var partner = World.Party.WithoutSlot(excludeAlliance: true, excludeNPCs: true).Exclude(Player).Where(x => Player.DistanceToHitbox(x) <= 30).MaxBy(p => p.Class switch { Class.SAM => 100, Class.NIN or Class.VPR or Class.ROG => 99, diff --git a/BossMod/Autorotation/xan/Ranged/MCH.cs b/BossMod/Autorotation/xan/Ranged/MCH.cs index c0f87cda75..09de5caa3f 100644 --- a/BossMod/Autorotation/xan/Ranged/MCH.cs +++ b/BossMod/Autorotation/xan/Ranged/MCH.cs @@ -1,11 +1,12 @@ using BossMod.MCH; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; public sealed class MCH(RotationModuleManager manager, Actor player) : Attackxan(manager, player) { - public enum Track { Queen = SharedTrack.Count } + public enum Track { Queen = SharedTrack.Count, Wildfire, Hypercharge, Tools } public enum QueenStrategy { MinGauge, @@ -13,12 +14,18 @@ public enum QueenStrategy RaidBuffsOnly, Never } + public enum WildfireStrategy + { + ASAP, + Delay, + Hypercharge + } public static RotationModuleDefinition Definition() { var def = new RotationModuleDefinition("xan MCH", "Machinist", "Standard rotation (xan)|Ranged", "xan", RotationModuleQuality.Basic, BitMask.Build(Class.MCH), 100); - def.DefineShared().AddAssociatedActions(AID.BarrelStabilizer, AID.Wildfire); + def.DefineShared().AddAssociatedActions(AID.BarrelStabilizer); def.Define(Track.Queen).As("Queen", "Queen") .AddOption(QueenStrategy.MinGauge, "Min", "Summon at 50+ gauge") @@ -27,6 +34,14 @@ public static RotationModuleDefinition Definition() .AddOption(QueenStrategy.Never, "Never", "Do not automatically summon Queen at all") .AddAssociatedActions(AID.AutomatonQueen, AID.RookAutoturret); + def.Define(Track.Wildfire).As("WF", "Wildfire") + .AddOption(WildfireStrategy.ASAP, "ASAP", "Use as soon as possible (delay in opener until after Full Metal Field)") + .AddOption(WildfireStrategy.Delay, "Delay", "Do not use") + .AddOption(WildfireStrategy.Hypercharge, "Hypercharge", "Delay until Hypercharge window"); + + def.DefineSimple(Track.Hypercharge, "Hypercharge").AddAssociatedActions(AID.Hypercharge); + def.DefineSimple(Track.Tools, "Tools").AddAssociatedActions(AID.Drill, AID.AirAnchor, AID.ChainSaw, AID.Bioblaster); + return def; } @@ -49,13 +64,13 @@ public static RotationModuleDefinition Definition() public int NumSawTargets; public int NumFlamethrowerTargets; - private Actor? BestAOETarget; - private Actor? BestRangedAOETarget; - private Actor? BestChainsawTarget; + private Enemy? BestAOETarget; + private Enemy? BestRangedAOETarget; + private Enemy? BestChainsawTarget; private bool IsPausedForFlamethrower => Service.Config.Get().PauseForFlamethrower && Flamethrower; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, range: 25); @@ -87,18 +102,19 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (CountdownRemaining > 0) { - if (CountdownRemaining < 0.4) + if (CountdownRemaining < 1.15f) + { PushGCD(AID.AirAnchor, primaryTarget); + PushGCD(AID.Drill, primaryTarget); + } return; } if (primaryTarget != null) { - var aoebreakpoint = 3; - if (Overheated && Unlocked(AID.AutoCrossbow)) - aoebreakpoint = 4; - GoalZoneCombined(25, Hints.GoalAOECone(primaryTarget, 12, 60.Degrees()), aoebreakpoint); + var aoebreakpoint = Overheated && Unlocked(AID.AutoCrossbow) ? 4 : 3; + GoalZoneCombined(strategy, 25, Hints.GoalAOECone(primaryTarget.Actor, 12, 60.Degrees()), AID.SpreadShot, aoebreakpoint); } if (Overheated && Unlocked(AID.HeatBlast)) @@ -118,17 +134,22 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (ExcavatorLeft > GCD) PushGCD(AID.Excavator, BestRangedAOETarget); - if (ReadyIn(AID.AirAnchor) <= GCD) - PushGCD(AID.AirAnchor, primaryTarget, priority: 20); + var toolOk = strategy.Simple(Track.Tools) != OffensiveStrategy.Delay; - if (ReadyIn(AID.ChainSaw) <= GCD) - PushGCD(AID.ChainSaw, BestChainsawTarget, 10); + if (toolOk) + { + if (ReadyIn(AID.AirAnchor) <= GCD) + PushGCD(AID.AirAnchor, primaryTarget, priority: 20); + + if (ReadyIn(AID.ChainSaw) <= GCD) + PushGCD(AID.ChainSaw, BestChainsawTarget, 10); - if (ReadyIn(AID.Bioblaster) <= GCD && NumAOETargets > 2) - PushGCD(AID.Bioblaster, BestAOETarget, priority: MaxChargesIn(AID.Bioblaster) <= GCD ? 20 : 2); + if (ReadyIn(AID.Bioblaster) <= GCD && NumAOETargets > 2) + PushGCD(AID.Bioblaster, BestAOETarget, priority: MaxChargesIn(AID.Bioblaster) <= GCD ? 20 : 2); - if (ReadyIn(AID.Drill) <= GCD) - PushGCD(AID.Drill, primaryTarget, priority: MaxChargesIn(AID.Drill) <= GCD ? 20 : 2); + if (ReadyIn(AID.Drill) <= GCD) + PushGCD(AID.Drill, primaryTarget, priority: MaxChargesIn(AID.Drill) <= GCD ? 20 : 2); + } // TODO work out priorities if (FMFLeft > GCD && ExcavatorLeft == 0) @@ -138,7 +159,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) PushGCD(AID.Scattergun, BestAOETarget); // different cdgroup? - if (!Unlocked(AID.AirAnchor) && ReadyIn(AID.HotShot) <= GCD) + if (!Unlocked(AID.AirAnchor) && ReadyIn(AID.HotShot) <= GCD && toolOk) PushGCD(AID.HotShot, primaryTarget); if (NumAOETargets > 2 && Unlocked(AID.SpreadShot)) @@ -155,12 +176,12 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) } } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (CountdownRemaining is > 0 and < 5 && ReassembleLeft == 0) PushOGCD(AID.Reassemble, Player); - if (CountdownRemaining == null && !Player.InCombat && Player.DistanceToHitbox(primaryTarget) <= 25 && NextToolCharge == 0 && ReassembleLeft == 0) + if (CountdownRemaining == null && !Player.InCombat && Player.DistanceToHitbox(primaryTarget) <= 25 && NextToolCharge == 0 && ReassembleLeft == 0 && !Overheated) PushGCD(AID.Reassemble, Player, 30); if (IsPausedForFlamethrower || !Player.InCombat || primaryTarget == null) @@ -190,42 +211,29 @@ private void OGCD(StrategyValues strategy, Actor? primaryTarget) private float MaxGaussCD => MaxChargesIn(AID.GaussRound); private float MaxRicochetCD => MaxChargesIn(AID.Ricochet); - private void UseCharges(StrategyValues strategy, Actor? primaryTarget) + private void UseCharges(StrategyValues strategy, Enemy? primaryTarget) { - var gaussRoundCD = ReadyIn(AID.GaussRound); - var ricochetCD = ReadyIn(AID.Ricochet); - - var canGauss = Unlocked(AID.GaussRound) && CanWeave(gaussRoundCD, 0.6f); - var canRicochet = Unlocked(AID.Ricochet) && CanWeave(ricochetCD, 0.6f); - - if (canGauss && CanWeave(MaxGaussCD, 0.6f)) + // checking for max charges + if (CanWeave(MaxGaussCD, 0.6f)) PushOGCD(AID.GaussRound, Unlocked(AID.DoubleCheck) ? BestRangedAOETarget : primaryTarget); - - if (canRicochet && CanWeave(MaxRicochetCD, 0.6f)) + if (CanWeave(MaxRicochetCD, 0.6f)) PushOGCD(AID.Ricochet, BestRangedAOETarget); var useAllCharges = RaidBuffsLeft > 0 || RaidBuffsIn > 9000 || Overheated || !Unlocked(AID.Hypercharge); if (!useAllCharges) return; - // this is a little awkward but we want to try to keep the cooldowns of both actions within range of each other - if (canGauss && canRicochet) - { - if (gaussRoundCD > ricochetCD) - UseRicochet(primaryTarget); - else - UseGauss(primaryTarget); - } - else if (canGauss) - UseGauss(primaryTarget); - else if (canRicochet) - UseRicochet(primaryTarget); + var gelapse = World.Client.Cooldowns[14].Elapsed; + var relapse = World.Client.Cooldowns[15].Elapsed; + + UseGauss(primaryTarget, gelapse > relapse ? 1 : 0); + UseRicochet(primaryTarget, relapse > gelapse ? 1 : 0); } - private void UseGauss(Actor? primaryTarget) => Hints.ActionsToExecute.Push(ActionID.MakeSpell(AID.GaussRound), Unlocked(AID.DoubleCheck) ? BestRangedAOETarget : primaryTarget, ActionQueue.Priority.Low - 50); - private void UseRicochet(Actor? primaryTarget) => Hints.ActionsToExecute.Push(ActionID.MakeSpell(AID.Ricochet), BestRangedAOETarget, ActionQueue.Priority.Low - 50); + private void UseGauss(Enemy? primaryTarget, int charges) => Hints.ActionsToExecute.Push(ActionID.MakeSpell(AID.GaussRound), (Unlocked(AID.DoubleCheck) ? BestRangedAOETarget : primaryTarget)?.Actor, ActionQueue.Priority.Low - 50 + charges); + private void UseRicochet(Enemy? primaryTarget, int charges) => Hints.ActionsToExecute.Push(ActionID.MakeSpell(AID.Ricochet), BestRangedAOETarget?.Actor, ActionQueue.Priority.Low - 50 + charges); - private bool ShouldReassemble(StrategyValues strategy, Actor? primaryTarget) + private bool ShouldReassemble(StrategyValues strategy, Enemy? primaryTarget) { if (ReassembleLeft > 0 || !Unlocked(AID.Reassemble) || Overheated || primaryTarget == null) return false; @@ -237,14 +245,16 @@ private bool ShouldReassemble(StrategyValues strategy, Actor? primaryTarget) return false; if (!Unlocked(AID.Drill)) - { return ComboLastMove == (Unlocked(AID.CleanShot) ? AID.SlugShot : AID.SplitShot); - } + + // past 58 we only reassemble on tool charges so don't bother + if (strategy.Simple(Track.Tools) == OffensiveStrategy.Delay) + return false; return NextToolCharge <= GCD; } - private bool ShouldMinion(StrategyValues strategy, Actor? primaryTarget) + private bool ShouldMinion(StrategyValues strategy, Enemy? primaryTarget) { if (!Unlocked(AID.RookAutoturret) || primaryTarget == null || HasMinion || Battery < 50 || ShouldWildfire(strategy)) return false; @@ -261,9 +271,24 @@ private bool ShouldMinion(StrategyValues strategy, Actor? primaryTarget) private bool ShouldHypercharge(StrategyValues strategy) { - if (!Unlocked(AID.Hypercharge) || HyperchargedLeft == 0 && Heat < 50 || Overheated || ReassembleLeft > GCD) + // strategy-independent preconditions, hypercharge cannot be used at all in these cases + if (!Unlocked(AID.Hypercharge) || HyperchargedLeft == 0 && Heat < 50 || Overheated) return false; + // don't want to use reassemble on heat blast, even if strategy is Force, since presumably next GCD will be a tool charge + if (ReassembleLeft > GCD) + return false; + + switch (strategy.Simple(Track.Hypercharge)) + { + case OffensiveStrategy.Force: + return true; + case OffensiveStrategy.Delay: + return false; + default: + break; + } + // avoid delaying wildfire // TODO figure out how long we actually need to wait to ensure enough heat if (ReadyIn(AID.Wildfire) < 20 && !ShouldWildfire(strategy)) @@ -273,6 +298,9 @@ private bool ShouldHypercharge(StrategyValues strategy) if (FMFLeft > 0 && GCD > 1.1f) return false; + if (DowntimeIn < GCD + 6) + return false; + /* A full segment of Hypercharge is exactly three GCDs worth of time, or 7.5 seconds. Because of this, you should never enter Hypercharge if Chainsaw, Drill or Air Anchor has less than eight seconds on their cooldown timers. Doing so will cause the Chainsaw, Drill or Air Anchor cooldowns to drift, which leads to a loss of DPS and will more than likely cause issues down the line in your rotation when you reach your rotational reset at Wildfire. */ return NextToolCap > GCD + 7.5f; @@ -280,9 +308,14 @@ private bool ShouldHypercharge(StrategyValues strategy) private bool ShouldWildfire(StrategyValues strategy) { - if (!Unlocked(AID.Wildfire) || !CanWeave(AID.Wildfire) || !strategy.BuffsOk()) + var wfStrat = strategy.Option(Track.Wildfire).As(); + + if (!Unlocked(AID.Wildfire) || !CanWeave(AID.Wildfire) || wfStrat == WildfireStrategy.Delay) return false; + if (wfStrat == WildfireStrategy.Hypercharge) + return Overheated || HyperchargedLeft > 0 || Heat >= 50; + // hack for opener - delay until all 4 tool charges are used if (CombatTimer < 60) return NextToolCharge > GCD; diff --git a/BossMod/Autorotation/xan/Tanks/DRK.cs b/BossMod/Autorotation/xan/Tanks/DRK.cs index a863f4a4b6..b6ac042b0a 100644 --- a/BossMod/Autorotation/xan/Tanks/DRK.cs +++ b/BossMod/Autorotation/xan/Tanks/DRK.cs @@ -1,5 +1,6 @@ using BossMod.DRK; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; public sealed class DRK(RotationModuleManager manager, Actor player) : Attackxan(manager, player) @@ -45,10 +46,10 @@ public static RotationModuleDefinition Definition() public int NumRangedAOETargets; public int NumLineTargets; - private Actor? BestRangedAOETarget; - private Actor? BestLineTarget; + private Enemy? BestRangedAOETarget; + private Enemy? BestLineTarget; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 3); @@ -73,7 +74,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (CountdownRemaining > 0) return; - GoalZoneCombined(3, Hints.GoalAOECircle(5), 3); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.Unleash, 3, maximumActionRange: 20); if (Darkside > GCD) { @@ -138,7 +139,7 @@ public enum OGCDPriority EdgeRefresh = 900, } - private void OGCD(StrategyValues strategy, Actor? primaryTarget) + private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { if (primaryTarget == null || !Player.InCombat) return; @@ -190,7 +191,7 @@ private bool ShouldBlood(StrategyValues strategy) return Blood + (impendingBlood ? 20 : 0) > 100; } - private void Edge(StrategyValues strategy, Actor? primaryTarget) + private void Edge(StrategyValues strategy, Enemy? primaryTarget) { var canUse = MP >= 3000 || DarkArts; var canUseTBN = MP >= 6000 || DarkArts || !Unlocked(AID.TheBlackestNight); diff --git a/BossMod/Autorotation/xan/Tanks/GNB.cs b/BossMod/Autorotation/xan/Tanks/GNB.cs index 9539742dce..ec71a10e6e 100644 --- a/BossMod/Autorotation/xan/Tanks/GNB.cs +++ b/BossMod/Autorotation/xan/Tanks/GNB.cs @@ -1,5 +1,6 @@ using BossMod.GNB; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; @@ -19,18 +20,18 @@ public static RotationModuleDefinition Definition() public float Reign; public float SonicBreak; - public bool Continuation; + public AID Continuation; public float NoMercy; public int NumAOETargets; public int NumReignTargets; - private Actor? BestReignTarget; + private Enemy? BestReignTarget; public bool FastGCD => GCDLength <= 2.47f; public int MaxAmmo => Unlocked(TraitID.CartridgeChargeII) ? 3 : 2; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 3); @@ -40,7 +41,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) Reign = StatusLeft(SID.ReadyToReign); SonicBreak = StatusLeft(SID.ReadyToBreak); - Continuation = Player.Statuses.Any(s => IsContinuationStatus((SID)s.ID)); + Continuation = GetContinuation(); NoMercy = StatusLeft(SID.NoMercy); NumAOETargets = NumMeleeAOETargets(strategy); @@ -51,7 +52,7 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) if (CountdownRemaining > 0) return; - GoalZoneCombined(3, Hints.GoalAOECircle(5), Unlocked(AID.FatedCircle) && Ammo > 0 ? 2 : 3); + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.DemonSlice, Unlocked(AID.FatedCircle) && Ammo > 0 ? 2 : 3, maximumActionRange: 20); if (ReadyIn(AID.NoMercy) > 20 && Ammo > 0) PushGCD(AID.GnashingFang, primaryTarget); @@ -127,13 +128,12 @@ private bool ShouldBust(StrategyValues strategy, AID spend) return ComboLastMove is AID.BrutalShell or AID.DemonSlice && Ammo == MaxAmmo; } - private void CalcNextBestOGCD(StrategyValues strategy, Actor? primaryTarget) + private void CalcNextBestOGCD(StrategyValues strategy, Enemy? primaryTarget) { if (!Player.InCombat || primaryTarget == null) return; - if (Continuation) - PushOGCD(AID.Continuation, primaryTarget); + PushOGCD(Continuation, primaryTarget); if (strategy.BuffsOk() && Unlocked(AID.Bloodfest) && Ammo == 0) PushOGCD(AID.Bloodfest, primaryTarget); @@ -162,5 +162,25 @@ private void UseNoMercy(StrategyValues strategy) PushOGCD(AID.NoMercy, Player, delay: GCD - 0.8f); } - private bool IsContinuationStatus(SID sid) => sid is SID.ReadyToBlast or SID.ReadyToRaze or SID.ReadyToGouge or SID.ReadyToTear or SID.ReadyToRip; + private AID GetContinuation() + { + foreach (var s in Player.Statuses) + { + switch ((SID)s.ID) + { + case SID.ReadyToBlast: + return AID.Hypervelocity; + case SID.ReadyToRaze: + return AID.FatedBrand; + case SID.ReadyToRip: + return AID.JugularRip; + case SID.ReadyToGouge: + return AID.EyeGouge; + case SID.ReadyToTear: + return AID.AbdomenTear; + } + } + + return AID.None; + } } diff --git a/BossMod/Autorotation/xan/Tanks/PLD.cs b/BossMod/Autorotation/xan/Tanks/PLD.cs index cfba2fa9a4..35ec9d45fa 100644 --- a/BossMod/Autorotation/xan/Tanks/PLD.cs +++ b/BossMod/Autorotation/xan/Tanks/PLD.cs @@ -1,18 +1,69 @@ using BossMod.PLD; using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using static BossMod.AIHints; namespace BossMod.Autorotation.xan; public sealed class PLD(RotationModuleManager manager, Actor player) : Attackxan(manager, player) { - public enum Track { Intervene = SharedTrack.Count } + public enum Track { Intervene = SharedTrack.Count, HolySpirit, Atonement } + + public enum HSStrategy + { + Standard, + ForceDM, + Force, + Ranged, + Delay + } + public enum AtonementStrategy + { + Automatic, + Force, + Delay + } + public enum DashStrategy + { + Automatic, + GapCloser, + Delay + } + + public enum GCDPriority + { + None = 0, + HS = 100, + Standard = 500, + Atonement = 600, + DMHS = 650, + AtonementCombo = 700, + BladeCombo = 750, + GoringBlade = 800, + Force = 900 + } public static RotationModuleDefinition Definition() { var def = new RotationModuleDefinition("xan PLD", "Paladin", "Standard rotation (xan)|Tanks", "xan", RotationModuleQuality.Basic, BitMask.Build(Class.PLD, Class.GLA), 100); def.DefineShared().AddAssociatedActions(AID.FightOrFlight); - def.DefineSimple(Track.Intervene, "Dash").AddAssociatedActions(AID.Intervene); + + def.Define(Track.Intervene).As("Intervene") + .AddOption(DashStrategy.Automatic, "Use during burst window", minLevel: 66) + .AddOption(DashStrategy.GapCloser, "Use if outside melee range", minLevel: 66) + .AddOption(DashStrategy.Delay, "Do not use", minLevel: 66) + .AddAssociatedActions(AID.Intervene); + + def.Define(Track.HolySpirit).As("HS") + .AddOption(HSStrategy.Standard, "Use during Divine Might only; ASAP in burst, otherwise when out of melee range, or if next GCD will overwrite DM", minLevel: 64) + .AddOption(HSStrategy.ForceDM, "Use ASAP during next Divine Might proc, regardless of range", minLevel: 64) + .AddOption(HSStrategy.Force, "Use now, even if in melee range or if DM is not active", minLevel: 64) + .AddOption(HSStrategy.Ranged, "Always use when out of melee range", minLevel: 64) + .AddOption(HSStrategy.Delay, "Do not use", minLevel: 64) + .AddAssociatedActions(AID.HolySpirit); + + def.DefineSimple(Track.Atonement, "Atone", minLevel: 76) + .AddAssociatedActions(AID.Atonement, AID.Supplication, AID.Sepulchre); return def; } @@ -34,7 +85,7 @@ public static RotationModuleDefinition Definition() public int NumAOETargets; - private Actor? BestRangedTarget; + private Enemy? BestRangedTarget; protected override float GetCastTime(AID aid) => aid switch { @@ -42,7 +93,7 @@ public static RotationModuleDefinition Definition() _ => 0 }; - public override void Exec(StrategyValues strategy, Actor? primaryTarget) + public override void Exec(StrategyValues strategy, Enemy? primaryTarget) { SelectPrimaryTarget(strategy, ref primaryTarget, 3); @@ -70,79 +121,62 @@ public override void Exec(StrategyValues strategy, Actor? primaryTarget) NumAOETargets = NumMeleeAOETargets(strategy); - CalcNextBestOGCD(strategy, primaryTarget); - if (CountdownRemaining > 0) { - if (CountdownRemaining < GetCastTime(AID.HolySpirit)) + if (CountdownRemaining < GetCastTime(AID.HolySpirit) + 0.76f) PushGCD(AID.HolySpirit, primaryTarget); return; } - GoalZoneCombined(3, Hints.GoalAOECircle(5), 3); + CalcNextBestOGCD(strategy, primaryTarget); + + GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.TotalEclipse, 3, maximumActionRange: 20); if (ConfiteorCombo != AID.None && MP >= 1000) - PushGCD(ConfiteorCombo, BestRangedTarget); + PushGCD(ConfiteorCombo, BestRangedTarget, GCDPriority.BladeCombo); - // use goring blade even in AOE if (GoringBladeReady > GCD) - PushGCD(AID.GoringBlade, primaryTarget, priority: 50); + PushGCD(AID.GoringBlade, primaryTarget, GCDPriority.GoringBlade); if (NumAOETargets >= 3 && Unlocked(AID.TotalEclipse)) { if ((Requiescat.Left > GCD || DivineMight > GCD && FightOrFlight > GCD) && MP >= 1000) - PushGCD(AID.HolyCircle, Player); + PushGCD(AID.HolyCircle, Player, GCDPriority.Standard); if (ComboLastMove == AID.TotalEclipse) { if (DivineMight > GCD && MP >= 1000) - PushGCD(AID.HolyCircle, Player); + PushGCD(AID.HolyCircle, Player, GCDPriority.Standard); - PushGCD(AID.Prominence, Player); + PushGCD(AID.Prominence, Player, GCDPriority.Standard); } - PushGCD(AID.TotalEclipse, Player); + PushGCD(AID.TotalEclipse, Player, GCDPriority.Standard); + return; } - else - { - // fallback - cast holy spirit if we don't have a melee - if (DivineMight > GCD && MP >= 1000) - Hints.ActionsToExecute.Push(ActionID.MakeSpell(AID.HolySpirit), primaryTarget, ActionQueue.Priority.High - 50); - - if (Requiescat.Left > GCD || DivineMight > GCD && FightOrFlight > GCD) - PushGCD(AID.HolySpirit, primaryTarget); - if (AtonementReady > GCD && FightOrFlight > GCD) - PushGCD(AID.Atonement, primaryTarget); + UseHS(strategy, primaryTarget); + UseAtone(strategy, primaryTarget); - if (SepulchreReady > GCD) - PushGCD(AID.Sepulchre, primaryTarget); + if (SepulchreReady > GCD) + PushGCD(AID.Sepulchre, primaryTarget, GCDPriority.AtonementCombo); - if (SupplicationReady > GCD) - PushGCD(AID.Supplication, primaryTarget); + if (SupplicationReady > GCD) + PushGCD(AID.Supplication, primaryTarget, GCDPriority.AtonementCombo); - if (ComboLastMove == AID.RiotBlade) - { - if (DivineMight > GCD && MP >= 1000) - PushGCD(AID.HolySpirit, primaryTarget); - - if (AtonementReady > GCD) - PushGCD(AID.Atonement, primaryTarget); - - PushGCD(AID.RageOfHalone, primaryTarget); - } + if (ComboLastMove == AID.RiotBlade) + PushGCD(AID.RageOfHalone, primaryTarget, GCDPriority.Standard); - if (ComboLastMove == AID.FastBlade) - PushGCD(AID.RiotBlade, primaryTarget); + if (ComboLastMove == AID.FastBlade) + PushGCD(AID.RiotBlade, primaryTarget, GCDPriority.Standard); - PushGCD(AID.FastBlade, primaryTarget); - } + PushGCD(AID.FastBlade, primaryTarget, GCDPriority.Standard); } - private void CalcNextBestOGCD(StrategyValues strategy, Actor? primaryTarget) + private void CalcNextBestOGCD(StrategyValues strategy, Enemy? primaryTarget) { - if (primaryTarget == null || CountdownRemaining > 0) + if (primaryTarget == null || !Player.InCombat) return; if (ShouldFoF(strategy, primaryTarget)) @@ -167,24 +201,75 @@ private void CalcNextBestOGCD(StrategyValues strategy, Actor? primaryTarget) PushOGCD(AID.CircleOfScorn, Player); } - switch (strategy.Simple(Track.Intervene)) + switch (strategy.Option(Track.Intervene).As()) { - case OffensiveStrategy.Automatic: + case DashStrategy.Automatic: if (FightOrFlight > 0) PushOGCD(AID.Intervene, primaryTarget); break; + case DashStrategy.GapCloser: + if (Player.DistanceToHitbox(primaryTarget) > 3) + PushOGCD(AID.Intervene, primaryTarget); + break; + } + } + + private void UseHS(StrategyValues strategy, Enemy? primaryTarget) + { + var track = strategy.Option(Track.HolySpirit).As(); + + if (MP < 1000 || track == HSStrategy.Delay) + return; + + var requiescat = Requiescat.Left > GCD; + var divineMight = DivineMight > GCD; + var fof = FightOrFlight > GCD; + + var useStandard = divineMight && fof || requiescat || divineMight && ComboLastMove == AID.RiotBlade; + + var prio = strategy.Option(Track.HolySpirit).As() switch + { + HSStrategy.Standard => useStandard ? GCDPriority.DMHS : GCDPriority.None, + HSStrategy.ForceDM => divineMight ? GCDPriority.Force : GCDPriority.None, + HSStrategy.Force => GCDPriority.Force, + HSStrategy.Ranged => useStandard ? GCDPriority.DMHS : GCDPriority.HS, + _ => GCDPriority.None + }; + + PushGCD(AID.HolySpirit, primaryTarget, prio); + } + + private void UseAtone(StrategyValues strategy, Enemy? primaryTarget) + { + if (AtonementReady <= GCD) + return; + + switch (strategy.Simple(Track.Atonement)) + { + case OffensiveStrategy.Automatic: + if (FightOrFlight > GCD) + // use after DMHS, which is higher potency + PushGCD(AID.Atonement, primaryTarget, GCDPriority.Atonement); + + if (ComboLastMove == AID.RiotBlade) + PushGCD(AID.Atonement, primaryTarget, GCDPriority.AtonementCombo); + break; case OffensiveStrategy.Force: - PushOGCD(AID.Intervene, primaryTarget); + if (AtonementReady > GCD) + PushGCD(AID.Atonement, primaryTarget, GCDPriority.Force); break; } } - private bool ShouldFoF(StrategyValues strategy, Actor? primaryTarget) + private bool ShouldFoF(StrategyValues strategy, Enemy? primaryTarget) { + if (strategy.Simple(SharedTrack.Buffs) == OffensiveStrategy.Delay) + return false; + if (!Unlocked(TraitID.DivineMagicMastery1)) return true; // hold FoF until 3rd GCD for opener, otherwise use on cooldown - return DivineMight > 0 || CombatTimer > 30; + return DivineMight > 0 || CombatTimer > 30 || strategy.Simple(SharedTrack.Buffs) == OffensiveStrategy.Force; } } diff --git a/BossMod/BossModule/AIHints.cs b/BossMod/BossModule/AIHints.cs index 5a2ffde7d4..26161fef2e 100644 --- a/BossMod/BossModule/AIHints.cs +++ b/BossMod/BossModule/AIHints.cs @@ -204,7 +204,7 @@ public Func GoalSingleTarget(WPos target, float radius, float weigh var effRsq = radius * radius; return p => (p - target).LengthSq() <= effRsq ? weight : 0; } - public Func GoalSingleTarget(Actor target, float range) => GoalSingleTarget(target.Position, range + target.HitboxRadius + 0.5f); + public Func GoalSingleTarget(Actor target, float range, float weight = 1) => GoalSingleTarget(target.Position, range + target.HitboxRadius + 0.5f, weight); // simple goal zone that returns 1 if target is in range (usually melee), 2 if it's also in correct positional public Func GoalSingleTarget(WPos target, Angle rotation, Positional positional, float radius) diff --git a/BossMod/BossModule/RaidCooldowns.cs b/BossMod/BossModule/RaidCooldowns.cs index 568ddfb485..3f3ec59623 100644 --- a/BossMod/BossModule/RaidCooldowns.cs +++ b/BossMod/BossModule/RaidCooldowns.cs @@ -39,10 +39,10 @@ public float NextDamageBuffIn() } // TODO: why do we need two versions?.. - public float NextDamageBuffIn2() + public float? NextDamageBuffIn2() { if (_damageCooldowns.Count == 0) - return float.MaxValue; + return null; var firstAvailable = _damageCooldowns.Select(e => e.AvailableAt).Min(); return MathF.Min(float.MaxValue, (float)(firstAvailable - _ws.CurrentTime).TotalSeconds); diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index 6e13ea4919..85bec4b8a3 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -1,4 +1,6 @@ -namespace BossMod; +using static BossMod.AIHints; + +namespace BossMod; // objkind << 8 + objsubkind public enum ActorType : ushort @@ -130,6 +132,7 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public int PredictedMPRaw => (int)HPMP.CurMP + PendingMPDiffence; public int PredictedHPClamped => Math.Clamp(PredictedHPRaw, 0, (int)HPMP.MaxHP); public bool PredictedDead => PredictedHPRaw <= 1 && !IsStrikingDummy; + public float PredictedHPRatio => (float)PredictedHPRaw / HPMP.MaxHP; // if expirationForPredicted is not null, search pending first, and return one if found; in that case only low byte of extra will be set public ActorStatus? FindStatus(uint sid, DateTime? expirationForPending = null) @@ -163,6 +166,7 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public Angle AngleTo(Actor other) => Angle.FromDirection(other.Position - Position); public float DistanceToHitbox(Actor? other) => other == null ? float.MaxValue : (other.Position - Position).Length() - other.HitboxRadius - HitboxRadius; + public float DistanceToHitbox(Enemy? other) => DistanceToHitbox(other?.Actor); public override string ToString() => $"{OID:X} '{Name}' <{InstanceID:X}>"; } From 70d28999d6111cc9085b0df61976cdc8f6fea46e Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:12:13 -0500 Subject: [PATCH 12/33] goodbye --- BossMod/Autorotation/xan/AI/AIBase.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/BossMod/Autorotation/xan/AI/AIBase.cs b/BossMod/Autorotation/xan/AI/AIBase.cs index b9a9813a19..d72e038e88 100644 --- a/BossMod/Autorotation/xan/AI/AIBase.cs +++ b/BossMod/Autorotation/xan/AI/AIBase.cs @@ -12,12 +12,6 @@ public abstract class AIBase(RotationModuleManager manager, Actor player) : Rota internal bool ShouldInterrupt(AIHints.Enemy e) => e.Actor.InCombat && e.ShouldBeInterrupted && (e.Actor.CastInfo?.Interruptible ?? false); internal bool ShouldStun(AIHints.Enemy e) => e.Actor.InCombat && e.ShouldBeStunned; - internal bool IsCastReactable(Actor act) - { - var castInfo = act.CastInfo; - return !(castInfo == null || castInfo.TotalTime <= 1.5 || castInfo.EventHappened); - } - internal IEnumerable EnemiesAutoingMe => Hints.PriorityTargets.Where(x => x.Actor.CastInfo == null && x.Actor.TargetID == Player.InstanceID && Player.DistanceToHitbox(x.Actor) <= 6); internal IEnumerable Raidwides => Hints.PredictedDamage.Where(d => World.Party.WithSlot(excludeAlliance: true).IncludedInMask(d.players).Count() >= 2).Select(t => t.activation); From 42594cc155dc749ae26a54e6074347669a4c0aa3 Mon Sep 17 00:00:00 2001 From: ace Date: Mon, 20 Jan 2025 10:16:19 -0800 Subject: [PATCH 13/33] targeting update --- BossMod/Autorotation/akechi/AkechiBLM.cs | 82 ++++++++++++++++++------ 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index 607a9ce5f7..bc314585a0 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -391,10 +391,52 @@ private bool JustUsed(AID aid, float variance) } #region Targeting - private int TargetsInRange() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 26); //Returns the number of targets within 26-yalm radius around the player - private bool ShouldUseAOE => TargetsInRange() >= 3; //Check if we should use AOE + private int TargetsInRange() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 25); //Returns the number of targets within 26-yalm radius around the player + private bool ShouldUseAOE + { + get + { + var bestTarget = BestAOETarget; + if (bestTarget != null) + { + var minimumTargetsForAOE = 2; + + //Are there enough targets in the general area? + if (TargetsInRange() < minimumTargetsForAOE) + { + return false; + } + + float splashPriorityFunc(Actor actor) + { + var distanceToPlayer = actor.DistanceToHitbox(Player); + if (distanceToPlayer < 26f) + { + var targetsInSplashRadius = 0; + foreach (var enemy in Hints.PriorityTargets) + { + var targetActor = enemy.Actor; + if (targetActor != actor && targetActor.Position.InCircle(actor.Position, 5f)) + { + targetsInSplashRadius++; + } + } + return targetsInSplashRadius; + } + return float.MinValue; + } + + var (_, bestPrio) = FindBetterTargetBy(null, 25f, splashPriorityFunc); + + return bestPrio >= minimumTargetsForAOE; + } + + return false; + } + } + private Actor? TargetChoice(StrategyValues.OptionRef strategy) => ResolveTargetOverride(strategy.Value); //Resolves the target choice based on the strategy - private Actor? FindBestTarget() + private Actor? FindBestAOETarget() { float AOEPriorityFunc(Actor actor) { @@ -414,13 +456,11 @@ float AOEPriorityFunc(Actor actor) } return float.MinValue; } - float STPriorityFunc(Actor actor) => actor.HPMP.CurHP > 0 ? 1f / actor.HPMP.CurHP : float.MinValue; - var (bestAOETarget, bestAOEPrio) = FindBetterTargetBy(null, 25f, STPriorityFunc); - var (bestTarget, bestPrio) = FindBetterTargetBy(bestAOETarget, 25f, AOEPriorityFunc); - return ShouldUseAOE ? bestAOETarget : bestTarget; + var (BestAOETarget, bestPrio) = FindBetterTargetBy(null, 25f, AOEPriorityFunc); + return BestAOETarget; } - private Actor? BestTarget => FindBestTarget(); // Find the best target for splash attack + private Actor? BestAOETarget => FindBestAOETarget(); // Find the best target for splash attack //TODO: BestDOTTarget #endregion @@ -513,11 +553,11 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa (Unlocked(TraitID.EnhancedAstralFire) && MP is < 1600 and not 0)))) //instant cast Despair { if (AOEStrategy is AOEStrategy.Auto) - BestRotation(TargetChoice(AOE) ?? BestTarget ?? primaryTarget); //target prio is user choice -> current target -> best AOE target + BestRotation(TargetChoice(AOE) ?? BestAOETarget ?? primaryTarget); if (forceST) - BestST(TargetChoice(AOE) ?? primaryTarget); //target prio is user choice -> current target + BestST(TargetChoice(AOE) ?? primaryTarget); if (forceAOE) - BestAOE(TargetChoice(AOE) ?? primaryTarget); //target prio is user choice -> best AOE target -> current target + BestAOE(TargetChoice(AOE) ?? primaryTarget); } #endregion @@ -535,17 +575,17 @@ or MovementStrategy.AllowNoScathe { if (Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0) QueueGCD(forceST ? BestXenoglossy : forceAOE ? AID.Foul : BestPolyglot, - TargetChoice(polyglot) ?? primaryTarget ?? BestTarget, + TargetChoice(polyglot) ?? primaryTarget ?? BestAOETarget, GCDPriority.Moving1); if (PlayerHasEffect(SID.Firestarter, 30)) QueueGCD(AID.Fire3, - TargetChoice(AOE) ?? primaryTarget ?? BestTarget, + TargetChoice(AOE) ?? primaryTarget ?? BestAOETarget, GCDPriority.Moving1); if (hasThunderhead) QueueGCD(forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder, - TargetChoice(thunder) ?? primaryTarget ?? BestTarget, + TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, GCDPriority.Moving1); } } @@ -565,7 +605,7 @@ or MovementStrategy.AllowNoScathe or MovementStrategy.OnlyScathe) { if (Unlocked(AID.Scathe) && MP >= 800) - QueueGCD(AID.Scathe, TargetChoice(AOE) ?? primaryTarget ?? BestTarget, GCDPriority.Moving1); + QueueGCD(AID.Scathe, TargetChoice(AOE) ?? primaryTarget ?? BestAOETarget, GCDPriority.Moving1); } } #endregion @@ -601,17 +641,17 @@ or MovementStrategy.AllowNoScathe { if (AOEStrategy is AOEStrategy.Auto) QueueGCD(BestThunder, - TargetChoice(thunder) ?? primaryTarget ?? BestTarget, + TargetChoice(thunder), ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); if (forceST) QueueGCD(BestThunderST, - TargetChoice(thunder) ?? primaryTarget ?? BestTarget, + TargetChoice(thunder) ?? primaryTarget, ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); if (forceAOE) QueueGCD(BestThunderAOE, - TargetChoice(thunder) ?? primaryTarget ?? BestTarget, + TargetChoice(thunder) ?? primaryTarget, ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); } @@ -623,7 +663,7 @@ or PolyglotStrategy.AutoHold1 or PolyglotStrategy.AutoHold2 or PolyglotStrategy.AutoHold3) QueueGCD(BestPolyglot, - TargetChoice(polyglot) ?? primaryTarget ?? BestTarget, + TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget, polyglotStrat is PolyglotStrategy.ForceXeno ? GCDPriority.ForcedGCD : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot : GCDPriority.Polyglot); @@ -632,7 +672,7 @@ or PolyglotStrategy.XenoHold1 or PolyglotStrategy.XenoHold2 or PolyglotStrategy.XenoHold3) QueueGCD(BestXenoglossy, - TargetChoice(polyglot) ?? primaryTarget ?? BestTarget, + TargetChoice(polyglot) ?? primaryTarget, polyglotStrat is PolyglotStrategy.ForceXeno ? GCDPriority.ForcedGCD : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot : GCDPriority.Polyglot); @@ -641,7 +681,7 @@ or PolyglotStrategy.FoulHold1 or PolyglotStrategy.FoulHold2 or PolyglotStrategy.FoulHold3) QueueGCD(AID.Foul, - TargetChoice(polyglot) ?? primaryTarget ?? BestTarget, + TargetChoice(polyglot) ?? primaryTarget, polyglotStrat is PolyglotStrategy.ForceFoul ? GCDPriority.ForcedGCD : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot : GCDPriority.Polyglot); From f922e9c6188e15828445a03a5973b69ee33a3158 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Mon, 20 Jan 2025 14:05:41 -0500 Subject: [PATCH 14/33] properties --- BossMod/Autorotation/xan/AI/Healer.cs | 15 +++++++----- BossMod/Autorotation/xan/Tanks/GNB.cs | 35 ++++++++++++++------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/BossMod/Autorotation/xan/AI/Healer.cs b/BossMod/Autorotation/xan/AI/Healer.cs index 8a6b552561..aab9d86b11 100644 --- a/BossMod/Autorotation/xan/AI/Healer.cs +++ b/BossMod/Autorotation/xan/AI/Healer.cs @@ -265,14 +265,17 @@ private void AutoAST(StrategyValues strategy) Hints.ActionsToExecute.Push(ActionID.MakeSpell(BossMod.AST.AID.EarthlyStar), Player, ActionQueue.Priority.Medium, targetPos: Player.PosRot.XYZ()); } - private Vector3? GetArenaCenter() + private Vector3? ArenaCenter { - if (Bossmods.ActiveModule is BossModule m) + get { - var center = m.Arena.Center; - return new Vector3(center.X, Player.PosRot.Y, center.Z); + if (Bossmods.ActiveModule is BossModule m) + { + var center = m.Arena.Center; + return new Vector3(center.X, Player.PosRot.Y, center.Z); + } + return null; } - return null; } private void AutoSCH(StrategyValues strategy, Actor? primaryTarget) @@ -281,7 +284,7 @@ void UseSoil(Vector3? location = null) { if (World.Client.GetGauge().Aetherflow == 0) return; - location ??= GetArenaCenter() ?? Player.PosRot.XYZ(); + location ??= ArenaCenter ?? Player.PosRot.XYZ(); Hints.ActionsToExecute.Push(ActionID.MakeSpell(BossMod.SCH.AID.SacredSoil), null, ActionQueue.Priority.Medium + 5, targetPos: location.Value); } diff --git a/BossMod/Autorotation/xan/Tanks/GNB.cs b/BossMod/Autorotation/xan/Tanks/GNB.cs index ec71a10e6e..beb720ae0f 100644 --- a/BossMod/Autorotation/xan/Tanks/GNB.cs +++ b/BossMod/Autorotation/xan/Tanks/GNB.cs @@ -20,7 +20,6 @@ public static RotationModuleDefinition Definition() public float Reign; public float SonicBreak; - public AID Continuation; public float NoMercy; public int NumAOETargets; @@ -41,7 +40,6 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) Reign = StatusLeft(SID.ReadyToReign); SonicBreak = StatusLeft(SID.ReadyToBreak); - Continuation = GetContinuation(); NoMercy = StatusLeft(SID.NoMercy); NumAOETargets = NumMeleeAOETargets(strategy); @@ -162,25 +160,28 @@ private void UseNoMercy(StrategyValues strategy) PushOGCD(AID.NoMercy, Player, delay: GCD - 0.8f); } - private AID GetContinuation() + private AID Continuation { - foreach (var s in Player.Statuses) + get { - switch ((SID)s.ID) + foreach (var s in Player.Statuses) { - case SID.ReadyToBlast: - return AID.Hypervelocity; - case SID.ReadyToRaze: - return AID.FatedBrand; - case SID.ReadyToRip: - return AID.JugularRip; - case SID.ReadyToGouge: - return AID.EyeGouge; - case SID.ReadyToTear: - return AID.AbdomenTear; + switch ((SID)s.ID) + { + case SID.ReadyToBlast: + return AID.Hypervelocity; + case SID.ReadyToRaze: + return AID.FatedBrand; + case SID.ReadyToRip: + return AID.JugularRip; + case SID.ReadyToGouge: + return AID.EyeGouge; + case SID.ReadyToTear: + return AID.AbdomenTear; + } } - } - return AID.None; + return AID.None; + } } } From 8c589183330c7a486d0ae3f712dd97a0ee20cd64 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:27:04 -0500 Subject: [PATCH 15/33] deep dungeon state wip --- BossMod/ActionQueue/ActionDefinition.cs | 21 +++ BossMod/Data/ActionID.cs | 2 + BossMod/Data/DeepDungeonState.cs | 180 ++++++++++++++++++++++++ BossMod/Data/PomanderID.cs | 48 +++++++ BossMod/Data/WorldState.cs | 3 + BossMod/Framework/WorldStateGameSync.cs | 80 +++++++++++ BossMod/Replay/ReplayParserLog.cs | 47 ++++++- 7 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 BossMod/Data/DeepDungeonState.cs create mode 100644 BossMod/Data/PomanderID.cs diff --git a/BossMod/ActionQueue/ActionDefinition.cs b/BossMod/ActionQueue/ActionDefinition.cs index 949b454609..8cc973050c 100644 --- a/BossMod/ActionQueue/ActionDefinition.cs +++ b/BossMod/ActionQueue/ActionDefinition.cs @@ -197,6 +197,27 @@ private ActionDefinitions() // bozja actions for (var i = BozjaHolsterID.None + 1; i < BozjaHolsterID.Count; ++i) RegisterBozja(i); + + // pomanders + for (var i = PomanderID.Safety; i < PomanderID.Count; ++i) + { + var pid = new ActionID(ActionType.Pomander, (uint)i); + _definitions[pid] = new(pid) + { + InstantAnimLock = 2.1f, + AllowedTargets = ActionTargets.Self + }; + } + + for (var i = 1u; i <= 3; i++) + { + var mid = new ActionID(ActionType.Magicite, i); + _definitions[mid] = new(mid) + { + InstantAnimLock = 2.1f, + AllowedTargets = ActionTargets.Self + }; + } } public void Dispose() diff --git a/BossMod/Data/ActionID.cs b/BossMod/Data/ActionID.cs index c8faac2f72..1655f46cf0 100644 --- a/BossMod/Data/ActionID.cs +++ b/BossMod/Data/ActionID.cs @@ -25,6 +25,8 @@ public enum ActionType : byte // below are custom additions, these aren't proper actions from game's point of view, but it makes sense for us to treat them as such BozjaHolsterSlot0 = 0xE0, // id = BozjaHolsterID, use from holster to replace duty action 0 BozjaHolsterSlot1 = 0xE1, // id = BozjaHolsterID, use from holster to replace duty action 1 + Pomander = 0xE2, // id = PomanderID + Magicite = 0xE3, // id = slot (1-3) } public enum Positional { Any, Flank, Rear, Front } diff --git a/BossMod/Data/DeepDungeonState.cs b/BossMod/Data/DeepDungeonState.cs new file mode 100644 index 0000000000..6dbc9fb2d4 --- /dev/null +++ b/BossMod/Data/DeepDungeonState.cs @@ -0,0 +1,180 @@ +using static FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon; + +namespace BossMod; + +public sealed class DeepDungeonState +{ + public DungeonProgress Progress; + public byte DungeonId; + public RoomFlags[] MapData = new RoomFlags[25]; + public PartyMember[] Party = new PartyMember[4]; + public Item[] Items = new Item[16]; + public Chest[] Chests = new Chest[16]; + public byte[] Magicite = new byte[3]; + + public enum DungeonType : byte + { + None = 0, + POTD = 1, + HOH = 2, + EO = 3 + } + + public DungeonType Type => (DungeonType)DungeonId; + + public record struct DungeonProgress(byte Floor, byte Tileset, byte WeaponLevel, byte ArmorLevel, byte SyncedGearLevel, byte HoardCount, byte ReturnProgress, byte PassageProgress) + { + public readonly bool IsBossFloor => Floor % 10 == 0; + } + public record struct PartyMember(ulong EntityId, byte Room); + public record struct Item(byte Count, byte Flags) + { + public readonly bool Usable => (Flags & (1 << 0)) != 0; + public readonly bool Active => (Flags & (1 << 1)) != 0; + } + public record struct Chest(byte Type, byte Room); + + public Item GetItem(PomanderID pid) => GetSlotForPomander(pid) is var s && s >= 0 ? Items[s] : default; + + public int GetSlotForPomander(PomanderID pid) => Service.LuminaRow(DungeonId)!.Value.PomanderSlot.ToList().FindIndex(p => p.RowId == (uint)pid); + public PomanderID GetPomanderForSlot(int slot) + { + var slots = Service.LuminaRow(DungeonId)!.Value.PomanderSlot; + return slot >= 0 && slot < slots.Count ? (PomanderID)slots[slot].RowId : PomanderID.None; + } + + public bool ReturnActive => Progress.ReturnProgress >= 11; + public bool PassageActive => Progress.PassageProgress >= 11; + public byte Floor => Progress.Floor; + + public IEnumerable CompareToInitial() + { + if (Progress != default || DungeonId != 0) + yield return new OpProgressChange(DungeonId, Progress); + + if (MapData.Any(m => m > 0)) + yield return new OpMapDataChange(MapData); + + if (Party.Any(p => p != default)) + yield return new OpPartyStateChange(Party); + + if (Items.Any(i => i != default)) + yield return new OpItemsChange(Items); + + if (Chests.Any(c => c != default)) + yield return new OpChestsChange(Chests); + + if (Magicite.Any(c => c > 0)) + yield return new OpMagiciteChange(Magicite); + } + + public Event ProgressChanged = new(); + public sealed record class OpProgressChange(byte DungeonId, DungeonProgress Value) : WorldState.Operation + { + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.DungeonId = DungeonId; + ws.DeepDungeon.Progress = Value; + ws.DeepDungeon.ProgressChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDPG"u8) + .Emit(DungeonId) + .Emit(Value.Floor) + .Emit(Value.Tileset) + .Emit(Value.WeaponLevel) + .Emit(Value.ArmorLevel) + .Emit(Value.SyncedGearLevel) + .Emit(Value.HoardCount) + .Emit(Value.ReturnProgress) + .Emit(Value.PassageProgress); + } + } + + public Event MapDataChanged = new(); + public sealed record class OpMapDataChange(RoomFlags[] Value) : WorldState.Operation + { + public readonly RoomFlags[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.MapData = Value; + ws.DeepDungeon.MapDataChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDMP"u8).Emit(Array.ConvertAll(Value, r => (byte)r)); + } + } + + public Event PartyStateChanged = new(); + public sealed record class OpPartyStateChange(PartyMember[] Value) : WorldState.Operation + { + public readonly PartyMember[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.Party = Value; + ws.DeepDungeon.PartyStateChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDPT"u8); + foreach (var member in Value) + output.EmitActor(member.EntityId).Emit(member.Room); + } + } + + public Event ItemsChanged = new(); + public sealed record class OpItemsChange(Item[] Value) : WorldState.Operation + { + public readonly Item[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.Items = Value; + ws.DeepDungeon.ItemsChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDIT"u8); + foreach (var item in Value) + output.Emit(item.Count).Emit(item.Flags, "X"); + } + } + + public Event ChestsChanged = new(); + public sealed record class OpChestsChange(Chest[] Value) : WorldState.Operation + { + public readonly Chest[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.Chests = Value; + ws.DeepDungeon.ChestsChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDCT"u8); + foreach (var chest in Value) + output.Emit(chest.Type).Emit(chest.Room); + } + } + + public Event MagiciteChanged = new(); + public sealed record class OpMagiciteChange(byte[] Value) : WorldState.Operation + { + public readonly byte[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.Magicite = Value; + ws.DeepDungeon.MagiciteChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDMG"u8).Emit(Value); + } + } +} diff --git a/BossMod/Data/PomanderID.cs b/BossMod/Data/PomanderID.cs new file mode 100644 index 0000000000..3bbc827320 --- /dev/null +++ b/BossMod/Data/PomanderID.cs @@ -0,0 +1,48 @@ +namespace BossMod; + +public enum PomanderID : uint +{ + None, + + // Pomanders - PotD/HoH + Safety, + Sight, + Strength, + Steel, + Affluence, + Flight, + Alteration, + Purity, + Fortune, + Witching, + Serenity, + Rage, // palace only + Lust, // palace only + Intuition, + Raising, + Resolution, // palace only + Frailty, // HoH only + Concealment, // HoH only + Petrification, // HoH only + + // Protomanders - EO + ProtoLethargy, + ProtoStorms, + ProtoDread, + ProtoSafety, + ProtoSight, + ProtoStrength, + ProtoSteel, + ProtoAffluence, + ProtoFlight, + ProtoAlteration, + ProtoPurity, + ProtoFortune, + ProtoWitching, + ProtoSerenity, + ProtoIntuition, + ProtoRaising, + + Count +} + diff --git a/BossMod/Data/WorldState.cs b/BossMod/Data/WorldState.cs index e03690d9af..c527daed9d 100644 --- a/BossMod/Data/WorldState.cs +++ b/BossMod/Data/WorldState.cs @@ -16,6 +16,7 @@ public sealed class WorldState public readonly ActorState Actors = new(); public readonly PartyState Party; public readonly ClientState Client = new(); + public readonly DeepDungeonState DeepDungeon = new(); public readonly NetworkState Network = new(); public DateTime CurrentTime => Frame.Timestamp; @@ -69,6 +70,8 @@ public IEnumerable CompareToInitial() yield return o; foreach (var o in Network.CompareToInitial()) yield return o; + foreach (var o in DeepDungeon.CompareToInitial()) + yield return o; } // implementation of operations diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs index 8c95de6219..52392137c2 100644 --- a/BossMod/Framework/WorldStateGameSync.cs +++ b/BossMod/Framework/WorldStateGameSync.cs @@ -4,6 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Game.Fate; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; @@ -182,6 +183,7 @@ public unsafe void Update(TimeSpan prevFramePerf) UpdateActors(); UpdateParty(); UpdateClient(); + UpdateDeepDungeon(); } private unsafe void UpdateWaymarks() @@ -646,6 +648,84 @@ private unsafe void UpdateClient() _ws.Execute(new ClientState.OpFocusTargetChange(focusTargetId)); } + private unsafe void UpdateDeepDungeon() + { + var ddold = _ws.DeepDungeon; + var ddnew = GetDeepDungeonState(); + + if (ddold.DungeonId != ddnew.DungeonId || ddold.Progress != ddnew.Progress) + _ws.Execute(new DeepDungeonState.OpProgressChange(ddnew.DungeonId, ddnew.Progress)); + if (!MemoryExtensions.SequenceEqual(ddold.MapData, ddnew.MapData)) + _ws.Execute(new DeepDungeonState.OpMapDataChange(ddnew.MapData)); + if (!MemoryExtensions.SequenceEqual(ddold.Party, ddnew.Party)) + _ws.Execute(new DeepDungeonState.OpPartyStateChange(ddnew.Party)); + if (!MemoryExtensions.SequenceEqual(ddold.Items, ddnew.Items)) + _ws.Execute(new DeepDungeonState.OpItemsChange(ddnew.Items)); + if (!MemoryExtensions.SequenceEqual(ddold.Chests, ddnew.Chests)) + _ws.Execute(new DeepDungeonState.OpChestsChange(ddnew.Chests)); + if (!MemoryExtensions.SequenceEqual(ddold.Magicite, ddnew.Magicite)) + _ws.Execute(new DeepDungeonState.OpMagiciteChange(ddnew.Magicite)); + } + + private unsafe DeepDungeonState GetDeepDungeonState() + { + var dd = EventFramework.Instance()->GetInstanceContentDeepDungeon(); + if (dd == null) + return new(); + + var progress = new DeepDungeonState.DungeonProgress + { + Floor = dd->Floor, + WeaponLevel = dd->WeaponLevel, + ArmorLevel = dd->ArmorLevel, + + SyncedGearLevel = dd->SyncedGearLevel, + HoardCount = dd->HoardCount, + + ReturnProgress = dd->ReturnProgress, + PassageProgress = dd->PassageProgress, + + Tileset = dd->ActiveLayoutIndex + }; + + var state = new DeepDungeonState + { + Progress = progress, + Magicite = dd->Magicite.ToArray(), + DungeonId = dd->DeepDungeonId + }; + + dd->MapData.CopyTo(state.MapData); + + var ddParty = dd->Party; + for (var i = 0; i < 4; i++) + { + ref var pinfo = ref state.Party[i]; + pinfo.EntityId = (uint)SanitizedObjectID(ddParty[i].EntityId); + pinfo.Room = SanitizeRoom(ddParty[i].RoomIndex); + } + + var ddItem = dd->Items; + for (var i = 0; i < ddItem.Length; i++) + { + ref var pitem = ref state.Items[i]; + pitem.Count = ddItem[i].Count; + pitem.Flags = ddItem[i].Flags; + } + + var ddChest = dd->Chests; + for (var i = 0; i < ddChest.Length; i++) + { + ref var pchest = ref state.Chests[i]; + pchest.Type = ddChest[i].ChestType; + pchest.Room = SanitizeRoom(ddChest[i].RoomIndex); + } + + return state; + } + + private byte SanitizeRoom(sbyte room) => room < 0 ? (byte)0 : (byte)room; + private ulong SanitizedObjectID(ulong raw) => raw != InvalidEntityId ? raw : 0; private void DispatchActorEvents(ulong instanceID) diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index 9c2f43237f..9f039efa6f 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Threading; @@ -340,6 +341,12 @@ private ReplayParserLog(Input input, ReplayBuilder builder) [new("CLAF"u8)] = ParseClientActiveFate, [new("CPET"u8)] = ParseClientActivePet, [new("CLFT"u8)] = ParseClientFocusTarget, + [new("DDPG"u8)] = ParseDeepDungeonProgress, + [new("DDMP"u8)] = ParseDeepDungeonMap, + [new("DDPT"u8)] = ParseDeepDungeonParty, + [new("DDIT"u8)] = ParseDeepDungeonItems, + [new("DDCT"u8)] = ParseDeepDungeonChests, + [new("DDMG"u8)] = ParseDeepDungeonMagicite, [new("IPCI"u8)] = ParseNetworkIDScramble, [new("IPCS"u8)] = ParseNetworkServerIPC, }; @@ -686,6 +693,44 @@ private ClientState.OpClassJobLevelsChange ParseClientClassJobLevels() private ClientState.OpActivePetChange ParseClientActivePet() => new(new(_input.ReadULong(true), _input.ReadByte(false), _input.ReadByte(false))); private ClientState.OpFocusTargetChange ParseClientFocusTarget() => new(_input.ReadULong(true)); + private DeepDungeonState.OpProgressChange ParseDeepDungeonProgress() => new(_input.ReadByte(false), new DeepDungeonState.DungeonProgress(_input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false))); + private DeepDungeonState.OpMapDataChange ParseDeepDungeonMap() => new(Array.ConvertAll(_input.ReadBytes(), b => (InstanceContentDeepDungeon.RoomFlags)b)); + private DeepDungeonState.OpPartyStateChange ParseDeepDungeonParty() + { + var pt = new DeepDungeonState.PartyMember[4]; + for (var i = 0; i < pt.Length; i++) + { + ref var p = ref pt[i]; + p.EntityId = _input.ReadActorID(); + p.Room = _input.ReadByte(false); + } + return new(pt); + } + private DeepDungeonState.OpItemsChange ParseDeepDungeonItems() + { + var it = new DeepDungeonState.Item[16]; + for (var i = 0; i < it.Length; i++) + { + ref var item = ref it[i]; + item.Count = _input.ReadByte(false); + item.Flags = _input.ReadByte(true); + } + return new(it); + } + private DeepDungeonState.OpChestsChange ParseDeepDungeonChests() + { + var ct = new DeepDungeonState.Chest[16]; + for (var i = 0; i < ct.Length; i++) + { + ref var chest = ref ct[i]; + chest.Type = _input.ReadByte(false); + chest.Room = _input.ReadByte(false); + } + return new(ct); + } + + private DeepDungeonState.OpMagiciteChange ParseDeepDungeonMagicite() => new(_input.ReadBytes()); + private NetworkState.OpIDScramble ParseNetworkIDScramble() => new(_input.ReadUInt(false)); private NetworkState.OpServerIPC ParseNetworkServerIPC() => new(new((Network.ServerIPC.PacketID)_input.ReadInt(), _input.ReadUShort(false), _input.ReadUInt(false), _input.ReadUInt(true), new(_input.ReadLong()), _input.ReadBytes())); From a68341ed3e1edbb61ed8169549f2faed1b35338a Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:25:54 -0500 Subject: [PATCH 16/33] lol --- .../Dawntrail/Quest/SomewhereOnlySheKnows.cs | 230 +++++++++++++++ .../LifeEphemeralPathEternal/AncelRockfist.cs | 59 ++++ .../Quest/LifeEphemeralPathEternal/Enums.cs | 74 +++++ .../LifeEphemeralPathEternal/Guildivain.cs | 91 ++++++ BossMod/Modules/Endwalker/Quest/SagesFocus.cs | 67 +++++ .../Modules/Endwalker/Quest/TheKillingArt.cs | 87 ++++++ .../Endwalker/Quest/WorthyOfHisBack.cs | 2 +- .../Heavensward/Quest/ASpectacleForTheAges.cs | 34 +++ .../Heavensward/Quest/AtTheEndOfOurHope.cs | 18 ++ .../Quest/CloseEncountersOfTheVIthKind.cs | 62 +++++ .../Heavensward/Quest/DivineIntervention.cs | 63 +++++ .../Modules/Heavensward/Quest/DragoonsFate.cs | 97 +++++++ .../Heavensward/Quest/FlyFreeMyPretty.cs | 111 ++++++++ .../Heavensward/Quest/OneLifeOneWorld.cs | 12 +- .../Heavensward/Quest/TheFateOfStars.cs | 50 ++++ .../RealmReborn/Quest/OperationArchon.cs | 66 +++++ .../RealmReborn/Quest/TheStepsOfFaith.cs | 263 ++++++++++++++++++ .../RealmReborn/Quest/TheUltimateWeapon.cs | 140 ++++++++++ .../Shadowbringers/Quest/AFeastOfLies.cs | 90 ++++++ .../Shadowbringers/Quest/ASleepDisturbed.cs | 11 +- .../Shadowbringers/Quest/ATearfulReunion.cs | 107 +++++++ .../Shadowbringers/Quest/CourageBornOfFear.cs | 102 +++++++ .../Quest/DeathUntoDawn/P1TelotekGamma.cs | 35 +++ .../Quest/DeathUntoDawn/P2LunarOdin.cs | 110 ++++++++ .../Quest/DeathUntoDawn/P3LunarRavana.cs | 109 ++++++++ .../Quest/DeathUntoDawn/P4LunarIfrit.cs | 44 +++ .../Quest/FadedMemories/Ardbert.cs | 113 ++++++++ .../Quest/FadedMemories/FadedMemories.cs | 53 ++++ .../Quest/FadedMemories/FlameGeneralAldynn.cs | 18 ++ .../Quest/FadedMemories/KingThordan.cs | 23 ++ .../Quest/FadedMemories/Nidhogg.cs | 15 + .../Quest/FadedMemories/Zenos.cs | 21 ++ .../Shadowbringers/Quest/FullSteamAhead.cs | 126 +++++++++ .../Shadowbringers/Quest/GambolingForGil.cs | 88 ++++++ .../Shadowbringers/Quest/NyelbertsLament.cs | 118 ++++++++ .../Quest/SaveTheLastDanceForMe.cs | 84 ++++++ .../SleepNowInSapphire/P1GuidanceSystem.cs | 38 +++ .../SleepNowInSapphire/P2SapphireWeapon.cs | 86 ++++++ .../Shadowbringers/Quest/SteelAgainstSteel.cs | 128 +++++++++ .../Quest/TheGreatShipVylbrand.cs | 116 ++++++++ .../Shadowbringers/Quest/TheHardenedHeart.cs | 143 ++++++++++ .../Shadowbringers/Quest/TheHuntersLegacy.cs | 92 ++++++ .../Quest/TheLostAndTheFound/Sophrosyne.cs | 29 ++ .../Quest/TheLostAndTheFound/Yxtlilton.cs | 87 ++++++ .../Shadowbringers/Quest/TheOracleOfLight.cs | 40 +++ .../Quest/TheSoulOfTemperance.cs | 76 +++++ .../Quest/ToHaveLovedAndLost.cs | 79 ++++++ .../Quest/VowsOfVirtueDeedsOfCruelty.cs | 139 +++++++++ .../Quest/ARequiemForHeroes/Enums.cs | 29 ++ .../Stormblood/Quest/ARequiemForHeroes/P1.cs | 51 ++++ .../Stormblood/Quest/ARequiemForHeroes/P2.cs | 89 ++++++ .../Stormblood/Quest/AnArtForTheLiving.cs | 114 ++++++++ .../Quest/BestServedWithColdSteel.cs | 156 +++++++++++ .../Stormblood/Quest/BloodOnTheDeck.cs | 42 +++ .../Modules/Stormblood/Quest/DragonSound.cs | 72 +++++ .../Stormblood/Quest/EmissaryOfTheDawn.cs | 38 +++ .../Stormblood/Quest/HisForgottenHome.cs | 70 +++++ .../Stormblood/Quest/HopeOnTheWaves.cs | 80 ++++++ BossMod/Modules/Stormblood/Quest/Naadam.cs | 142 ++++++++++ .../Stormblood/Quest/OurUnsungHeroes.cs | 57 ++++ .../Stormblood/Quest/RaisingTheSword.cs | 75 +++++ .../Stormblood/Quest/ReturnOfTheBull.cs | 104 +++++++ .../Modules/Stormblood/Quest/RhalgrsBeacon.cs | 123 ++++++++ .../Stormblood/Quest/TheBattleOnBekko.cs | 76 +++++ .../Stormblood/Quest/TheFaceOfTrueEvil.cs | 74 +++++ .../Stormblood/Quest/TheMeasureOfHisReach.cs | 47 ++++ .../Quest/TheOrphansAndTheBrokenBlade.cs | 57 ++++ .../Stormblood/Quest/ThePowerToProtect.cs | 77 +++++ .../Modules/Stormblood/Quest/TheResonant.cs | 86 ++++++ .../Quest/TheTimeBetweenTheSeconds.cs | 90 ++++++ .../Stormblood/Quest/TheWillOfTheMoon.cs | 169 +++++++++++ .../Stormblood/Quest/TortoiseInTime.cs | 119 ++++++++ 72 files changed, 5875 insertions(+), 8 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Quest/SomewhereOnlySheKnows.cs create mode 100644 BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs create mode 100644 BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs create mode 100644 BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs create mode 100644 BossMod/Modules/Endwalker/Quest/SagesFocus.cs create mode 100644 BossMod/Modules/Endwalker/Quest/TheKillingArt.cs create mode 100644 BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs create mode 100644 BossMod/Modules/Heavensward/Quest/AtTheEndOfOurHope.cs create mode 100644 BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs create mode 100644 BossMod/Modules/Heavensward/Quest/DivineIntervention.cs create mode 100644 BossMod/Modules/Heavensward/Quest/DragoonsFate.cs create mode 100644 BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs create mode 100644 BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs create mode 100644 BossMod/Modules/RealmReborn/Quest/OperationArchon.cs create mode 100644 BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs create mode 100644 BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs create mode 100644 BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs create mode 100644 BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs create mode 100644 BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs create mode 100644 BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs create mode 100644 BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs create mode 100644 BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs create mode 100644 BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs create mode 100644 BossMod/Modules/Stormblood/Quest/DragonSound.cs create mode 100644 BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs create mode 100644 BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs create mode 100644 BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs create mode 100644 BossMod/Modules/Stormblood/Quest/Naadam.cs create mode 100644 BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs create mode 100644 BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs create mode 100644 BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs create mode 100644 BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs create mode 100644 BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs create mode 100644 BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs create mode 100644 BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs create mode 100644 BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs create mode 100644 BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs create mode 100644 BossMod/Modules/Stormblood/Quest/TheResonant.cs create mode 100644 BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs create mode 100644 BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs create mode 100644 BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs diff --git a/BossMod/Modules/Dawntrail/Quest/SomewhereOnlySheKnows.cs b/BossMod/Modules/Dawntrail/Quest/SomewhereOnlySheKnows.cs new file mode 100644 index 0000000000..3de3f4e56b --- /dev/null +++ b/BossMod/Modules/Dawntrail/Quest/SomewhereOnlySheKnows.cs @@ -0,0 +1,230 @@ +/* +namespace BossMod.Dawntrail.Quest.SomewhereOnlySheKnows; + +public enum OID : uint +{ + _Gen_SonOfTheKingdom = 0x4295, // R0.750, x? + _Gen_SonOfTheKingdom1 = 0x4294, // R0.750, x? + _Gen_TheWingedSteed = 0x4293, // R1.300, x? + _Gen_TheBirdOfPrey = 0x4297, // R1.960, x? + _Gen_FlightOfTheGriffin = 0x4296, // R9.200, x? + Boss = 0x4298, // R4.000, x0 (spawn during fight) + Helper = 0x233C, // R0.500, x0 (spawn during fight), Helper type + _Gen_AFlowerInTheSun = 0x4299, // R2.720, x0 (spawn during fight) +} + +public enum AID : uint +{ + _AutoAttack_Attack = 6498, // 4295->player, no cast, single-target + _AutoAttack_Attack1 = 6497, // 4294/4297/4296->player, no cast, single-target + _AutoAttack_Attack2 = 6499, // 4293->player, no cast, single-target + _Weaponskill_BurningBright = 37517, // 4293->self, 3.0s cast, range 47 width 6 rect + _Weaponskill_SwoopingFrenzy = 37519, // 4296->location, 4.0s cast, range 12 circle + _Weaponskill_Feathercut = 37522, // 4297->self, 3.0s cast, range 10 width 5 rect + _Weaponskill_FrigidPulse = 37520, // 4296->self, 5.0s cast, range 4-60 donut + _Weaponskill_EyeOfTheFierce = 37523, // 4297->self, 5.0s cast, range 40 circle + _Weaponskill_FervidPulse = 37521, // 4296->self, 5.0s cast, range 50 width 14 cross + _AutoAttack_ = 37542, // 4298->player, no cast, single-target + _Weaponskill_FlowerMotif = 37524, // 4298->self, 5.0s cast, single-target + _Weaponskill_BloodyCaress = 37527, // 4299->self, 5.0s cast, range 60 180-degree cone + _Weaponskill_ = 37541, // 4298->location, no cast, single-target + _Weaponskill_FloodInBlue = 37535, // 233C->self, 5.0s cast, range 50 width 10 rect + _Weaponskill_FloodInBlue1 = 37534, // 4298->self, 5.0s cast, single-target + _Weaponskill_FloodInBlue2 = 37536, // 233C->self, no cast, range 50 width 5 rect + _Weaponskill_BlazeInRed = 37539, // Boss->location, 6.0s cast, range 40 circle + _Weaponskill_ArborMotif = 37525, // Boss->self, 5.0s cast, single-target + _Weaponskill_TornadoInGreen = 37538, // Boss->self, 5.0s cast, range -40 donut + _Weaponskill_NineIvies = 37528, // 429A->self, 3.0s cast, single-target + _Weaponskill_NineIvies1 = 37529, // Helper->self, 3.0s cast, range 50 20-degree cone + _Weaponskill_1 = 39744, // 429A->self, no cast, single-target + _Weaponskill_SculptureCast = 37537, // Boss->self, 5.0s cast, range 45 circle + _Weaponskill_MountainMotif = 37526, // Boss->self, 5.0s cast, single-target + _Weaponskill_Earthquake = 37531, // Helper->self, 5.0s cast, range 10 circle + _Weaponskill_Earthquake1 = 37530, // 429B->self, 5.0s cast, single-target + _Weaponskill_FreezeInCyan = 37540, // Boss->self, 5.0s cast, range 40 45-degree cone + _Weaponskill_Earthquake2 = 37532, // Helper->self, 7.0s cast, range 10-20 donut + _Weaponskill_Earthquake3 = 37533, // Helper->self, 9.0s cast, range 20-30 donut +} + +class BurningBright(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_BurningBright), new AOEShapeRect(47, 3)); +class SwoopingFrenzy(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_SwoopingFrenzy), 12); +class Feathercut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Feathercut), new AOEShapeRect(10, 2.5f)); +class FrigidPulse(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_FrigidPulse), new AOEShapeDonut(11.9f, 60)); +class FervidPulse(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_FervidPulse), new AOEShapeCross(50, 7)); +class EyeOfTheFierce(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID._Weaponskill_EyeOfTheFierce)); +class BloodyCaress(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_BloodyCaress), new AOEShapeCone(60, 90.Degrees())) +{ + private DateTime? Predicted; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (ActiveCasters.Any()) + { + foreach (var e in base.ActiveAOEs(slot, actor)) + yield return e; + } + else if (Module.Enemies(OID._Gen_AFlowerInTheSun).FirstOrDefault() is Actor flower && Predicted is DateTime dt) + yield return new AOEInstance(Shape, flower.Position, flower.Rotation, dt); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + base.OnCastStarted(caster, spell); + if (spell.Action == WatchedAction) + Predicted = null; + } + + public override void OnActorCreated(Actor actor) + { + base.OnActorCreated(actor); + if ((OID)actor.OID == OID._Gen_AFlowerInTheSun) + Predicted = WorldState.FutureTime(10); + } +} +class Flood(BossModule module) : Components.Exaflare(module, new AOEShapeRect(50, 2.5f, 50)) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_FloodInBlue) + { + Lines.Add(new Line() + { + Next = caster.Position + new WDir(-2.5f, 0), + Advance = new(-5, 0), + Rotation = default, + NextExplosion = Module.CastFinishAt(spell), + TimeToMove = 2, + ExplosionsLeft = 5, + MaxShownExplosions = 1 + }); + Lines.Add(new Line() + { + Next = caster.Position + new WDir(2.5f, 0), + Advance = new(5, 0), + Rotation = default, + NextExplosion = Module.CastFinishAt(spell), + TimeToMove = 2, + ExplosionsLeft = 5, + MaxShownExplosions = 1 + }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_FloodInBlue) + { + AdvanceLine(Lines[0], caster.Position + new WDir(-2.5f, 0)); + AdvanceLine(Lines[1], caster.Position + new WDir(2.5f, 0)); + } + + if ((AID)spell.Action.ID == AID._Weaponskill_FloodInBlue2) + { + var rectCenter = caster.Position + caster.Rotation.ToDirection().OrthoR() * 2.5f; + if (Lines.FirstOrDefault(l => l.Next.AlmostEqual(rectCenter, 0.1f)) is Line l) + { + AdvanceLine(l, rectCenter); + if (l.ExplosionsLeft == 0) + Lines.Remove(l); + } + } + } +} + +class P1Bounds(BossModule module) : BossComponent(module) +{ + public override void Update() + { + Arena.Center = Raid.Player()?.Position ?? Arena.Center; + } +} + +class BlazeInRed(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_BlazeInRed)); +class TornadoInGreen(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_TornadoInGreen), new AOEShapeDonut(12, 40)); +class NineIvies(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_NineIvies1), new AOEShapeCone(50, 10.Degrees()), 9); +class SculptureCast(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID._Weaponskill_SculptureCast)); +class Earthquake(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeCircle(10), new AOEShapeDonut(10, 20), new AOEShapeDonut(20, 30)]) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_Earthquake) + AddSequence(caster.Position, Module.CastFinishAt(spell)); + } + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var idx = (AID)spell.Action.ID switch + { + AID._Weaponskill_Earthquake => 0, + AID._Weaponskill_Earthquake2 => 1, + AID._Weaponskill_Earthquake3 => 2, + _ => -1 + }; + AdvanceSequence(idx, caster.Position, WorldState.FutureTime(2)); + } +} +class Freeze(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_FreezeInCyan), new AOEShapeCone(40, 22.5f.Degrees())); + +public class QuestStates : StateMachineBuilder +{ + public QuestStates(BossModule module) : base(module) + { + bool DutyEnd() => module.WorldState.CurrentCFCID != 966; + bool P1End() => module.Enemies(OID._Gen_FlightOfTheGriffin).Any(x => x.IsTargetable) || P2End(); + bool P2End() => module.Enemies(OID.Boss).Any(x => x.IsTargetable) || DutyEnd(); + + TrivialPhase() + .ActivateOnEnter() + .OnEnter(() => + { + Module.Arena.Center = new(54, -219); + Module.Arena.Bounds = new ArenaBoundsRect(26, 9); + }) + .Raw.Update = P1End; + TrivialPhase(1) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .OnEnter(() => + { + Module.Arena.Center = new(0, -250); + Module.Arena.Bounds = new ArenaBoundsRect(20, 40); + }) + .Raw.Update = P2End; + TrivialPhase(2) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .OnEnter(() => + { + Module.Arena.Center = new(0, -340); + Module.Arena.Bounds = new ArenaBoundsSquare(25); + }) + .Raw.Update = DutyEnd; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 966, PrimaryActorOID = BossModuleInfo.PrimaryActorNone)] +public class Quest(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(20)) +{ + protected override bool CheckPull() => true; + + protected override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + Arena.Actors(WorldState.Actors.Where(x => x.IsAlly), ArenaColor.PlayerGeneric); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = 0; + } +} +*/ diff --git a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs new file mode 100644 index 0000000000..51163d5057 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs @@ -0,0 +1,59 @@ +namespace BossMod.Endwalker.Quest.LifeEphemeralPathEternal; + +class ElectrogeneticForce(BossModule module) : Components.CastTowers(module, ActionID.MakeSpell(AID.ElectrogeneticForce), 6); +class RawRockbreaker(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeCircle(10), new AOEShapeDonut(10, 20)]) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.RawRockbreaker) + AddSequence(caster.Position, Module.CastFinishAt(spell)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var idx = (AID)spell.Action.ID switch + { + AID.RawRockbreaker1 => 0, + AID.RawRockbreaker2 => 1, + _ => -1 + }; + AdvanceSequence(idx, caster.Position, WorldState.FutureTime(2)); + } + + public override void Update() + { + if (!Module.PrimaryActor.IsTargetable) + Sequences.Clear(); + } +} +class ChiBlast(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ChiBlast1)); +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCircle(6)); +class ArmOfTheScholar(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArmOfTheScholar), new AOEShapeCircle(5)); + +class ClassicalFire(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ClassicalFire), 6); +class ClassicalThunder(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.ClassicalThunder), 6); +class ClassicalBlizzard(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ClassicalBlizzard), 6); +class ClassicalStone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ClassicalStone), new AOEShapeCircle(15)); + +class AncelRockfistStates : StateMachineBuilder +{ + public AncelRockfistStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69608, NameID = 10732)] +public class AncelRockfist(WorldState ws, Actor primary) : BossModule(ws, primary, new(224.8f, -855.8f), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs new file mode 100644 index 0000000000..d9879ffa03 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs @@ -0,0 +1,74 @@ +namespace BossMod.Endwalker.Quest.LifeEphemeralPathEternal; + +public enum OID : uint +{ + Boss = 0x35C5, + BossP2 = 0x35C6, + Helper = 0x233C, + MahaudFlamehand = 0x35C4, // R0.500, x1 + Lalah = 0x35C2, + Loifa = 0x35C3, + Mahaud = 0x361C, + Ancel = 0x361D, + EnhancedNoulith = 0x3859, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + ChiBlast = 26838, // Boss->self, 5.0s cast, single-target + ChiBlast1 = 26839, // Helper->self, 5.0s cast, range 100 circle + ChiBomb = 26835, // Boss->self, 5.0s cast, single-target + Explosion = 26837, // 35C7->self, 5.0s cast, range 6 circle + ArmOfTheScholar = 26836, // Boss->self, 5.0s cast, range 5 circle + RawRockbreaker = 26832, // Boss->self, 5.0s cast, single-target + RawRockbreaker1 = 26833, // Helper->self, 4.0s cast, range 10 circle + RawRockbreaker2 = 26834, // Helper->self, 4.0s cast, range 10-20 donut + DemifireII = 26842, // MahaudFlamehand->Lalah, 8.0s cast, single-target + Demiburst = 26843, // MahaudFlamehand->self, 7.0s cast, single-target + ElectrogeneticForce = 26844, // Helper->self, 8.0s cast, range 6 circle + ElectrogeneticBlast = 26845, // Helper->self, 1.0s cast, range 80 circle + DemifireIII = 26841, // MahaudFlamehand->Lalah, 3.0s cast, single-target + FourElements = 26846, // MahaudFlamehand->self, 8.0s cast, single-target + ClassicalFire = 26847, // Helper->Lalah, 8.0s cast, range 6 circle + ClassicalThunder = 26848, // Helper->player/Loifa/Lalah, 5.0s cast, range 6 circle + ClassicalBlizzard = 26849, // Helper->location, 5.0s cast, range 6 circle + ClassicalStone = 26850, // Helper->self, 9.0s cast, range 50 circle + + Nouliths = 26851, // BossP2->self, 5.0s cast, single-target + AetherstreamTank = 26852, // 35C8->Lalah, no cast, range 50 width 4 rect + AetherstreamPlayer = 26853, // 35C8->players/Loifa, no cast, range 50 width 4 rect + Tracheostomy = 26854, // BossP2->self, 5.0s cast, range 10-20 donut + RightScalpel = 26855, // BossP2->self, 5.0s cast, range 15 210-degree cone + LeftScalpel = 26856, // BossP2->self, 5.0s cast, range 15 210-degree cone + Laparotomy = 26857, // BossP2->self, 5.0s cast, range 15 120-degree cone + Amputation = 26858, // BossP2->self, 7.0s cast, range 20 120-degree cone + Hypothermia = 26861, // BossP2->self, 5.0s cast, range 50 circle + Cryonics = 26860, // Helper->player, 8.0s cast, range 6 circle + Cryonics1 = 26859, // BossP2->self, 8.0s cast, single-target + Craniotomy = 28386, // BossP2->self, 8.0s cast, range 40 circle + RightLeftScalpel = 26862, // BossP2->self, 7.0s cast, range 15 210-degree cone + RightLeftScalpel1 = 26863, // BossP2->self, 3.0s cast, range 15 210-degree cone + LeftRightScalpel = 26864, // BossP2->self, 7.0s cast, range 15 210-degree cone + LeftRightScalpel1 = 26865, // BossP2->self, 3.0s cast, range 15 210-degree cone + Frigotherapy = 26866, // BossP2->self, 5.0s cast, single-target + Frigotherapy1 = 26867, // Helper->players/Mahaud/Loifa, 7.0s cast, range 5 circle +} + +public enum IconID : uint +{ + Tankbuster = 230, // Lalah + Noulith = 244, // player/Loifa +} + +public enum TetherID : uint +{ + Noulith = 17, // StrengthenedNoulith->Lalah/player/Loifa + Craniotomy = 174, // EnhancedNoulith->Lalah/Loifa/player/Mahaud/Ancel +} + +public enum SID : uint +{ + Craniotomy = 2968, // none->player/Lalah/Mahaud/Ancel/Loifa, extra=0x0 + DownForTheCount = 1953, // none->player/Lalah/Mahaud/Ancel/Loifa, extra=0xEC7 + +} diff --git a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs new file mode 100644 index 0000000000..5e12572506 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs @@ -0,0 +1,91 @@ +namespace BossMod.Endwalker.Quest.LifeEphemeralPathEternal; + +class AetherstreamTether(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeRect(50, 2), (uint)TetherID.Noulith) +{ + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.AetherstreamPlayer or AID.AetherstreamTank) + CurrentBaits.RemoveAll(x => x.Target.InstanceID == spell.MainTargetID); + } +} + +class Tracheostomy : Components.SelfTargetedAOEs +{ + public Tracheostomy(BossModule module) : base(module, ActionID.MakeSpell(AID.Tracheostomy), new AOEShapeDonut(10, 20)) + { + WorldState.Actors.EventStateChanged.Subscribe((act) => + { + if (act.OID == 0x1EA1A1 && act.EventState == 7) + Arena.Bounds = new ArenaBoundsCircle(20); + }); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (spell.Action == WatchedAction) + Arena.Bounds = new ArenaBoundsCircle(10); + } +} + +class RightScalpel(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightScalpel), new AOEShapeCone(15, 105.Degrees())); +class LeftScalpel(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LeftScalpel), new AOEShapeCone(15, 105.Degrees())); +class Laparotomy(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Laparotomy), new AOEShapeCone(15, 60.Degrees())); +class Amputation(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Amputation), new AOEShapeCone(20, 60.Degrees())); + +class Hypothermia(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Hypothermia)); +class Cryonics(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.Cryonics), 6); +class Craniotomy(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Craniotomy)); +class RightLeftScalpel1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightLeftScalpel), new AOEShapeCone(15, 105.Degrees())); +class RightLeftScalpel2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightLeftScalpel1), new AOEShapeCone(15, 105.Degrees())); +class LeftRightScalpel1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LeftRightScalpel), new AOEShapeCone(15, 105.Degrees())); +class LeftRightScalpel2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LeftRightScalpel1), new AOEShapeCone(15, 105.Degrees())); + +class EnhancedNoulith(BossModule module) : Components.Adds(module, (uint)OID.EnhancedNoulith) +{ + private readonly List<(Actor, Actor)> Tethers = []; + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.Craniotomy && WorldState.Actors.Find(tether.Target) is Actor target) + Tethers.Add((source, target)); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if (status.ID == (uint)SID.Craniotomy) + Tethers.RemoveAll(t => t.Item2 == actor); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + foreach (var t in Tethers) + Arena.AddLine(t.Item1.Position, t.Item2.Position, ArenaColor.Danger); + } +} +class Frigotherapy(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Frigotherapy1), 5); + +class GuildivainOfTheTaintedEdgeStates : StateMachineBuilder +{ + public GuildivainOfTheTaintedEdgeStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69608, NameID = 10733, PrimaryActorOID = (uint)OID.BossP2)] +public class GuildivainOfTheTaintedEdge(WorldState ws, Actor primary) : BossModule(ws, primary, new(224.8f, -855.8f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Endwalker/Quest/SagesFocus.cs b/BossMod/Modules/Endwalker/Quest/SagesFocus.cs new file mode 100644 index 0000000000..732e098c5a --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/SagesFocus.cs @@ -0,0 +1,67 @@ +namespace BossMod.Endwalker.Quest.SagesFocus; + +public enum OID : uint +{ + Boss = 0x3587, + Helper = 0x233C, + _Gen_ChiBomb = 0x358D, // R1.000, x0 (spawn during fight) + Mahaud = 0x3586, + Loifa = 0x3588, +} + +public enum AID : uint +{ + _AutoAttack_Attack = 872, // Boss->3589, no cast, single-target + TripleThreat = 26535, // Boss->3589, 8.0s cast, single-target + ChiBomb = 26536, // Boss->self, 5.0s cast, single-target + Explosion = 26537, // 358D->self, 5.0s cast, range 6 circle + ArmOfTheScholar = 26543, // Boss->self, 5.0s cast, range 5 circle + Nouliths = 26538, // 3588->self, 5.0s cast, single-target + Noubelea = 26541, // 3588->self, 5.0s cast, single-target + Noubelea1 = 26542, // 358E->self, 5.0s cast, range 50 width 4 rect + DemiblizzardIII = 26545, // 3586->self, 5.0s cast, single-target + DemiblizzardIII1 = 26546, // Helper->self, 5.0s cast, range -40 donut + Demigravity = 26539, // 3586->location, 5.0s cast, range 6 circle + Demigravity1 = 26550, // Helper->location, 5.0s cast, range 6 circle + DemifireIII = 26547, // 3586->self, 5.0s cast, single-target + DemifireIII1 = 26548, // Helper->self, 5.6s cast, range 40 circle + DemifireII = 26552, // Mahaud->self, 7.0s cast, single-target + DemifireII1 = 26553, // Helper->player/3589, 5.0s cast, range 5 circle + DemifireII2 = 26554, // Helper->location, 5.0s cast, range 14 circle +} + +class DemifireSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.DemifireII1), 5); +class DemifireII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.DemifireII2), 14); +class DemifireIII(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DemifireIII1)); +class Noubelea(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Noubelea1), new AOEShapeRect(50, 2)); +class Demigravity(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Demigravity), 6); +class Demigravity1(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Demigravity1), 6); +class Demiblizzard(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DemiblizzardIII1), new AOEShapeDonut(10, 40)); +class TripleThreat(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.TripleThreat)); +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCircle(6)); +class ArmOfTheScholar(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArmOfTheScholar), new AOEShapeCircle(5)); + +class AncelRockfistStates : StateMachineBuilder +{ + public AncelRockfistStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69604, NameID = 10732)] +public class AncelRockfist(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, -82.17f), new ArenaBoundsCircle(18.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Endwalker/Quest/TheKillingArt.cs b/BossMod/Modules/Endwalker/Quest/TheKillingArt.cs new file mode 100644 index 0000000000..7c93da35e1 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/TheKillingArt.cs @@ -0,0 +1,87 @@ +namespace BossMod.Endwalker.Quest.TheKillingArt; + +public enum OID : uint +{ + Boss = 0x3664, // R1.500, x1 + Helper = 0x233C, // R0.500, x10, Helper type + VoidHecteyes = 0x3666, // R1.200, x0 (spawn during fight) + VoidPersona = 0x3667, // R1.200, x0 (spawn during fight) + Voidzone = 0x1E963D +} + +public enum AID : uint +{ + MeatySlice = 27590, // Boss->self, 3.4+0.6s cast, single-target + MeatySlice1 = 27591, // Helper->self, 4.0s cast, range 50 width 12 rect + Cleaver = 27594, // Boss->self, 3.5+0.5s cast, single-target + Cleaver1 = 27595, // Helper->self, 4.0s cast, range 40 120-degree cone + FlankCleaver = 27596, // Boss->self, 3.5+0.5s cast, single-target + FlankCleaver1 = 27597, // Helper->self, 4.0s cast, range 40 120-degree cone + Explosion = 27606, // VoidHecteyes->self, 20.0s cast, range 60 circle + Explosion1 = 27607, // VoidPersona->self, 20.0s cast, range 50 circle + FocusInferi = 27592, // Boss->self, 2.9+0.6s cast, single-target + FocusInferi1 = 27593, // Helper->location, 3.5s cast, range 6 circle + CarnemLevare = 27598, // Boss->self, 4.0s cast, single-target + CarnemLevare1 = 27599, // Helper->self, 4.0s cast, range 40 width 8 cross + CarnemLevare2 = 27602, // Helper->self, 3.5s cast, range -17 donut + CarnemLevare3 = 27600, // Helper->self, 3.5s cast, range -7 donut + CarnemLevare4 = 27603, // Helper->self, 3.5s cast, range -22 donut + CarnemLevare5 = 27601, // Helper->self, 3.5s cast, range -12 donut + VoidMortar = 27604, // Boss->self, 4.0+1.0s cast, single-target + VoidMortar1 = 27605, // Helper->self, 5.0s cast, range 13 circle +} + +class VoidMortar(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VoidMortar1), new AOEShapeCircle(13)); +class FocusInferi(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 6, ActionID.MakeSpell(AID.FocusInferi1), m => m.Enemies(OID.Voidzone).Where(x => x.EventState != 7), 0); +class CarnemLevareCross(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CarnemLevare1), new AOEShapeCross(40, 4)); +class CarnemLevareDonut(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor, AOEShape)> Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Casters.Take(4).Select(c => new AOEInstance(c.Item2, c.Item1.Position, c.Item1.CastInfo!.Rotation, Module.CastFinishAt(c.Item1.CastInfo))); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + AOEShape? sh = (AID)spell.Action.ID switch + { + AID.CarnemLevare2 => new AOEShapeDonutSector(12, 17, 90.Degrees()), + AID.CarnemLevare3 => new AOEShapeDonutSector(2, 7, 90.Degrees()), + AID.CarnemLevare4 => new AOEShapeDonutSector(17, 22, 90.Degrees()), + AID.CarnemLevare5 => new AOEShapeDonutSector(7, 12, 90.Degrees()), + _ => null + }; + + if (sh != null) + Casters.Add((caster, sh)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.CarnemLevare2 or AID.CarnemLevare3 or AID.CarnemLevare4 or AID.CarnemLevare5) + Casters.RemoveAll(x => x.Item1 == caster); + } +} +class MeatySlice(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MeatySlice1), new AOEShapeRect(50, 6)); +class Cleaver(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Cleaver1), new AOEShapeCone(40, 60.Degrees())); +class FlankCleaver(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FlankCleaver1), new AOEShapeCone(40, 60.Degrees())); +class Adds(BossModule module) : Components.AddsMulti(module, [(uint)OID.VoidHecteyes, (uint)OID.VoidPersona], 1); + +class OrcusStates : StateMachineBuilder +{ + public OrcusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69614, NameID = 10581)] +public class Orcus(WorldState ws, Actor primary) : BossModule(ws, primary, new(-69.7f, -388.5f), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Endwalker/Quest/WorthyOfHisBack.cs b/BossMod/Modules/Endwalker/Quest/WorthyOfHisBack.cs index 4450c7cee8..17605c5654 100644 --- a/BossMod/Modules/Endwalker/Quest/WorthyOfHisBack.cs +++ b/BossMod/Modules/Endwalker/Quest/WorthyOfHisBack.cs @@ -1,4 +1,4 @@ -namespace BossMod.Endwalker.Quest.WorthyOfHisBack; +namespace BossMod.Endwalker.Quest.WorthyOfHisBack; public enum OID : uint { diff --git a/BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs b/BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs new file mode 100644 index 0000000000..3d1a98e541 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs @@ -0,0 +1,34 @@ +namespace BossMod.Heavensward.Quest.ASpectacleForTheAges; + +public enum OID : uint +{ + Boss = 0x154E, + Tizona = 0x1552 +} + +public enum AID : uint +{ + FlamingTizona = 5763, // D25->location, 3.0s cast, range 6 circle + TheCurse = 5765, // D25->self, 3.0s cast, range 7+R ?-degree cone +} + +class FlamingTizona(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FlamingTizona), 6); +class TheCurse(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TheCurse), new AOEShapeDonutSector(2, 7, 90.Degrees())); + +class Demoralize(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(0x1E9FA8).Where(e => e.EventState != 7)); +class Tizona(BossModule module) : Components.Adds(module, (uint)OID.Tizona, 5); + +class FlameGeneralAldynnStates : StateMachineBuilder +{ + public FlameGeneralAldynnStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67775, NameID = 4739)] +public class FlameGeneralAldynn(WorldState ws, Actor primary) : BossModule(ws, primary, new(-35.75f, -205.5f), new ArenaBoundsCircle(15)); diff --git a/BossMod/Modules/Heavensward/Quest/AtTheEndOfOurHope.cs b/BossMod/Modules/Heavensward/Quest/AtTheEndOfOurHope.cs new file mode 100644 index 0000000000..6b47c42ce4 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/AtTheEndOfOurHope.cs @@ -0,0 +1,18 @@ +using BossMod.QuestBattle; + +namespace BossMod.Heavensward.Quest; + +[ZoneModuleInfo(BossModuleInfo.Maturity.WIP, 416)] +public class AtTheEndOfOurHope(WorldState ws) : QuestBattle.QuestBattle(ws) +{ + public override List DefineObjectives(WorldState ws) => [ + new QuestObjective(ws).WithConnections( + // doorway + new Vector3(455.42f, 164.31f, -542.78f), + // basement + new Vector3(456.10f, 157.41f, -554.90f) + ) + .WithInteract(0x1E9B5A) + .PauseForCombat(false) + ]; +} diff --git a/BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs b/BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs new file mode 100644 index 0000000000..b8941f2050 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs @@ -0,0 +1,62 @@ +namespace BossMod.Heavensward.Quest.CloseEncountersOfTheVIthKind; + +public enum OID : uint +{ + Boss = 0xF1C, // R0.550, x? + Puddle = 0x1E88F5, // R0.500, x? + TerminusEst = 0xF5D, // R1.000, x? +} + +public enum AID : uint +{ + HandOfTheEmpire = 4000, // Boss->location, 2.0s cast, range 2 circle + TerminusEstBoss = 4005, // Boss->self, 3.0s cast, range 50 circle + TerminusEstAOE = 3825, // TerminusEst->self, no cast, range 40+R width 4 rect +} + +class RegulaVanHydrusStates : StateMachineBuilder +{ + public RegulaVanHydrusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +class HandOfTheEmpire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HandOfTheEmpire), 2); + +class Voidzone(BossModule module) : Components.PersistentVoidzone(module, 8, m => m.Enemies(OID.Puddle)); + +class TerminusEst(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.TerminusEstAOE)) +{ + private bool _active; + + private IEnumerable Adds => Module.Enemies(OID.TerminusEst).Where(x => !x.IsDead); + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Adds, ArenaColor.Danger, true); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + => _active ? Adds.Select(x => new AOEInstance(new AOEShapeRect(40, 2), x.Position, x.Rotation)) : []; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TerminusEstBoss) + _active = true; + } + + public override void OnActorDestroyed(Actor actor) + { + if ((OID)actor.OID == OID.TerminusEst) + _active = false; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67203, NameID = 3818)] +public class RegulaVanHydrus(WorldState ws, Actor primary) : BossModule(ws, primary, new(252.75f, 553), new ArenaBoundsCircle(19.5f)); + diff --git a/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs b/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs new file mode 100644 index 0000000000..c81f39bf27 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs @@ -0,0 +1,63 @@ +namespace BossMod.Heavensward.Quest.DivineIntervention; + +public enum OID : uint +{ + Boss = 0x1010, + Helper = 0x233C, + IshgardianSteelChain = 0x102C, // R1.000, x1 + SerPaulecrainColdfire = 0x1011, // R0.500, x1 + ThunderPicket = 0xEC4, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + LightningBolt = 3993, // EC4->E0F, 2.0s cast, width 4 rect charge + IronTempest = 1003, // Boss->self, 3.5s cast, range 5+R circle + Overpower = 720, // Boss->self, 2.5s cast, range 6+R 90-degree cone + RingOfFrost = 1316, // 1011->self, 3.0s cast, range 6+R circle + Rive = 1135, // Boss->self, 2.5s cast, range 30+R width 2 rect + Heartstopper = 866, // 1011->self, 2.5s cast, range 3+R width 3 rect +} + +class LightningBolt(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.LightningBolt), 2); +class IronTempest(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IronTempest), new AOEShapeCircle(5.5f)); +class Overpower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6.5f, 45.Degrees())); +class RingOfFrost(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RingOfFrost), new AOEShapeCircle(6.5f)); +class Rive(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Rive), new AOEShapeRect(30.5f, 1)); +class Heartstopper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Heartstopper), new AOEShapeRect(3.5f, 1.5f)); +class Chain(BossModule module) : Components.Adds(module, (uint)OID.IshgardianSteelChain, 1); + +class SerGrinnauxTheBullStates : StateMachineBuilder +{ + public SerGrinnauxTheBullStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.PrimaryActor.IsDeadOrDestroyed && module.Enemies(OID.SerPaulecrainColdfire).All(x => x.IsDeadOrDestroyed); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67133, NameID = 3850)] +public class SerGrinnauxTheBull(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 2), FunnyBounds) +{ + public static ArenaBoundsCustom NewBounds() + { + var arc = CurveApprox.CircleArc(new(3.6f, 0), 11.5f, 0.Degrees(), 180.Degrees(), 0.01f); + var arc2 = CurveApprox.CircleArc(new(-3.6f, 0), 11.5f, 180.Degrees(), 360.Degrees(), 0.01f); + + return new(16, new(arc.Concat(arc2).Select(a => a.ToWDir()))); + } + + public static readonly ArenaBoundsCustom FunnyBounds = NewBounds(); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Heavensward/Quest/DragoonsFate.cs b/BossMod/Modules/Heavensward/Quest/DragoonsFate.cs new file mode 100644 index 0000000000..6ffdd0b21f --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/DragoonsFate.cs @@ -0,0 +1,97 @@ +namespace BossMod.Heavensward.Quest.DragoonsFate; + +public enum OID : uint +{ + Boss = 0x10B9, // R7.000, x1 + Icicle = 0x10BC, // R2.500, x0 (spawn during fight) + Graoully = 0x10BA, // R7.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + PillarImpact = 3095, // 10BC->self, 3.0s cast, range 4+R circle + PillarPierce = 4259, // 10BC->self, 2.0s cast, range 80+R width 4 rect + Cauterize = 4260, // 10BA->self, 3.0s cast, range 48+R width 20 rect + SheetOfIce = 4261, // Boss->location, 2.5s cast, range 5 circle +} + +public enum SID : uint +{ + Prey = 904, // none->player/10BB, extra=0x0 + SlipperyPrey = 475, // none->player/10BB, extra=0x0 + ThinIce = 905, // Boss->player/10BB, extra=0x1/0x2/0x3 + DeepFreeze = 3479, // Boss->10BB/player, extra=0x1 +} + +class SheetOfIce(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SheetOfIce), 5); +class PillarImpact(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PillarImpact), new AOEShapeCircle(6.5f)); +class PillarPierce(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PillarPierce), new AOEShapeRect(82.5f, 2)); +class Cauterize(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Cauterize), new AOEShapeRect(55, 10)); + +class Prey(BossModule module) : BossComponent(module) +{ + private static readonly AOEShape Cleave = new AOEShapeCone(27, 65.Degrees()); + private int IceStacks(Actor actor) => actor.FindStatus(SID.ThinIce) is ActorStatus st ? st.Extra & 0xFF : 0; + + private Actor? PreyCur; + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if (status.ID == (uint)SID.Prey) + PreyCur = actor; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (PreyCur is not Actor prey) + return; + + var partner = WorldState.Party[slot == 0 ? PartyState.MaxAllianceSize : slot]!; + + // force debuff swap + if (IceStacks(prey) == 3) + hints.GoalZones.Add(p => p.InCircle(partner.Position, 2) ? 1 : 0); + else + { + // prevent premature swap, even though it doesn't really matter, because the debuff generally falls off with plenty of time left + hints.AddForbiddenZone(ShapeDistance.Circle(partner.Position, 5), WorldState.FutureTime(1)); + + if (Module.PrimaryActor.IsTargetable) + hints.AddForbiddenZone(Cleave.Distance(Module.PrimaryActor.Position, Module.PrimaryActor.AngleTo(partner)), WorldState.FutureTime(1)); + } + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + // sometimes partner loses prey status *after* we get it + if (status.ID == (uint)SID.Prey && actor == PreyCur) + PreyCur = null; + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (PreyCur is Actor p && Module.PrimaryActor is var primary && primary.IsTargetable) + Cleave.Outline(Arena, primary.Position, primary.AngleTo(p), ArenaColor.Danger); + } +} + +class GraoullyStates : StateMachineBuilder +{ + public GraoullyStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67231, NameID = 4190)] +public class Graoully(WorldState ws, Actor primary) : BossModule(ws, primary, BCenter, BBounds) +{ + public static readonly WPos BCenter = new(-515.285f, -304.69f); + private static readonly WPos[] Corners = [new(-483.91f, -299.22f), new(-519.70f, -272.85f), new(-546.66f, -309.50f), new(-510.38f, -336.53f)]; + public static readonly ArenaBoundsCustom BBounds = new(32, new(Corners.Select(c => c - BCenter))); +} diff --git a/BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs b/BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs new file mode 100644 index 0000000000..da8ec7d2bd --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs @@ -0,0 +1,111 @@ +namespace BossMod.Heavensward.Quest.FlyFreeMyPretty; + +public enum OID : uint +{ + Boss = 0x195E, + Helper = 0x233C, + GrynewahtP2 = 0x195F, // R0.500, x0 (spawn during fight) + ImperialColossus = 0x1966, // R3.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + AugmentedUprising = 7608, // Boss->self, 3.0s cast, range 8+R 120-degree cone + AugmentedSuffering = 7607, // Boss->self, 3.5s cast, range 6+R circle + Heartstopper = 866, // ImperialEques->self, 2.5s cast, range 3+R width 3 rect + Overpower = 720, // ImperialLaquearius->self, 2.1s cast, range 6+R 90-degree cone + GrandSword = 7615, // ImperialColossus->self, 3.0s cast, range 18+R 120-degree cone + MagitekRay = 7617, // ImperialColossus->location, 3.0s cast, range 6 circle + GrandStrike = 7616, // ImperialColossus->self, 2.5s cast, range 45+R width 4 rect + ShrapnelShell = 7614, // GrynewahtP2->location, 2.5s cast, range 6 circle + MagitekMissiles = 7612, // GrynewahtP2->location, 5.0s cast, range 15 circle + +} + +class MagitekMissiles(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekMissiles), 15); +class ShrapnelShell(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ShrapnelShell), 6); +class Firebomb(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(0x1E86DF).Where(e => e.EventState != 7)); + +class Uprising(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); +class Suffering(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), new AOEShapeCircle(6.5f)); +class Heartstopper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Heartstopper), new AOEShapeRect(3.5f, 1.5f)); +class Overpower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6, 45.Degrees())); +class GrandSword(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GrandSword), new AOEShapeCone(21, 60.Degrees())); +class MagitekRay(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRay), 6); +class GrandStrike(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GrandStrike), new AOEShapeRect(48, 2)); + +class Adds(BossModule module) : Components.AddsMulti(module, [0x1960, 0x1961, 0x1962, 0x1963, 0x1964, 0x1965, 0x1966]) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = (OID)e.Actor.OID == OID.ImperialColossus ? 5 : e.Actor.TargetID == actor.InstanceID ? 1 : 0; + } +} + +class Bounds(BossModule module) : BossComponent(module) +{ + public override void OnEventDirectorUpdate(uint updateID, uint param1, uint param2, uint param3, uint param4) + { + if (updateID == 0x10000002) + Arena.Bounds = new ArenaBoundsCircle(20); + } +} + +class ReaperAI(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.MountId == 103 && WorldState.Actors.Find(actor.TargetID) is var target && target != null) + { + if ((OID)target.OID == OID.ImperialColossus) + hints.ActionsToExecute.Push(ActionID.MakeSpell(Roleplay.AID.DiffractiveMagitekCannon), target, ActionQueue.Priority.High, targetPos: target.PosRot.XYZ()); + hints.ActionsToExecute.Push(ActionID.MakeSpell(Roleplay.AID.MagitekCannon), target, ActionQueue.Priority.High, targetPos: target.PosRot.XYZ()); + + hints.GoalZones.Add(hints.GoalSingleTarget(target, 25)); + } + } +} + +class GrynewahtStates : StateMachineBuilder +{ + public GrynewahtStates(BossModule module) : base(module) + { + State build(uint id) => SimpleState(id, 10000, "Enrage") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + + SimplePhase(1, id => build(id).ActivateOnEnter(), "P1") + .Raw.Update = () => Module.Enemies(OID.GrynewahtP2).Any(); + DeathPhase(0x100, id => build(id).ActivateOnEnter().OnEnter(() => + { + Module.Arena.Bounds = new ArenaBoundsCircle(20); + })); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67894, NameID = 5576)] +public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), HexBounds) +{ + public static readonly ArenaBoundsCustom HexBounds = BuildHexBounds(); + + private static ArenaBoundsCustom BuildHexBounds() + { + var hexSideLen = 20 / MathF.Sqrt(3); + + // slight adjustment to account for player hitbox radius, otherwise dodges can get very sketchy + hexSideLen -= 1.5f; + + List verts = [new(hexSideLen, 0), hexSideLen * 30.Degrees().ToDirection(), -hexSideLen * 150.Degrees().ToDirection(), new(-hexSideLen, 0), hexSideLen * -30.Degrees().ToDirection(), hexSideLen * 150.Degrees().ToDirection()]; + return new(hexSideLen, new(verts)); + } +} diff --git a/BossMod/Modules/Heavensward/Quest/OneLifeOneWorld.cs b/BossMod/Modules/Heavensward/Quest/OneLifeOneWorld.cs index e75f5c25bb..71cb97b23b 100644 --- a/BossMod/Modules/Heavensward/Quest/OneLifeOneWorld.cs +++ b/BossMod/Modules/Heavensward/Quest/OneLifeOneWorld.cs @@ -13,14 +13,14 @@ public enum AID : uint UnlitCyclone = 6684, // Boss->self, 4.0s cast, range 5+R circle UnlitCycloneAdds = 6685, // 18D6->location, 4.0s cast, range 9 circle Skydrive = 6686, // Boss->player, 5.0s cast, single-target - UtterDestruction = 6690, // _Gen_FirstWard->self, 3.0s cast, range 20+R circle + UtterDestruction = 6690, // FirstWard->self, 3.0s cast, range 20+R circle RollingBladeCircle = 6691, // Boss->self, 3.0s cast, range 7 circle - RollingBladeCone = 6692, // _Gen_FirstWard->self, 3.0s cast, range 60+R 30-degree cone + RollingBladeCone = 6692, // FirstWard->self, 3.0s cast, range 60+R 30-degree cone } public enum SID : uint { - Invincibility = 325, // _Gen_KnightOfDarkness->Boss/_Gen_FirstWard, extra=0x0 + Invincibility = 325, // KnightOfDarkness->Boss/FirstWard, extra=0x0 } class Overpower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(7, 45.Degrees())); @@ -47,7 +47,7 @@ class Adds(BossModule module) : Components.AddsMulti(module, [0x17CE, 0x17CF, 0x class TargetPriorityHandler(BossModule module) : BossComponent(module) { private Actor? Knight => Module.Enemies(OID.KnightOfDarkness).FirstOrDefault(); - private Actor? Covered => WorldState.Actors.FirstOrDefault(s => s.FindStatus(SID.Invincibility) != null); + private Actor? Covered => WorldState.Actors.FirstOrDefault(s => s.OID != 0x18D6 && s.FindStatus(SID.Invincibility) != null); private Actor? BladeOfLight => WorldState.Actors.FirstOrDefault(s => (OID)s.OID == OID.BladeOfLight && s.IsTargetable); public override void DrawArenaBackground(int pcSlot, Actor pc) @@ -71,7 +71,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } else { - e.Priority = -1; + e.Priority = AIHints.Enemy.PriorityUndesirable; } } @@ -120,5 +120,5 @@ public WarriorOfDarknessStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 194, NameID = 5240)] +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67885, NameID = 5240)] public class WarriorOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs b/BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs new file mode 100644 index 0000000000..e9f3b69463 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs @@ -0,0 +1,50 @@ +namespace BossMod.Heavensward.Quest.TheFateOfStars; + +public enum OID : uint +{ + Boss = 0x161E, + Helper = 0x233C, + MagitekTurretI = 0x161F, // R0.600, x0 (spawn during fight) + MagitekTurretII = 0x1620, // R0.600, x0 (spawn during fight) + TerminusEst = 0x1621, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + MagitekSlug = 6026, // Boss->self, 2.5s cast, range 60+R width 4 rect + AetherochemicalGrenado = 6031, // 1620->location, 3.0s cast, range 8 circle + SelfDetonate = 6032, // 161F/1620->self, 5.0s cast, range 40+R circle + MagitekSpread = 6027, // Boss->self, 3.0s cast, range 20+R 240-degree cone +} + +class MagitekSlug(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekSlug), new AOEShapeRect(60, 2)); +class AetherochemicalGrenado(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AetherochemicalGrenado), 8); +class SelfDetonate(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SelfDetonate), "Kill turret before detonation!", true) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PriorityTargets) + if (h.Actor.CastInfo?.Action == WatchedAction) + h.Priority = 5; + } +} +class MagitekSpread(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekSpread), new AOEShapeCone(20.55f, 120.Degrees())); + +class RegulaVanHydrusStates : StateMachineBuilder +{ + public RegulaVanHydrusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67824, NameID = 3818)] +public class RegulaVanHydrus(WorldState ws, Actor primary) : BossModule(ws, primary, new(230, 79), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/RealmReborn/Quest/OperationArchon.cs b/BossMod/Modules/RealmReborn/Quest/OperationArchon.cs new file mode 100644 index 0000000000..2b69db0e91 --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/OperationArchon.cs @@ -0,0 +1,66 @@ +namespace BossMod.RealmReborn.Quest.OperationArchon; + +public enum OID : uint +{ + Boss = 0x38F5, // R1.500, x? + Helper = 0x233C, // R0.500, x?, Helper type + ImperialPilusPrior = 0x38F7, // R1.500, x0 (spawn during fight) + ImperialCenturion = 0x38F6, // R1.500, x0 (spawn during fight) +} + +public enum SID : uint +{ + DirectionalParry = 680 +} + +public enum AID : uint +{ + TartareanShockwave = 28871, // 38F5->self, 3.0s cast, range 7 circle + GalesOfTartarus = 28870, // 38F5->self, 3.0s cast, range 30 width 5 rect + MagitekMissiles = 28865, // 233C->location, 4.0s cast, range 7 circle + TartareanTomb = 28869, // 233C->self, 8.0s cast, range 11 circle + DrillShot = 28874, // Boss->self, 3.0s cast, range 30 width 5 rect + TartareanShockwave1 = 28877, // Boss->self, 6.0s cast, range 14 circle + GalesOfTartarus1 = 28876, // Boss->self, 6.0s cast, range 30 width 30 rect +} + +class Adds(BossModule module) : Components.Adds(module, (uint)OID.ImperialCenturion); +class Adds1(BossModule module) : Components.Adds(module, (uint)OID.ImperialPilusPrior); + +class MagitekMissiles(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekMissiles), 7); +class DrillShot(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DrillShot), new AOEShapeRect(30, 2.5f)); +class TartareanShockwave(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TartareanShockwave), new AOEShapeCircle(7)); +class BigTartareanShockwave(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TartareanShockwave1), new AOEShapeCircle(14)); +class GalesOfTartarus(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GalesOfTartarus), new AOEShapeRect(30, 2.5f)); +class BigGalesOfTartarus(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GalesOfTartarus1), new AOEShapeRect(30, 15)); +class DirectionalParry(BossModule module) : Components.DirectionalParry(module, (uint)OID.Boss) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Module.PrimaryActor.FindStatus(SID.DirectionalParry) != null) + hints.AddForbiddenZone(new AOEShapeCone(100, 45.Degrees()), Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, WorldState.FutureTime(10)); + } +} +class TartareanTomb(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TartareanTomb), new AOEShapeCircle(11)); + +class RhitahtynSasArvinaStates : StateMachineBuilder +{ + public RhitahtynSasArvinaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70057, NameID = 2160)] +public class RhitahtynSasArvina(WorldState ws, Actor primary) : BossModule(ws, primary, new(-689, -815), new ArenaBoundsCircle(14.5f)); diff --git a/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs b/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs new file mode 100644 index 0000000000..98729f1aad --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs @@ -0,0 +1,263 @@ +namespace BossMod.RealmReborn.Quest.TheStepsOfFaith; + +public enum OID : uint +{ + Boss = 0x3A5F, // R30.000, x1 +} + +public enum AID : uint +{ + FlameBreathCast = 30185, // Vishap->self, 5.0s cast, range 1 width 2 rect + FlameBreathChannel = 30884, // Vishap->self, no cast, range 40 width 20 rect + Cauterize = 30878, // Boss->self, 30.5+4.5s cast, single-target + Touchdown = 26408, // Vishap->self, 6.0s cast, range 80 circle + Fireball = 30875, // Vishap->players/3A71/3A6F/3A6C/3A69/3A68/3A62/3A61/3A60/3A72/3A70/3A6B/3A6A/3A64/3A63, 6.0s cast, range 6 circle + BodySlam = 26401, // Vishap->self, 6.0s cast, range 80 width 44 rect + Flamisphere = 30883, // Vishap->location, 8.0s cast, range 10 circle + FlameBreath2Cast = 26411, // Boss->self, 3.8+1.2s cast, range 60 width 20 rect + RipperClaw = 31262, // 3ABD->self, 3.7s cast, range 9 ?-degree cone + EarthshakerAOE = 30880, // Boss->self, 4.5s cast, range 31 circle + Earthshaker = 30887, // Vishap->self, 6.5s cast, range 80 30-degree cone + EarthrisingAOE = 26410, // Boss->self, 4.5s cast, range 31 circle + EarthrisingCast = 30888, // Vishap->self, 7.0s cast, range 8 circle + EarthrisingRepeat = 26412, // Vishap->self, no cast, range 8 circle + SidewiseSlice = 30879, // Boss->self, 8.0s cast, range 50 120-degree cone + ScorchingBreath = 29785, // Boss->self, 15.0+5.0s cast, single-target + +} + +class RipperClaw(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RipperClaw), new AOEShapeCone(9, 45.Degrees())); + +class EarthShakerAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EarthshakerAOE), new AOEShapeCircle(31)); +class Earthshaker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Earthshaker), new AOEShapeCone(80, 15.Degrees()), maxCasts: 2); + +class EarthrisingAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EarthrisingAOE), new AOEShapeCircle(31)); +class Earthrising(BossModule module) : Components.Exaflare(module, 8) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.EarthrisingCast) + { + Lines.Add(new() { Next = caster.Position, Advance = new(0, -7.5f), NextExplosion = Module.CastFinishAt(spell), TimeToMove = 1, ExplosionsLeft = 5, MaxShownExplosions = 2 }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.EarthrisingRepeat or AID.EarthrisingCast) + { + foreach (var l in Lines.Where(l => l.Next.AlmostEqual(caster.Position, 1))) + AdvanceLine(l, caster.Position); + ++NumCasts; + } + } +} + +class SidewiseSlice(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SidewiseSlice), new AOEShapeCone(50, 60.Degrees())); + +class FireballSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Fireball), 6); + +class Flamisphere(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Flamisphere), new AOEShapeCircle(10)); + +class BodySlam(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.BodySlam), 20, kind: Kind.DirForward, stopAtWall: true); + +class FlameBreath(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.FlameBreathChannel)) +{ + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.FlameBreathCast) + _aoe = new(new AOEShapeRect(500, 10), Module.PrimaryActor.Position, 180.Degrees(), Module.CastFinishAt(spell).AddSeconds(1)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (NumCasts >= 35) + { + _aoe = null; + NumCasts = 0; + } + } +} + +class FlameBreath2(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.FlameBreathChannel)) +{ + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.FlameBreath2Cast) + { + NumCasts = 0; + + _aoe = new(new AOEShapeRect(60, 10), caster.Position, spell.Rotation, Module.CastFinishAt(spell)); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (NumCasts >= 14) + { + _aoe = null; + } + } +} + +class Cauterize(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.Cauterize)) +{ + private Actor? Source; + + private static readonly AOEShapeRect MoveIt = new(40, 22, 38); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Source == null) + yield break; + + if (Arena.Center.Z > 218) + yield return new AOEInstance(MoveIt, Arena.Center); + else + yield return new AOEInstance(new AOEShapeRect(160, 22), Source.Position, 180.Degrees(), Module.CastFinishAt(Source.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + Source = Module.PrimaryActor; + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + Source = null; + } +} + +class Touchdown(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.Touchdown), 10, stopAtWall: true); + +class ScorchingBreath(BossModule module) : Components.GenericAOEs(module) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.ScorchingBreath) + NumCasts++; + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (NumCasts > 0) + yield return new AOEInstance(new AOEShapeRect(100, 10, 100), Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, Module.CastFinishAt(Module.PrimaryActor.CastInfo)); + } +} + +class ScrollingBounds(BossModule module) : BossComponent(module) +{ + public const float HalfHeight = 40; + public const float HalfWidth = 22; + + public static readonly ArenaBoundsRect Bounds = new(HalfWidth, HalfHeight); + + private int Phase = 1; + private (float Min, float Max) ZBounds = (120, 300); + + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 3 && state == 0x20001) + { + ZBounds = (120, 200); + Phase = 2; + } + + if (index == 0 && state == 0x800040) + { + ZBounds = (-40, 200); + Phase = 3; + } + + if (index == 4 && state == 0x20001) + { + ZBounds = (-40, 40); + Phase = 4; + } + + if (index == 1 && state == 0x800040) + { + ZBounds = (-200, 40); + Phase = 5; + } + + if (index == 6 && state == 0x20001) + { + ZBounds = (-200, -120); + Phase = 6; + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // force player to walk south to aggro vishap (status 1268 = In Event, not actionable) + if (Phase == 1 && !actor.InCombat && actor.FindStatus(1268) == null) + hints.AddForbiddenZone(new AOEShapeRect(38, 22, 40), Arena.Center); + + // subsequent state transitions don't trigger until player moves into the area + if (Phase == 3 && actor.Position.Z > 25) + hints.AddForbiddenZone(new AOEShapeRect(40, 22, 38), Arena.Center); + + if (Phase == 5 && actor.Position.Z > -135) + hints.AddForbiddenZone(new AOEShapeRect(40, 22, 38), Arena.Center); + } + + public override void Update() + { + base.Update(); + if (WorldState.Party.Player() is not Actor p) + return; + + Arena.Center = new(0, Math.Clamp(p.Position.Z, ZBounds.Min + HalfHeight, ZBounds.Max - HalfHeight)); + } +} + +class VishapStates : StateMachineBuilder +{ + public VishapStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70127, NameID = 3330)] +public class TheStepsOfFaith(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 245), ScrollingBounds.Bounds) +{ + // vishap doesn't start targetable + protected override bool CheckPull() => PrimaryActor.InCombat; + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + Arena.Actor(PrimaryActor, ArenaColor.Enemy, true); + } +} + diff --git a/BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs b/BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs new file mode 100644 index 0000000000..ef6bf5515e --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs @@ -0,0 +1,140 @@ +namespace BossMod.RealmReborn.Quest.TheUltimateWeapon; + +public enum OID : uint +{ + Boss = 0x3933, // R1.750, x? + SeaOfPitch = 0x1EB738, // R0.500, x?, EventObj type + Firesphere = 0x3934, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + AncientFireIII = 29327, // Boss->self, 4.0s cast, range 40 circle + DarkThunder = 29329, // Lahabrea->self, 4.0s cast, range 1 circle + EndOfDays = 29331, // Boss->self, 4.0s cast, range 60 width 8 rect + EndOfDaysAdds = 29762, // PhantomLahabrea->self, 4.0s cast, range 60 width 8 rect + Nightburn = 29340, // Boss->player, 4.0s cast, single-target + FiresphereSummon = 29332, // Boss->self, 4.0s cast, single-target + Burst = 29333, // Firesphere->self, 3.0s cast, range 8 circle + AncientEruption = 29335, // Lahabrea->self, 4.0s cast, range 6 circle + FluidFlare = 29760, // Lahabrea->self, 4.0s cast, range 40 60-degree cone + AncientCross = 29756, // Lahabrea->self, 4.0s cast, range 6 circle + BurstFlare = 29758, // Lahabrea->self, 5.0s cast, range 60 circle + GripOfNight = 29337, // Boss->self, 6.0s cast, range 40 150-degree cone +} + +class BurstFlare(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.BurstFlare), 10) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + + // don't add any hints if Burst hasn't gone off yet, it tends to spook AI mode into running into deathwall + if (Module.Enemies(OID.Firesphere).Any(x => x.CastInfo?.RemainingTime > 0)) + return; + + foreach (var c in Casters) + hints.AddForbiddenZone(new AOEShapeDonut(5, 100), Arena.Center, default, Module.CastFinishAt(c.CastInfo)); + } +} + +class GripOfNight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GripOfNight), new AOEShapeCone(40, 75.Degrees())); + +class AncientCross(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AncientCross), new AOEShapeCircle(6), maxCasts: 8); + +class AncientEruption(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AncientEruption), new AOEShapeCircle(6)); + +class FluidFlare(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FluidFlare), new AOEShapeCone(40, 30.Degrees())); + +class FireSphere(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.Burst)) +{ + private DateTime? _predictedCast; + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.FiresphereSummon) + _predictedCast = WorldState.CurrentTime.AddSeconds(12); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Burst) + _predictedCast = Module.CastFinishAt(spell); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (_predictedCast is DateTime dt && dt > WorldState.CurrentTime) + foreach (var enemy in Module.Enemies(OID.Firesphere)) + yield return new AOEInstance(new AOEShapeCircle(8), enemy.Position, default, dt); + } +} + +class Nightburn(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Nightburn), "WoLbuster"); + +class AncientFire(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AncientFireIII), hint: "Raidwide + spawn deathwall"); + +class DeathWall(BossModule module) : BossComponent(module) +{ + private bool _active; + private bool _completed; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.AncientFireIII && !_completed) + _active = true; + } + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (_active) + new AOEShapeDonut(15, 100).Draw(Arena, Arena.Center, default, ArenaColor.AOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_active) + hints.AddForbiddenZone(new AOEShapeDonut(15, 100), Arena.Center); + } + + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 0 && state == 0x20001) + { + Module.Arena.Bounds = new ArenaBoundsCircle(15); + _completed = true; + _active = false; + } + } +} + +class DarkThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkThunder), new AOEShapeCircle(1)); + +class SeaOfPitch(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.SeaOfPitch).Where(x => x.EventState != 7)); + +class EndOfDays(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EndOfDays), new AOEShapeRect(60, 4)); +class EndOfDaysAdds(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EndOfDaysAdds), new AOEShapeRect(60, 4)); + +class LahabreaStates : StateMachineBuilder +{ + public LahabreaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70058, NameID = 2143)] +public class Lahabrea(WorldState ws, Actor primary) : BossModule(ws, primary, new(-704, 480), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs b/BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs new file mode 100644 index 0000000000..f605961e83 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs @@ -0,0 +1,90 @@ +namespace BossMod.Shadowbringers.Quest.AFeastOfLies; + +public enum OID : uint +{ + Boss = 0x295A, + Helper = 0x233C, +} + +public enum AID : uint +{ + UnceremoniousBeheading = 16274, // Boss->self, 4.0s cast, range 10 circle + KatunCycle = 16275, // Boss->self, 4.0s cast, range 5-40 donut + MercilessRight = 16278, // Boss->self, 4.0s cast, single-target + MercilessRight1 = 16283, // 29FB->self, 3.8s cast, range 40 120-degree cone + MercilessRight2 = 16284, // 29FE->self, 4.2s cast, range 40 120-degree cone + Evisceration = 16277, // Boss->self, 4.5s cast, range 40 120-degree cone + HotPursuit = 16291, // Boss->self, 2.5s cast, single-target + HotPursuit1 = 16285, // 29E6->location, 3.0s cast, range 5 circle + NexusOfThunder = 16280, // Boss->self, 2.5s cast, single-target + NexusOfThunder1 = 16276, // 29E6->self, 4.3s cast, range 45 width 5 rect + LivingFlame = 16294, // Boss->self, 3.0s cast, single-target + Spiritcall = 16292, // Boss->self, 3.0s cast, range 40 circle + Burn = 16290, // 29C2->self, 4.5s cast, range 8 circle + RisingThunder = 16293, // Boss->self, 3.0s cast, single-target + Electrocution = 16286, // 295B->self, 10.0s cast, range 6 circle + ShatteredSky = 17191, // Boss->self, 4.0s cast, single-target + ShatteredSky1 = 16282, // 29E6->self, 0.5s cast, range 40 circle + NexusOfThunder2 = 16296, // 29E6->self, 6.3s cast, range 45 width 5 rect + MercilessLeft = 16279, // Boss->self, 4.0s cast, single-target + MercilessLeft1 = 16298, // 29FC->self, 3.8s cast, range 40 120-degree cone + MercilessLeft2 = 16297, // 29FD->self, 4.2s cast, range 40 120-degree cone +} + +class UnceremoniousBeheading(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.UnceremoniousBeheading), new AOEShapeCircle(10)); +class KatunCycle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KatunCycle), new AOEShapeDonut(5, 40)); +class MercilessRight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessRight1), new AOEShapeCone(40, 60.Degrees())); +class MercilessRight1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessRight2), new AOEShapeCone(40, 60.Degrees())); +class MercilessLeft(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessLeft1), new AOEShapeCone(40, 60.Degrees())); +class MercilessLeft1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessLeft2), new AOEShapeCone(40, 60.Degrees())); +class Evisceration(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Evisceration), new AOEShapeCone(40, 60.Degrees())); +class HotPursuit(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); +class NexusOfThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder1), new AOEShapeRect(45, 2.5f)); +class NexusOfThunder1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder2), new AOEShapeRect(45, 2.5f)); +class Burn(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Burn), new AOEShapeCircle(8), maxCasts: 5); +class Spiritcall(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.Spiritcall), 20, stopAtWall: true); + +class Electrocution(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Electrocution), new AOEShapeCircle(6)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Casters.Count == 12) + { + var enemy = hints.PotentialTargets.Where(x => x.Actor.OID == 0x295B).MinBy(e => actor.DistanceToHitbox(e.Actor)); + foreach (var e in hints.PotentialTargets) + e.Priority = e == enemy ? 1 : 0; + } + else + { + base.AddAIHints(slot, actor, assignment, hints); + } + } +} + +class SerpentHead(BossModule module) : Components.Adds(module, 0x29E8, 1); + +class RanjitStates : StateMachineBuilder +{ + public RanjitStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69167, NameID = 8374)] +public class Ranjit(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 18), new ArenaBoundsCircle(15)); diff --git a/BossMod/Modules/Shadowbringers/Quest/ASleepDisturbed.cs b/BossMod/Modules/Shadowbringers/Quest/ASleepDisturbed.cs index ae1fbb5708..df8d7f7d88 100644 --- a/BossMod/Modules/Shadowbringers/Quest/ASleepDisturbed.cs +++ b/BossMod/Modules/Shadowbringers/Quest/ASleepDisturbed.cs @@ -49,6 +49,7 @@ class GraceOfCalamity(BossModule module) : Components.StackWithCastTargets(modul class SoundOfHeat(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TheSoundOfHeat), new AOEShapeCone(60, 30.Degrees())); class DeceitOfPain(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.TheDeceitOfPain), 14); class BalmOfDisgrace(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TheBalmOfDisgrace), new AOEShapeCircle(12)); + class ASleepDisturbedStates : StateMachineBuilder { public ASleepDisturbedStates(BossModule module) : base(module) @@ -66,4 +67,12 @@ public ASleepDisturbedStates(BossModule module) : base(module) } [ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "croizat", GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69301, NameID = 9296)] -public class ASleepDisturbed(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsSquare(20)); +public class ASleepDisturbed(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsSquare(20)) +{ + protected override bool CheckPull() => PrimaryActor.IsTargetable; + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + hints.PrioritizeTargetsByOID(OID.Boss, 0); + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs b/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs new file mode 100644 index 0000000000..f1d2eabaa1 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs @@ -0,0 +1,107 @@ +namespace BossMod.Shadowbringers.Quest.ATearfulReunion; + +public enum OID : uint +{ + Boss = 0x29C5, + _Gen_Phronesis = 0x29E7, // R0.500, x3 + _Gen_ = 0x2A1A, // R0.500, x0 (spawn during fight) + _Gen_1 = 0x2A1B, // R0.500, x0 (spawn during fight) + _Gen_2 = 0x2A1C, // R0.500, x0 (spawn during fight) + _Gen_3 = 0x2A19, // R0.500, x0 (spawn during fight) + _Gen_Hollow = 0x29C6, // R0.750-2.250, x0 (spawn during fight) + _Gen_4 = 0x2AC5, // R0.500, x0 (spawn during fight) + _Gen_5 = 0x2A1D, // R0.500, x0 (spawn during fight) + _Gen_LightningGlobe = 0x29C8, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + SanctifiedFireIII = 17036, // 29E7->location, 4.0s cast, range 6 circle + SanctifiedFlare = 17039, // Boss->players, 5.0s cast, range 6 circle + // spread from npc + SanctifiedFireIV1 = 17038, // _Gen_Phronesis->players/29C3, 4.0s cast, range 10 circle + // stack with npc + SanctifiedBlizzardII = 17044, // Boss->self, 3.0s cast, range 5 circle + SanctifiedBlizzardIII = 17045, // Boss->self, 4.0s cast, range 40+R 45-degree cone + SanctifiedBlizzardIV = 17047, // _Gen_Phronesis->self, 5.0s cast, range 5-20 donut +} + +class SanctifiedBlizzardIV(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardIV), new AOEShapeDonut(5, 20)); +class SanctifiedBlizzardII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardII), new AOEShapeCircle(5)); +class SanctifiedFireIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedFireIII), 6); +class SanctifiedBlizzardIII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardIII), new AOEShapeCone(40.5f, 22.5f.Degrees())); +class Hollow(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID._Gen_Hollow)); +class HollowTether(BossModule module) : Components.Chains(module, 1, chainLength: 5); +class SanctifiedFireIV(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SanctifiedFireIV1), 10); +class SanctifiedFlare(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.SanctifiedFlare), 6, 1) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + if (ActiveStacks.Any() && WorldState.Actors.First(x => x.OID == 0x29C3) is Actor cerigg) + { + hints.AddForbiddenZone(new AOEShapeDonut(6, 100), cerigg.Position, default, ActiveStacks.First().Activation); + } + } +} + +class LightningGlobe(BossModule module) : Components.GenericLineOfSightAOE(module, default, 100, false) +{ + private readonly List Balls = []; + private IEnumerable<(WPos Center, float Radius)> Hollows => Module.Enemies(OID._Gen_Hollow).Select(h => (h.Position, h.HitboxRadius)); + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == 6) + Balls.Add(source); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var b in Balls) + Arena.AddLine(pc.Position, b.Position, ArenaColor.Danger); + } + + public override void Update() + { + var player = Raid.Player(); + if (player == null) + return; + + Balls.RemoveAll(b => b.IsDead); + + var closestBall = Balls.OrderBy(player.DistanceToHitbox).FirstOrDefault(); + Modify(closestBall?.Position, Hollows); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Origin != null + && actor.Position.InCircle(Origin.Value, MaxRange) + && !Visibility.Any(v => !actor.Position.InCircle(Origin.Value, v.Distance) && actor.Position.InCone(Origin.Value, v.Dir, v.HalfWidth))) + { + hints.Add("Pull lightning orb into black hole!"); + } + } +} + +class PhronesisStates : StateMachineBuilder +{ + public PhronesisStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69164, NameID = 8931)] +public class Phronesis(WorldState ws, Actor primary) : BossModule(ws, primary, new(-256, -284), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs b/BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs new file mode 100644 index 0000000000..e6976f2ef1 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs @@ -0,0 +1,102 @@ +namespace BossMod.Shadowbringers.Quest.CourageBornOfFear; + +public enum OID : uint +{ + Boss = 0x29E1, // r=0.5 + Helper = 0x233C, + Andreia = 0x29E0, + Knight = 0x29E4, +} + +public enum AID : uint +{ + Overcome = 17088, // Boss->self, 3.0s cast, range 8+R 120-degree cone + SanctifiedFireII1 = 17188, // 29E3->29DF, no cast, range 5 circle + MythrilCyclone1 = 17087, // 29DD->self, 4.0s cast, range 50 circle + SanctifiedMeltdown = 17323, // 29DD->player/29DF, 5.0s cast, range 6 circle + MythrilCyclone2 = 17207, // 29DD->self, 8.0s cast, range 8-20 donut + UncloudedAscension1 = 17335, // 2AD1->self, 5.0s cast, range 10 circle + ThePathOfLight = 17230, // 2A3F->self, 5.5s cast, range 15 circle + InquisitorsBlade = 17095, // 29E4->self, 5.0s cast, range 40 180-degree cone + RainOfLight = 17082, // 29DD->location, 3.0s cast, range 4 circle + ArrowOfFortitude = 17211, // Andreia->self, 4.0s cast, range 30 width 8 rect + BodkinVolley1 = 17189, // Andreia->29DF, 6.0s cast, range 5 circle +} + +class ArrowOfFortitude(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArrowOfFortitude), new AOEShapeRect(30, 4)); +class BodkinVolley(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.BodkinVolley1), 5, minStackSize: 1); +class RainOfLight(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.RainOfLight), 4); +class ThePathOfLight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ThePathOfLight), new AOEShapeCircle(15)); +class InquisitorsBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.InquisitorsBlade), new AOEShapeCone(40, 90.Degrees())); +class MythrilCycloneKB(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.MythrilCyclone1), 18, stopAtWall: true); +class MythrilCycloneDonut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MythrilCyclone2), new AOEShapeDonut(8, 20)); +class SanctifiedMeltdown(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SanctifiedMeltdown), 6); +class UncloudedAscension(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.UncloudedAscension1), new AOEShapeCircle(10)); +class Overcome(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overcome), new AOEShapeCone(8.5f, 60.Degrees())); + +class SanctifiedFireII(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(5), 23, centerAtTarget: true) +{ + private DateTime Timeout = DateTime.MaxValue; + + public override void Update() + { + // for some reason, the magus can just forget to cast the two followups, leaving lue-reeq to run around like a moron + if (WorldState.CurrentTime > Timeout && CurrentBaits.Count > 0) + Reset(); + } + + private void Reset() + { + CurrentBaits.Clear(); + NumCasts = 0; + Timeout = DateTime.MaxValue; + } + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + base.OnEventIcon(actor, iconID, targetID); + if (iconID == IID) + Timeout = WorldState.FutureTime(10); + } + + public override void OnActorCreated(Actor actor) + { + if (actor.OID == 0x29E5 && ++NumCasts >= 3) + Reset(); + } +} + +class FireVoidzone(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 5, ActionID.MakeSpell(AID.SanctifiedFireII1), m => m.Enemies(0x29E5).Where(e => e.EventState != 7), 0.25f); + +class ImmaculateWarriorStates : StateMachineBuilder +{ + public ImmaculateWarriorStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.Enemies(OID.Andreia).All(x => x.IsDeadOrDestroyed); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68814, NameID = 8782)] +public class ImmaculateWarrior(WorldState ws, Actor primary) : BossModule(ws, primary, new(-247, 688.5f), new ArenaBoundsCircle(19.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.TargetID == actor.InstanceID ? 1 : 0; + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs new file mode 100644 index 0000000000..bdb2da0d87 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs @@ -0,0 +1,35 @@ +using BossMod.QuestBattle.Shadowbringers.MSQ; + +namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P1; + +public enum AID : uint +{ + AntiPersonnelMissile = 24845, // 233C->player/321D, 5.0s cast, range 6 circle + MRVMissile = 24843, // 233C->location, 8.0s cast, range 12 circle +} + +enum OID : uint +{ + Boss = 0x3376 +} + +class AlisaieAI(BossModule module) : Components.RotationModule(module); +class AntiPersonnelMissile(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.AntiPersonnelMissile), 6); +class MRVMissile(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MRVMissile), 12, maxCasts: 6); + +public class TelotekGammaStates : StateMachineBuilder +{ + public TelotekGammaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69602, NameID = 10189)] +public class TelotekGamma(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, -180), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs new file mode 100644 index 0000000000..a0768d5224 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs @@ -0,0 +1,110 @@ +using BossMod.QuestBattle; +using RID = BossMod.Roleplay.AID; + +namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P2; + +public enum OID : uint +{ + Boss = 0x3200, + Fetters = 0x3218 +} + +public enum AID : uint +{ + LunarGungnir = 24025, // LunarOdin->31EC, 12.0s cast, range 6 circle + LunarGungnir1 = 24026, // LunarOdin->2E2E, 25.0s cast, range 6 circle + GungnirAOE = 24698, // 233C->self, 10.0s cast, range 10 circle + Gagnrath = 24030, // 321C->self, 3.0s cast, range 50 width 4 rect + GungnirSpread = 24029, // 321C->self, no cast, range 10 circle + LeftZantetsuken = 24034, // LunarOdin->self, 4.0s cast, range 70 width 39 rect + RightZantetsuken = 24032, // LunarOdin->self, 4.0s cast, range 70 width 39 rect +} + +class UriangerAI(WorldState ws) : UnmanagedRotation(ws, 25) +{ + public const ushort StatusParam = 158; + + private float HeliosLeft(Actor p) => p.IsTargetable ? StatusDetails(p, 836, Player.InstanceID).Left : float.MaxValue; + + protected override void Exec(Actor? primaryTarget) + { + var partyPositions = World.Party.WithoutSlot().Select(p => p.Position).ToList(); + + Hints.GoalZones.Add(pos => partyPositions.Count(p => p.InCircle(pos, 16))); + + if (World.Party.WithoutSlot().All(p => HeliosLeft(p) < 1 && p.Position.InCircle(Player.Position, 15.5f + p.HitboxRadius))) + UseAction(RID.AspectedHelios, Player); + + if (World.Party.WithoutSlot().FirstOrDefault(p => p.HPMP.CurHP < p.HPMP.MaxHP * 0.4f) is Actor low) + UseAction(RID.Benefic, low); + + UseAction(RID.MaleficIII, primaryTarget); + + if (Player.FindStatus(Roleplay.SID.DestinyDrawn) != null) + { + if (ComboAction == RID.DestinyDrawn) + UseAction(RID.LordOfCrowns, primaryTarget, -100); + + if (ComboAction == RID.DestinysSleeve) + UseAction(RID.TheScroll, Player, -100); + } + else + { + UseAction(RID.DestinyDrawn, Player, -100); + UseAction(RID.DestinysSleeve, Player, -100); + } + + UseAction(RID.FixedSign, Player, -150); + } +} + +class Fetters(BossModule module) : Components.Adds(module, (uint)OID.Fetters); +class AutoUri(BossModule module) : Components.RotationModule(module); +class GunmetalSoul(BossModule module) : Components.GenericAOEs(module) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Module.Enemies(0x1EB1D5).Where(e => e.EventState != 7).Select(e => new AOEInstance(new AOEShapeDonut(4, 100), e.Position)); +} +class LunarGungnir(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.LunarGungnir), 6); +class LunarGungnir2(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.LunarGungnir1), 6); +class Gungnir(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GungnirAOE), new AOEShapeCircle(10)); +class Gagnrath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Gagnrath), new AOEShapeRect(50, 2)); +class GungnirSpread(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(10), 189, ActionID.MakeSpell(AID.GungnirSpread), 5.3f, centerAtTarget: true); + +class Zantetsuken(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Casters.Select(c => new AOEInstance(new AOEShapeRect(70, 19.5f), actor.CastInfo!.LocXZ, actor.CastInfo!.Rotation, Module.CastFinishAt(actor.CastInfo))); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.RightZantetsuken or AID.LeftZantetsuken) + Casters.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.RightZantetsuken or AID.LeftZantetsuken) + Casters.Remove(caster); + } +} + +public class LunarOdinStates : StateMachineBuilder +{ + public LunarOdinStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69602, NameID = 10034)] +public class LunarOdin(WorldState ws, Actor primary) : BossModule(ws, primary, new(146.5f, 84.5f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs new file mode 100644 index 0000000000..21aa217e5f --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs @@ -0,0 +1,109 @@ +using BossMod.QuestBattle; +using RID = BossMod.Roleplay.AID; + +namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P3; + +public enum OID : uint +{ + Boss = 0x3201, + Helper = 0x233C, + MoonGana = 0x3219, + SpiritGana = 0x321A, + RavanasWill = 0x321B, +} + +public enum AID : uint +{ + Explosion = 24046, // 3204->self, 5.0s cast, range 80 width 10 cross +} + +public enum SID : uint +{ + Invincibility = 325, +} + +class GrahaAI(WorldState ws) : UnmanagedRotation(ws, 25) +{ + private IEnumerable Adds => World.Actors.Where(x => (OID)x.OID is OID.MoonGana or OID.SpiritGana or OID.RavanasWill && x.IsTargetable && !x.IsDead); + + // Ravana's Wills just move to boss, whereas butterflies are only a threat once they start casting + private bool ShouldBreak(Actor a) => StatusDetails(a, Roleplay.SID.Break, Player.InstanceID).Left == 0 && ((OID)a.OID == OID.RavanasWill || a.CastInfo != null); + + protected override void Exec(Actor? primaryTarget) + { + var adds = Adds.ToList(); + + if (adds.Any(ShouldBreak)) + { + Hints.GoalZones.Add(p => adds.Count(a => a.Position.InCircle(p, 20))); + if (adds.Any(a => ShouldBreak(a) && a.Position.InCircle(Player.Position, 20))) + UseAction(RID.Break, Player); + } + + if (MP >= 1000 && Player.HPMP.CurHP * 3 < Player.HPMP.MaxHP) + UseAction(RID.CureII, Player); + + if (MP < 800) + UseAction(RID.AllaganBlizzardIV, primaryTarget); + + if (primaryTarget?.OID == 0x3201) + { + var thunder = StatusDetails(primaryTarget, Roleplay.SID.ThunderIV, Player.InstanceID); + if (thunder.Left < 3) + UseAction(RID.ThunderIV, primaryTarget); + } + + switch (ComboAction) + { + case RID.FireIV: + UseAction(RID.FireIV2, primaryTarget); + break; + case RID.FireIV2: + UseAction(RID.FireIV3, primaryTarget); + break; + case RID.FireIV3: + UseAction(RID.Foul, primaryTarget); + break; + default: + UseAction(RID.FireIV, primaryTarget); + break; + } + } +} + +class AutoGraha(BossModule module) : Components.RotationModule(module); +class DirectionalParry(BossModule module) : Components.DirectionalParry(module, 0x3201) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Module.PrimaryActor.FindStatus(680) != null) + { + hints.AddForbiddenZone(new AOEShapeCone(100, 45.Degrees()), Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, WorldState.FutureTime(10)); + } + } +} +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), maxCasts: 2); + +class LunarRavanaStates : StateMachineBuilder +{ + public LunarRavanaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69602, NameID = 10037)] +public class LunarRavana(WorldState ws, Actor primary) : BossModule(ws, primary, new(-144, 83), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.CalculateModuleAIHints(slot, actor, assignment, hints); + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs new file mode 100644 index 0000000000..6201f5dbce --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs @@ -0,0 +1,44 @@ +namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P4; + +public enum OID : uint +{ + Boss = 0x3202, + Helper = 0x233C, + InfernalNail = 0x3205, +} + +public enum AID : uint +{ + RadiantPlume1 = 24057, // Helper->self, 7.0s cast, range 8 circle + Hellfire = 24058, // Boss->self, 36.0s cast, range 40 circle + Hellfire1 = 24059, // Boss->self, 28.0s cast, range 40 circle + CrimsonCyclone = 24054, // 3203->self, 4.5s cast, range 49 width 18 rect + Explosion = 24046, // 3204->self, 5.0s cast, range 80 width 10 cross + AgonyOfTheDamned1 = 24062, // Helper->self, 0.7s cast, range 40 circle +} + +class Hellfire(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Hellfire)); +class Hellfire1(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Hellfire1)); +class AgonyOfTheDamned(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AgonyOfTheDamned1)); +class RadiantPlume(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RadiantPlume1), new AOEShapeCircle(8)); +class CrimsonCyclone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CrimsonCyclone), new AOEShapeRect(49, 9), maxCasts: 3); +class InfernalNail(BossModule module) : Components.Adds(module, (uint)OID.InfernalNail, 5); +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), maxCasts: 2); + +class LunarIfritStates : StateMachineBuilder +{ + public LunarIfritStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69602, NameID = 10041)] +public class LunarIfrit(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs new file mode 100644 index 0000000000..d70c8c34b0 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs @@ -0,0 +1,113 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class Overcome(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overcome), new AOEShapeCone(8, 60.Degrees()), 2); +class Skydrive(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Skydrive), new AOEShapeCircle(5)); + +class SkyHighDrive(BossModule module) : Components.GenericRotatingAOE(module) +{ + Angle angle; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.SkyHighDriveCCW: + angle = -20.Degrees(); + return; + case AID.SkyHighDriveCW: + angle = 20.Degrees(); + return; + case AID.SkyHighDriveFirst: + if (angle != default) + { + Sequences.Add(new(new AOEShapeRect(40, 4), caster.Position, spell.Rotation, angle, Module.CastFinishAt(spell, 0.5f), 0.6f, 10, 4)); + } + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.SkyHighDriveFirst or AID.SkyHighDriveRest) + { + AdvanceSequence(caster.Position, caster.Rotation, WorldState.CurrentTime); + if (Sequences.Count == 0) + angle = default; + } + } +} + +class AvalancheAxe(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe1), new AOEShapeCircle(10)); +class AvalancheAxe2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe2), new AOEShapeCircle(10)); +class AvalancheAxe3(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe3), new AOEShapeCircle(10)); +class OvercomeAllOdds(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.OvercomeAllOdds), new AOEShapeCone(60, 15.Degrees()), 1) +{ + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + base.OnCastFinished(caster, spell); + if (NumCasts > 0) + MaxCasts = 2; + } +} +class Soulflash(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Soulflash1), new AOEShapeCircle(4)); +class EtesianAxe(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.EtesianAxe), 15, kind: Kind.DirForward); +class Soulflash2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Soulflash2), new AOEShapeCircle(8)); + +class GroundbreakerExaflares(BossModule module) : Components.Exaflare(module, new AOEShapeCircle(6)) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.GroundbreakerExaFirst) + { + Lines.Add(new Line + { + Next = caster.Position, + Advance = caster.Rotation.ToDirection() * 6, + Rotation = default, + NextExplosion = Module.CastFinishAt(spell), + TimeToMove = 1, + ExplosionsLeft = 8, + MaxShownExplosions = 3 + }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID is (uint)AID.GroundbreakerExaFirst or (uint)AID.GroundbreakerExaRest) + { + var line = Lines.FirstOrDefault(x => x.Next.AlmostEqual(caster.Position, 1)); + if (line != null) + AdvanceLine(line, caster.Position); + } + } +} + +class GroundbreakerCone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundbreakerCone), new AOEShapeCone(40, 45.Degrees())); +class GroundbreakerDonut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundbreakerDonut), new AOEShapeDonut(5, 20)); +class GroundbreakerCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundbreakerCircle), new AOEShapeCircle(15)); + +class ArdbertStates : StateMachineBuilder +{ + public ArdbertStates(BossModule module) : base(module) + { + TrivialPhase(0) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 8258, PrimaryActorOID = (uint)OID.Ardbert)] +public class Ardbert(WorldState ws, Actor primary) : BossModule(ws, primary, new(-392, 780), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs new file mode 100644 index 0000000000..f7e49aae1f --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs @@ -0,0 +1,53 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +public enum OID : uint +{ + KingThordan = 0x2F1D, + FlameGeneralAldynn = 0x2F1E, + Nidhogg = 0x2F21, + Zenos = 0x2F28, + Ardbert = 0x2F2E, + Helper = 0x233C, +} + +public enum AID : uint +{ + // raubahn + FlamingTizona = 21094, // player->location, 4.0s cast, range 6 circle + + // thordan + TheDragonsGaze = 21090, // 2F1D->self, 4.0s cast, range 80 circle + + // nidhogg + HighJump = 21299, // player->self, 4.0s cast, range 8 circle + Geirskogul = 21098, // 2F22/2F21->self, 4.0s cast, range 62 width 8 rect + + // zenos + EntropicFlame = 21117, // Helper->self, 5.0s cast, range 50 width 8 rect + VeinSplitter = 21118, // 2F29->self, 5.0s cast, range 10 circle + + // ardbert + Overcome = 21126, // Ardbert->self, 2.5s cast, range 8 120-degree cone + Skydrive = 21127, // Ardbert->self, 2.5s cast, range 5 circle + SkyHighDriveCCW = 21138, // Ardbert->self, 4.5s cast, single-target + SkyHighDriveCW = 21139, // Ardbert->self, 4.5s cast, single-target + SkyHighDriveFirst = 21140, // 233C->self, 5.0s cast, range 40 width 8 rect + SkyHighDriveRest = 21141, // 233C->self, no cast, range 40 width 8 rect + AvalanceAxe1 = 21145, // 233C->self, 4.0s cast, range 10 circle + AvalanceAxe2 = 21144, // 233C->self, 7.0s cast, range 10 circle + AvalanceAxe3 = 21143, // 233C->self, 10.0s cast, range 10 circle + OvercomeAllOdds = 21130, // 233C->self, 2.5s cast, range 60 30-degree cone + Soulflash1 = 21136, // 233C->self, 4.0s cast, range 4 circle + EtesianAxe = 21147, // 233C->self, 6.5s cast, range 80 circle + Soulflash2 = 21137, // 233C->self, 4.0s cast, range 8 circle + GroundbreakerExaFirst = 21563, // 233C->self, 5.0s cast, range 6 circle + GroundbreakerExaRest = 21151, // 233C->self, no cast, range 6 circle + GroundbreakerCone = 21153, // 233C->self, 6.0s cast, range 40 90-degree cone + GroundbreakerDonut = 21157, // 233C->self, 6.0s cast, range 5-20 donut + GroundbreakerCircle = 21155, // 233C->self, 6.0s cast, range 15 circle +} + +public enum SID : uint +{ + Invincibility = 671 +} diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs new file mode 100644 index 0000000000..54d236eee2 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs @@ -0,0 +1,18 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class FlamingTizona(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FlamingTizona), 6); + +class FlameGeneralAldynnStates : StateMachineBuilder +{ + public FlameGeneralAldynnStates(BossModule module) : base(module) + { + TrivialPhase().ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 4739, PrimaryActorOID = (uint)OID.FlameGeneralAldynn)] +public class FlameGeneralAldynn(WorldState ws, Actor primary) : BossModule(ws, primary, new(-143, 357), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs new file mode 100644 index 0000000000..4eb731e55c --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs @@ -0,0 +1,23 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class DragonsGaze(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.TheDragonsGaze)); + +class KingThordanStates : StateMachineBuilder +{ + public KingThordanStates(BossModule module) : base(module) + { + TrivialPhase().ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 3632, PrimaryActorOID = (uint)OID.KingThordan)] +public class KingThordan(WorldState ws, Actor primary) : BossModule(ws, primary, new(-247, 321), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs new file mode 100644 index 0000000000..a7cc3c74c6 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs @@ -0,0 +1,15 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class HighJump(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HighJump), new AOEShapeCircle(8)); +class Geirskogul(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Geirskogul), new AOEShapeRect(62, 4)); + +class NidhoggStates : StateMachineBuilder +{ + public NidhoggStates(BossModule module) : base(module) + { + TrivialPhase().ActivateOnEnter().ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 3458, PrimaryActorOID = (uint)OID.Nidhogg)] +public class Nidhogg(WorldState ws, Actor primary) : BossModule(ws, primary, new(-242, 436.5f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs new file mode 100644 index 0000000000..848cdcc2a3 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs @@ -0,0 +1,21 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class Swords(BossModule module) : Components.AddsMulti(module, [0x2F2A, 0x2F2B, 0x2F2C]); + +class EntropicFlame(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EntropicFlame), new AOEShapeRect(50, 4)); +class VeinSplitter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), new AOEShapeCircle(10)); + +class ZenosYaeGalvusStates : StateMachineBuilder +{ + public ZenosYaeGalvusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 6039, PrimaryActorOID = (uint)OID.Zenos)] +public class ZenosYaeGalvus(WorldState ws, Actor primary) : BossModule(ws, primary, new(-321.03f, 617.73f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs b/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs new file mode 100644 index 0000000000..d77256e6f8 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs @@ -0,0 +1,126 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.FullSteamAhead; + +public enum OID : uint +{ + Boss = 0x295D, + LightningVoidzone = 0x1E9685 +} + +public enum AID : uint +{ + ShatteredSky = 16405, // Boss->self, 5.0s cast, single-target + ShatteredSky1 = 16429, // 233C->self, 6.0s cast, range 45 circle + HotPursuit = 16406, // Boss->self, 3.0s cast, single-target + HotPursuit1 = 16430, // 233C->location, 3.0s cast, range 5 circle + NexusOfThunder = 16404, // Boss->self, 3.0s cast, single-target + NexusOfThunder1 = 16427, // 233C->self, 7.0s cast, range 60+R width 5 rect + Wrath = 16425, // 295E->self, no cast, range 100 circle + CoiledLevin = 16424, // 295E->self, 3.0s cast, single-target + CoiledLevin1 = 16428, // 233C->self, 7.0s cast, range 6 circle + UnbridledWrath = 16426, // 295E->self, no cast, range 100 circle + HiddenCurrent = 16403, // Boss->location, no cast, ??? + VeilOfGukumatz = 16423, // 2998->self, no cast, single-target + VeilOfGukumatz1 = 16422, // 295D->self, no cast, single-target + VeilOfGukumatz2 = 16402, // Boss->self, no cast, single-target + UnceremoniousBeheading = 16412, // 295D->self, 3.5s cast, range 10 circle + HiddenCurrent1 = 16411, // 295D->location, no cast, ??? + MercilessLeft = 16415, // 295D->self, 4.0s cast, single-target + MercilessLeft1 = 33202, // 233C->self, 4.0s cast, range 40 120-degree cone + MercilessRight = 16431, // 233C->self, 4.0s cast, range 40 120-degree cone + KatunCycle = 16413, // 295D->self, 5.5s cast, range 5-40 donut + HotPursuit2 = 16410, // 295D->self, 3.0s cast, single-target + AgelessSerpent = 16417, // 295D->self, no cast, single-target + SerpentRising = 16433, // 295F->self, no cast, single-target + Evisceration = 16419, // 295D->self, 2.0s cast, range 40 120-degree cone + Spiritcall = 16420, // 295D->self, no cast, range 100 circle + SnakingFlame = 16432, // 295F->player, 40.0s cast, width 4 rect charge +} + +public enum SID : uint +{ + Smackdown = 2068, +} + +class KatunCycle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KatunCycle), new AOEShapeDonut(5, 40)); +class MercilessLeft(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessLeft1), new AOEShapeCone(40, 60.Degrees())); +class MercilessRight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessRight), new AOEShapeCone(40, 60.Degrees())); +class UnceremoniousBeheading(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.UnceremoniousBeheading), new AOEShapeCircle(10)); +class Evisceration(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Evisceration), new AOEShapeCone(40, 60.Degrees())); + +class HotPursuit(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); +class NexusOfThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder1), new AOEShapeRect(60, 2.5f)); +class CoiledLevin(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CoiledLevin1), new AOEShapeCircle(6)); +class LightningVoidzone(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.LightningVoidzone).Where(x => x.EventState != 7)); + +class ThancredAI(BossModule module) : Components.RotationModule(module); + +class AutoThancred(WorldState ws) : UnmanagedRotation(ws, 3) +{ + protected override void Exec(Actor? primaryTarget) + { + if (World.Client.DutyActions[0].CurCharges > 0) + { + UseAction(World.Client.DutyActions[0].Action, primaryTarget); + return; + } + + if (primaryTarget == null) + return; + + var distance = Player.DistanceToHitbox(primaryTarget); + + if (distance <= 3) + { + UseAction(Roleplay.AID.Smackdown, Player, -100); + + if (Player.FindStatus(SID.Smackdown) != null) + UseAction(Roleplay.AID.RoughDivide, primaryTarget, -100); + } + + if (Player.HPMP.CurHP * 2 < Player.HPMP.MaxHP) + UseAction(Roleplay.AID.SoothingPotion, Player, -100); + + switch (ComboAction) + { + case Roleplay.AID.BrutalShell: + UseAction(Roleplay.AID.SolidBarrel, primaryTarget); + break; + case Roleplay.AID.KeenEdge: + UseAction(Roleplay.AID.BrutalShell, primaryTarget); + break; + default: + UseAction(Roleplay.AID.KeenEdge, primaryTarget); + break; + } + } +} + +class RanjitStates : StateMachineBuilder +{ + public RanjitStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69155, NameID = 8374)] +public class Ranjit(WorldState ws, Actor primary) : BossModule(ws, primary, new(-203, 395), new ArenaBoundsCircle(19.5f)) +{ + protected override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Enemies(0x295C), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs b/BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs new file mode 100644 index 0000000000..29fcdbbe6e --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs @@ -0,0 +1,88 @@ +namespace BossMod.Shadowbringers.Quest.GambolingForGil; + +public enum OID : uint +{ + Boss = 0x29D2, // R0.500, x1 + Whirlwind = 0x29D5, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + WarDance = 17197, // Boss->self, 3.0s cast, range 5 circle + CharmingChasse = 17198, // Boss->self, 3.0s cast, range 40 circle + HannishFire1 = 17204, // 29D6->location, 3.3s cast, range 6 circle + Foxshot = 17289, // Boss->player, 6.0s cast, width 4 rect charge + HannishWaters = 17214, // 2A0B->self, 5.0s cast, range 40+R 30-degree cone + RanaasFinish = 15646, // Boss->self, 6.0s cast, range 15 circle +} + +class Foxshot(BossModule module) : Components.BaitAwayChargeCast(module, ActionID.MakeSpell(AID.Foxshot), 2); +class FoxshotKB(BossModule module) : Components.Knockback(module, stopAtWall: true) +{ + private readonly List Casters = []; + private Whirlwind? ww; + + public override IEnumerable Sources(int slot, Actor actor) => Casters.Select(c => new Source(c.Position, 25, Module.CastFinishAt(c.CastInfo))); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + ww ??= Module.FindComponent(); + + if (Casters.FirstOrDefault() is not Actor source) + return; + + var sources = ww?.Sources(Module).Select(p => p.Position).ToList() ?? []; + if (sources.Count == 0) + return; + + hints.AddForbiddenZone(p => + { + foreach (var s in sources) + if (Intersect.RayCircle(source.Position, source.DirectionTo(p), s, 6) < 1000) + return -1; + + return 1; + }, Module.CastFinishAt(source.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Foxshot) + Casters.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Foxshot) + Casters.Remove(caster); + } +} +class Whirlwind(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.Whirlwind).Where(x => !x.IsDead)); +class WarDance(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WarDance), new AOEShapeCircle(5)); +class CharmingChasse(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.CharmingChasse)); +class HannishFire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HannishFire1), 6); +class HannishWaters(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HannishWaters), new AOEShapeCone(40, 15.Degrees())); +class RanaasFinish(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RanaasFinish), new AOEShapeCircle(15)); + +class RanaaMihgoStates : StateMachineBuilder +{ + public RanaaMihgoStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 670, NameID = 8489)] +public class RanaaMihgo(WorldState ws, Actor primary) : BossModule(ws, primary, new(520.47f, 124.99f), WeirdBounds) +{ + public static readonly ArenaBoundsCustom WeirdBounds = new(17.5f, new(CurveApprox.Ellipse(17.5f, 16f, 0.01f))); +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs b/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs new file mode 100644 index 0000000000..2a2292da1c --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs @@ -0,0 +1,118 @@ +using BossMod.QuestBattle.Shadowbringers.RoleQuests; + +namespace BossMod.Shadowbringers.Quest.NyelbertsLament; + +// TODO: add AI hint for the "enrage" + paladin safe zone + +public enum OID : uint +{ + Boss = 0x2977, + Helper = 0x233C, + BovianBull = 0x2976, + _Gen_LooseBoulder = 0x2978, // R2.400, x0 (spawn during fight) +} + +public enum AID : uint +{ + FallingRock = 16595, // Helper->location, 3.0s cast, range 4 circle + ZoomTargetSelect = 16599, // Helper->player, no cast, single-target + ZoomIn = 16598, // Helper->self, no cast, range 42 width 8 rect + TwoThousandMinaSlash = 16601, // Bovian->self/player, 5.0s cast, range 40 ?-degree cone +} + +public enum SID : uint +{ + WingedShield = 1900 +} + +class TwoThousandMinaSlash : Components.GenericLineOfSightAOE +{ + private readonly List _casters = []; + + public TwoThousandMinaSlash(BossModule module) : base(module, ActionID.MakeSpell(AID.TwoThousandMinaSlash), 40, false) + { + Refresh(); + } + + public Actor? ActiveCaster => _casters.MinBy(c => c.CastInfo!.RemainingTime); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + { + _casters.Add(caster); + Refresh(); + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + { + _casters.Remove(caster); + Refresh(); + } + } + + private void Refresh() + { + var blockers = Module.Enemies(OID._Gen_LooseBoulder); + + Modify(ActiveCaster?.CastInfo?.LocXZ, blockers.Select(b => (b.Position, b.HitboxRadius)), Module.CastFinishAt(ActiveCaster?.CastInfo)); + } +} + +class FallingRock(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FallingRock), 4); +class ZoomIn(BossModule module) : Components.SimpleLineStack(module, 4, 42, ActionID.MakeSpell(AID.ZoomTargetSelect), ActionID.MakeSpell(AID.ZoomIn), 5.1f) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Source != null) + hints.AddForbiddenZone(new AOEShapeDonut(3, 100), Arena.Center, default, Activation); + } +} + +class PassageOfArms(BossModule module) : BossComponent(module) +{ + private ActorCastInfo? EnrageCast => Module.PrimaryActor.CastInfo is { Action.ID: 16604 } castInfo ? castInfo : null; + private Actor? Paladin => WorldState.Actors.FirstOrDefault(x => x.FindStatus(SID.WingedShield) != null); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (EnrageCast != null && Paladin != null) + hints.AddForbiddenZone(ShapeDistance.InvertedCone(Paladin.Position, 8, Paladin.Rotation + 180.Degrees(), 60.Degrees()), Module.CastFinishAt(EnrageCast)); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (EnrageCast != null && Paladin != null) + Arena.ZoneCone(Paladin.Position, 0, 8, Paladin.Rotation + 180.Degrees(), 60.Degrees(), ArenaColor.SafeFromAOE); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (EnrageCast != null && Paladin != null && !actor.Position.InCircleCone(Paladin.Position, 8, Paladin.Rotation + 180.Degrees(), 60.Degrees())) + hints.Add("Hide behind tank!"); + } +} + +class NyelbertAI(BossModule module) : Components.RotationModule(module); + +class BovianStates : StateMachineBuilder +{ + public BovianStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69162, NameID = 8363)] +public class Bovian(WorldState ws, Actor primary) : BossModule(ws, primary, new(-440, -691), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs b/BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs new file mode 100644 index 0000000000..b69b7bc9cd --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs @@ -0,0 +1,84 @@ +namespace BossMod.Shadowbringers.Quest.SaveTheLastDanceForMe; + +public enum OID : uint +{ + Boss = 0x2AC7, // R2.400, x1 + ShadowySpume = 0x2AC8, // R0.800, x0 (spawn during fight) + ForebodingAura = 0x2ACB, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + Dread = 17476, // Boss->location, 3.0s cast, range 5 circle + Anguish = 17487, // ->2ACD, 5.5s cast, range 6 circle + WhelmingLossFirst = 17480, // AethericShadow->self, 5.0s cast, range 5 circle + WhelmingLossRest = 17481, // AethericShadow1->self, no cast, range 5 circle + BitterLove = 15650, // 2AC9->self, 3.0s cast, range 12 120-degree cone +} + +class Dread(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Dread), 5); +class BitterLove(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BitterLove), new AOEShapeCone(12, 60.Degrees())); +class WhelmingLoss(BossModule module) : Components.Exaflare(module, new AOEShapeCircle(5)) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.WhelmingLossFirst) + Lines.Add(new Line + { + Next = caster.Position, + Advance = caster.Rotation.ToDirection() * 5, + NextExplosion = Module.CastFinishAt(spell), + TimeToMove = 1, + ExplosionsLeft = 7, + MaxShownExplosions = 3 + }); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID is (uint)AID.WhelmingLossFirst or (uint)AID.WhelmingLossRest) + { + var index = Lines.FindIndex(l => l.Next.AlmostEqual(caster.Position, 1)); + if (index == -1) + { + ReportError($"Failed to find entry for {caster.InstanceID:X}"); + return; + } + + AdvanceLine(Lines[index], caster.Position); + } + } +} +class Adds(BossModule module) : Components.Adds(module, (uint)OID.ShadowySpume); +class Anguish(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.Anguish), 6); +class ForebodingAura(BossModule module) : Components.GenericAOEs(module) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Module.Enemies(OID.ForebodingAura).Where(e => !e.IsDead).Select(e => new AOEInstance(new AOEShapeCircle(8), e.Position)); +} + +class AethericShadowStates : StateMachineBuilder +{ + public AethericShadowStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68790, NameID = 8493)] +public class AethericShadow(WorldState ws, Actor primary) : BossModule(ws, primary, new(73.6f, -743.6f), new ArenaBoundsCircle(20)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.FindStatus(DNC.SID.ClosedPosition) == null && Raid.WithoutSlot().Exclude(actor).FirstOrDefault() is Actor partner) + { + hints.ActionsToExecute.Push(ActionID.MakeSpell(DNC.AID.ClosedPosition), partner, ActionQueue.Priority.VeryHigh); + } + } +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs new file mode 100644 index 0000000000..81930eb7aa --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs @@ -0,0 +1,38 @@ +using BossMod.QuestBattle.Shadowbringers.SideQuests; + +namespace BossMod.Shadowbringers.Quest.SleepNowInSapphire.P1GuidanceSystem; + +public enum OID : uint +{ + Boss = 0x2DFF, + Helper = 0x233C, +} + +public enum AID : uint +{ + AerialBombardment = 21492, // 233C->location, 2.5s cast, range 12 circle +} + +class AerialBombardment(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AerialBombardment), 12); + +class GWarrior(BossModule module) : Components.RotationModule(module); + +class GuidanceSystemStates : StateMachineBuilder +{ + public GuidanceSystemStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69431, NameID = 9461)] +public class GuidanceSystem(WorldState ws, Actor primary) : BossModule(ws, primary, new(-15, 610), new ArenaBoundsSquare(60, 1)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.FindStatus(Roleplay.SID.PyreticBooster) == null) + hints.ActionsToExecute.Push(ActionID.MakeSpell(Roleplay.AID.PyreticBooster), actor, ActionQueue.Priority.Medium); + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs new file mode 100644 index 0000000000..a0ec6bca18 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs @@ -0,0 +1,86 @@ +using BossMod.Shadowbringers.Quest.SleepNowInSapphire.P1GuidanceSystem; + +namespace BossMod.Shadowbringers.Quest.SleepNowInSapphire.P2SapphireWeapon; + +public enum OID : uint +{ + Boss = 0x2DFA, + Helper = 0x233C, +} + +public enum AID : uint +{ + TailSwing = 20326, // Boss->self, 4.0s cast, range 46 circle + OptimizedJudgment = 20325, // Boss->self, 4.0s cast, range -60 donut + MagitekSpread = 20336, // RegulasImage->self, 5.0s cast, range 43 ?-degree cone + SideraysRight = 20329, // Helper->self, 8.0s cast, range 128 ?-degree cone + SideraysLeft = 21021, // Helper->self, 8.0s cast, range 128 ?-degree cone + SapphireRay = 20327, // Boss->self, 8.0s cast, range 120 width 40 rect + MagitekRay = 20332, // 2DFC->self, 3.0s cast, range 100 width 6 rect + ServantRoar = 20339, // 2DFD->self, 2.5s cast, range 100 width 8 rect +} + +public enum SID : uint +{ + Invincibility = 775, // none->Boss, extra=0x0 +} + +class MagitekRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRay), new AOEShapeRect(100, 3)); +class ServantRoar(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ServantRoar), new AOEShapeRect(100, 4)); +class TailSwing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TailSwing), new AOEShapeCircle(46)); +class OptimizedJudgment(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.OptimizedJudgment), new AOEShapeDonut(21, 60)); +class MagitekSpread(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekSpread), new AOEShapeCone(43, 120.Degrees())); +class SapphireRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SapphireRay), new AOEShapeRect(120, 20)); +class Siderays(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor, WPos)> Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Casters.Select(c => new AOEInstance(new AOEShapeCone(128, 45.Degrees()), c.Item2, c.Item1.CastInfo!.Rotation, Module.CastFinishAt(c.Item1.CastInfo))); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.SideraysLeft: + Casters.Add((caster, caster.Position + caster.Rotation.ToDirection().OrthoL() * 15)); + break; + case AID.SideraysRight: + Casters.Add((caster, caster.Position + caster.Rotation.ToDirection().OrthoR() * 15)); + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + Casters.RemoveAll(c => c.Item1 == caster); + } +} + +class TheSapphireWeaponStates : StateMachineBuilder +{ + public TheSapphireWeaponStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69431, NameID = 9458)] +public class TheSapphireWeapon(WorldState ws, Actor primary) : BossModule(ws, primary, new(-15, 610), new ArenaBoundsSquare(60, 1)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs b/BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs new file mode 100644 index 0000000000..dfd2b3530e --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs @@ -0,0 +1,128 @@ +namespace BossMod.Shadowbringers.Quest.SteelAgainstSteel; + +public enum OID : uint +{ + Boss = 0x2A45, + Helper = 0x233C, + Fustuarium = 0x2AD8, // R0.500, x1 (spawn during fight) + CullingBlade = 0x2AD3, // R0.500, x0 (spawn during fight) + IndustrialForce = 0x2BCE, // R0.500, x0 (spawn during fight) + TerminusEst = 0x2A46, // R1.000, x0 (spawn during fight) + CaptiveBolt = 0x2AD7, // R0.500, x0 (spawn during fight) +} + +public enum AID : uint +{ + CullingBlade1 = 17553, // CullingBlade->self, 3.5s cast, range 60 30-degree cone + TheOrder = 17568, // Boss->self, 4.0s cast, single-target + TerminusEst1 = 17567, // TerminusEst->self, no cast, range 40+R width 4 rect + CaptiveBolt = 17561, // CaptiveBolt->self, 7.0s cast, range 50+R width 10 rect + AetherochemicalGrenado = 17575, // 2A47->location, 4.0s cast, range 8 circle + Exsanguination = 17565, // 2AD6->self, 5.0s cast, range -17 donut + Exsanguination1 = 17564, // 2AD5->self, 5.0s cast, range -12 donut + Exsanguination2 = 17563, // 2AD4->self, 5.0s cast, range -7 donut + DiffractiveLaser = 17574, // 2A48->self, 3.0s cast, range 45+R width 4 rect + SnakeShot = 17569, // Boss->self, 4.0s cast, range 20 240-degree cone + ScaldingTank1 = 17558, // Fustuarium->2A4A, 6.0s cast, range 6 circle + ToTheSlaughter = 17559, // Boss->self, 4.0s cast, range 40 180-degree cone +} + +class ScaldingTank(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ScaldingTank1), 6); +class ToTheSlaughter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ToTheSlaughter), new AOEShapeCone(40, 90.Degrees())); +class Exsanguination(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor Actor, float Inner)> Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Casters.Select(c => new AOEInstance(new AOEShapeDonutSector(c.Inner, c.Inner + 5, 90.Degrees()), c.Actor.CastInfo!.LocXZ, c.Actor.Rotation, Module.CastFinishAt(c.Actor.CastInfo))); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + var radius = (AID)spell.Action.ID switch + { + AID.Exsanguination => 12, + AID.Exsanguination1 => 7, + AID.Exsanguination2 => 2, + _ => 0 + }; + + if (radius > 0) + Casters.Add((caster, radius)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.Exsanguination or AID.Exsanguination1 or AID.Exsanguination2) + Casters.RemoveAll(c => c.Actor == caster); + } +} +class CaptiveBolt(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CaptiveBolt), new AOEShapeRect(50, 5), maxCasts: 4); +class AetherochemicalGrenado(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AetherochemicalGrenado), 8); +class DiffractiveLaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), new AOEShapeRect(45, 2)); +class SnakeShot(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SnakeShot), new AOEShapeCone(20, 120.Degrees())); +class CullingBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CullingBlade1), new AOEShapeCone(60, 15.Degrees())) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + + // zone rasterization can end up missing the arena center since it only contains the tips of a bunch of very pointy triangles + if (Casters.FirstOrDefault() is Actor c) + hints.AddForbiddenZone(ShapeDistance.Circle(c.Position, 0.5f), Module.CastFinishAt(c.CastInfo)); + } +} +class TerminusEst(BossModule module) : Components.GenericAOEs(module) +{ + private Actor? Caster; + private readonly List Actors = []; + + public override void OnActorCreated(Actor actor) + { + if (actor.OID == (uint)OID.TerminusEst) + Actors.Add(actor); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Caster is Actor c) + foreach (var t in Actors) + yield return new AOEInstance(new AOEShapeRect(40, 2), t.Position, t.Rotation, Module.CastFinishAt(c.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + // check if we already have terminuses out, because he can use this spell for a diff mechanic + if (spell.Action.ID == (uint)AID.TheOrder && Actors.Count > 0) + Caster = caster; + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.TerminusEst1) + { + Actors.Remove(caster); + // reset for next iteration + if (Actors.Count == 0) + Caster = null; + } + } +} + +class VitusQuoMessallaStates : StateMachineBuilder +{ + public VitusQuoMessallaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68802, NameID = 8872)] +public class VitusQuoMessalla(WorldState ws, Actor primary) : BossModule(ws, primary, new(-266, -507), new ArenaBoundsCircle(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs b/BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs new file mode 100644 index 0000000000..44d334849b --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs @@ -0,0 +1,116 @@ +namespace BossMod.Shadowbringers.Quest.TheGreatShipVylbrand; + +public enum OID : uint +{ + Boss = 0x3107 +} + +public enum AID : uint +{ + W10TrolleyWallop = 22950, // 3104->self, 6.0s cast, range 40 60-degree cone + W10TrolleyTap = 23362, // 3104->self, 3.5s cast, range 8 120-degree cone + W10TrolleyTorque = 22949, // 3104->self, 6.0s cast, range 16 circle + Bulldoze = 22955, // 3107->location, 8.0s cast, width 6 rect charge + Bulldoze1 = 22957, // 233C->location, 8.0s cast, width 6 rect charge + TunnelShaker1 = 22959, // 233C->self, 5.0s cast, range 60 30-degree cone + Uplift = 22961, // 233C->self, 6.0s cast, range 10 circle + Uplift1 = 22962, // 233C->self, 8.0s cast, range 10-20 donut + Uplift2 = 22963, // 233C->self, 10.0s cast, range 20-30 donut +} + +class Torque(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W10TrolleyTorque), new AOEShapeCircle(16)); +class Tap(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W10TrolleyTap), new AOEShapeCone(8, 60.Degrees())); +class Wallop(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W10TrolleyWallop), new AOEShapeCone(40, 30.Degrees())); +class Bulldoze(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.Bulldoze), 3); +class Bulldoze2(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.Bulldoze1), 3); +class TunnelShaker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TunnelShaker1), new AOEShapeCone(60, 15.Degrees())); +class Uplift(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeCircle(10), new AOEShapeDonut(10, 20), new AOEShapeDonut(20, 30)]) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Uplift) + { + AddSequence(caster.Position, Module.CastFinishAt(spell)); + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + var order = (AID)spell.Action.ID switch + { + AID.Uplift => 0, + AID.Uplift1 => 1, + AID.Uplift2 => 2, + _ => -1 + }; + if (!AdvanceSequence(order, caster.Position, WorldState.FutureTime(2))) + ReportError($"unexpected order {order}"); + } +} + +class BombTether : Components.BaitAwayTethers +{ + private DateTime? Activation; + + public BombTether(BossModule module) : base(module, new AOEShapeCircle(6), 97) + { + CenterAtTarget = true; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Activation != null) + hints.AddForbiddenZone(new AOEShapeDonut(1.5f, 100), new(9.15f, -8.44f), activation: Activation.Value); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (CurrentBaits.Count > 0) + hints.Add("Intercept tether!", CurrentBaits.Any(b => b.Target != actor)); + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + base.OnTethered(source, tether); + if (tether.ID == TID) + Activation = WorldState.FutureTime(15); + } + + public override void OnUntethered(Actor source, ActorTetherInfo tether) + { + base.OnUntethered(source, tether); + if (tether.ID == TID) + Activation = null; + } +} + +public class SecondOrderRocksplitterStates : StateMachineBuilder +{ + public SecondOrderRocksplitterStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.WorldState.CurrentCFCID != 764; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69551)] +public class SecondOrderRocksplitter(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(27)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + hints.InteractWithTarget = Enemies(0x1EB0F7).FirstOrDefault(x => x.IsTargetable); + + foreach (var e in hints.PotentialTargets) + if (e.Actor.OID == 0x3106) + e.Priority = AIHints.Enemy.PriorityPointless; + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs b/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs new file mode 100644 index 0000000000..d828e9c33a --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs @@ -0,0 +1,143 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.TheHardenedHeart; + +public enum OID : uint +{ + Boss = 0x2919, + Helper = 0x233C, +} + +public enum AID : uint +{ + SanctifiedFireIII = 18090, // 2922/2923->players/2917/2915/2914, 8.0s cast, range 6 circle + TwistedTalent1 = 13637, // Helper->player/2916/2914/2915/2917, 5.0s cast, range 5 circle + AbyssalCharge1 = 15539, // 25BB->self, 3.0s cast, range 40+R width 4 rect + DeadlyBite = 15543, // 291D/291C->player/2914, no cast, single-target + RustingClaw = 15540, // 291B/291A->self, 5.0s cast, range 8+R ?-degree cone +} + +class SanctifiedFireIII(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.SanctifiedFireIII), 6) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == StackAction && WorldState.Actors.Find(spell.TargetID) is Actor stackTarget && stackTarget.OID == 0x2915) + AddStack(stackTarget, Module.CastFinishAt(spell)); + } +} + +class TwistedTalent(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.TwistedTalent1), 5); +class AbyssalCharge(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AbyssalCharge1), new AOEShapeRect(40, 2)); + +class AutoBranden(WorldState ws) : UnmanagedRotation(ws, 3) +{ + protected override void Exec(Actor? primaryTarget) + { + if (primaryTarget == null) + return; + + var numAOETargets = Hints.PotentialTargets.Count(x => x.Actor.Position.InCircle(Player.Position, 5)); + + if (numAOETargets > 1) + { + if (ComboAction == Roleplay.AID.Sunshadow) + UseAction(Roleplay.AID.GreatestEclipse, Player); + + UseAction(Roleplay.AID.Sunshadow, Player); + } + + if (Player.HPMP.CurHP * 3 < Player.HPMP.MaxHP) + UseAction(Roleplay.AID.ChivalrousSpirit, Player); + + var gcd = ComboAction switch + { + Roleplay.AID.RightfulSword => Roleplay.AID.Swashbuckler, + Roleplay.AID.FastBlade => Roleplay.AID.RightfulSword, + _ => Roleplay.AID.FastBlade + }; + + UseAction(gcd, primaryTarget); + if (Player.DistanceToHitbox(primaryTarget) <= 3) + UseAction(Roleplay.AID.FightOrFlight, Player, -10); + + if (primaryTarget.CastInfo?.Interruptible ?? false) + UseAction(Roleplay.AID.Interject, primaryTarget, -10); + } +} + +class TankbusterTether(BossModule module) : BossComponent(module) +{ + private record class Tether(Actor Source, Actor Target, DateTime Activation); + private Tether? DwarfTether = null; + + private bool Danger => DwarfTether?.Target.OID == 0x2917; + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == 84 && WorldState.Actors.Find(tether.Target) is Actor target) + DwarfTether = new(source, target, DwarfTether?.Activation ?? WorldState.FutureTime(10)); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (DwarfTether?.Target.OID == 0x2917) + hints.AddForbiddenZone(ShapeDistance.InvertedRect(DwarfTether.Source.Position, DwarfTether.Target.Position, 1), DwarfTether.Activation); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Danger) + hints.Add("Take tether!"); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (DwarfTether is Tether t) + Arena.AddLine(t.Source.Position, t.Target.Position, ArenaColor.Danger); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.DeadlyBite) + DwarfTether = null; + } +} + +class BrandenAI(BossModule module) : Components.RotationModule(module); + +class RustingClaw(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RustingClaw), new AOEShapeCone(10.3f, 45.Degrees())); + +class TadricTheVaingloriousStates : StateMachineBuilder +{ + public TadricTheVaingloriousStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68783, NameID = 8339)] +public class TadricTheVainglorious(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsSquare(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + { + h.Priority = h.Actor.FindStatus(775) == null ? (h.Actor.TargetID == actor.InstanceID ? 2 : 1) : 0; + if (h.Actor.OID is not (0x291D or 0x2919) && h.Actor.CastInfo == null) + { + h.DesiredPosition = Arena.Center; + if (h.Actor.TargetID == actor.InstanceID && !h.Actor.Position.InCircle(Arena.Center, 5)) + hints.ForcedTarget = h.Actor; + } + } + } +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs b/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs new file mode 100644 index 0000000000..5ab0e350f1 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs @@ -0,0 +1,92 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.TheHuntersLegacy; + +public enum OID : uint +{ + Boss = 0x29EE, + Helper = 0x233C +} + +public enum AID : uint +{ + BalamBlaster = 17137, // Boss->self, 4.5s cast, range 30+R 270-degree cone + BalamBlasterRear = 17138, // Boss->self, 4.5s cast, range 30+R 270-degree cone + ElectricWhisker = 17126, // Boss->self, 3.5s cast, range 8+R 90-degree cone + RoaringThunder = 17135, // Boss->self, 4.0s cast, range 8-30 donut + StreakLightning = 17148, // 233C->location, 2.5s cast, range 3 circle + AlternatingCurrent1 = 17150, // Helper->self, 4.0s cast, range 60 width 5 rect + RumblingThunderStack = 17134, // Helper->player, 6.0s cast, range 5 circle + Thunderbolt1 = 17140, // Helper->players/29EC, 6.0s cast, range 5 circle + StreakLightning1 = 17147, // Helper->location, 2.5s cast, range 3 circle +} + +class Thunderbolt(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Thunderbolt1), 5); +class BalamBlaster(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BalamBlaster), new AOEShapeCone(38.05f, 135.Degrees())); +class BalamBlasterRear(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BalamBlasterRear), new AOEShapeCone(38.05f, 135.Degrees())); +class ElectricWhisker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ElectricWhisker), new AOEShapeCone(16.05f, 45.Degrees())); +class RoaringThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RoaringThunder), new AOEShapeDonut(8, 30)); +class StreakLightning(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.StreakLightning), 3); +class StreakLightning1(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.StreakLightning1), 3); +class AlternatingCurrent(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AlternatingCurrent1), new AOEShapeRect(60, 2.5f)); +class RumblingThunder(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.RumblingThunderStack), 5, 1); + +class RendaRae(WorldState ws) : UnmanagedRotation(ws, 20) +{ + protected override void Exec(Actor? primaryTarget) + { + var dot = StatusDetails(primaryTarget, Roleplay.SID.AcidicBite, Player.InstanceID); + if (dot.Left < 2.5f) + UseAction(Roleplay.AID.AcidicBite, primaryTarget, 10); + + UseAction(Roleplay.AID.RadiantArrow, primaryTarget, -5); + UseAction(Roleplay.AID.HeavyShot, primaryTarget); + + if (primaryTarget?.CastInfo?.Interruptible ?? false) + UseAction(Roleplay.AID.DullingArrow, primaryTarget, 5); + + if (Player.HPMP.MaxHP * 0.8f > Player.HPMP.CurHP) + UseAction(Roleplay.AID.HuntersPrudence, Player, -15); + } +} + +class RendaRaeAI(BossModule module) : Components.RotationModule(module); + +class RonkanAura(BossModule module) : BossComponent(module) +{ + private Actor? AuraCenter => Module.Enemies(0x1EADA5).FirstOrDefault(); + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (AuraCenter is Actor a) + Arena.ZoneCircle(a.Position, 10, ArenaColor.SafeFromAOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (AuraCenter is Actor a) + hints.AddForbiddenZone(new AOEShapeDonut(10, 100), a.Position, activation: WorldState.FutureTime(5)); + } +} + +class BalamQuitzStates : StateMachineBuilder +{ + public BalamQuitzStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68812, NameID = 8397)] +public class BalamQuitz(WorldState ws, Actor primary) : BossModule(ws, primary, new(-247.11f, 688.33f), new ArenaBoundsCircle(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs new file mode 100644 index 0000000000..f6a259691a --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs @@ -0,0 +1,29 @@ +namespace BossMod.Shadowbringers.Quest.TheLostAndTheFound.Sophrosyne; + +public enum OID : uint +{ + Boss = 0x29AA, + Helper = 0x233C, +} + +public enum AID : uint +{ + Charge = 16999, // 29AB->29A9, 3.0s cast, width 4 rect charge +} + +class Charge(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.Charge), 2); + +class SophrosyneStates : StateMachineBuilder +{ + public SophrosyneStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68806, NameID = 8395)] +public class Sophrosyne(WorldState ws, Actor primary) : BossModule(ws, primary, new(632, 64.15f), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs new file mode 100644 index 0000000000..a230df22b4 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs @@ -0,0 +1,87 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.TheLostAndTheFound.Yxtlilton; + +public enum OID : uint +{ + Boss = 0x29B0, + Helper = 0x233C, +} + +public enum AID : uint +{ + TheCodexOfDarknessII = 17010, // Boss->self, 3.0s cast, range 100 circle + TheCodexOfGravity = 17014, // Boss->player, 4.5s cast, range 6 circle +} + +class CodexOfDarknessII(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.TheCodexOfDarknessII)); +class CodexOfGravity(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.TheCodexOfGravity), 6) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + if (Stacks.Count > 0) + hints.AddForbiddenZone(new AOEShapeDonut(1.5f, 100), Arena.Center, default, Stacks[0].Activation); + } +} + +class LamittAI(WorldState ws) : UnmanagedRotation(ws, 25) +{ + protected override void Exec(Actor? primaryTarget) + { + if (primaryTarget == null) + return; + + var party = World.Party.WithoutSlot().ToList(); + + Hints.GoalZones.Add(p => party.Count(act => act.Position.InCircle(p, 15 + Player.HitboxRadius + act.HitboxRadius))); + + var lowest = party.MinBy(p => p.PredictedHPRatio)!; + var esunable = party.FirstOrDefault(x => x.FindStatus(482) != null); + var doomed = party.FirstOrDefault(x => x.FindStatus(1769) != null); + var partyHealth = party.Average(p => p.PredictedHPRatio); + + // pre heal during doom cast since it does insane damage for some reason + if (primaryTarget.CastInfo is { Action.ID: 17011 } ci && ci.TargetID == Player.InstanceID) + { + if (Player.PredictedHPRatio <= 0.8f) + UseAction(Roleplay.AID.RonkanCureII, Player); + } + + if (partyHealth < 0.6f) + UseAction(Roleplay.AID.RonkanMedica, Player); + + if (lowest.HPMP.CurHP * 3 <= lowest.HPMP.MaxHP) + UseAction(Roleplay.AID.RonkanCureII, lowest); + + if (esunable != null) + UseAction(Roleplay.AID.RonkanEsuna, esunable); + + if (doomed != null) + { + UseAction(Roleplay.AID.RonkanRenew, doomed); + UseAction(Roleplay.AID.RonkanCureII, doomed); + } + + UseAction(Roleplay.AID.RonkanStoneII, primaryTarget); + } +} + +class AutoLamitt(BossModule module) : Components.RotationModule(module); + +class YxtliltonStates : StateMachineBuilder +{ + public YxtliltonStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68806, NameID = 8393)] +public class Yxtlilton(WorldState ws, Actor primary) : BossModule(ws, primary, new(-120, -770), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs b/BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs new file mode 100644 index 0000000000..af4008c356 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs @@ -0,0 +1,40 @@ +namespace BossMod.Shadowbringers.Quest.TheOracleOfLight; + +public enum OID : uint +{ + Boss = 0x299D, + Helper = 0x233C, +} + +public enum AID : uint +{ + HotPursuit1 = 17622, // 2AF0->location, 3.0s cast, range 5 circle + NexusOfThunder1 = 17621, // 2AF0->self, 7.0s cast, range 60+R width 5 rect + NexusOfThunder2 = 17823, // 2AF0->self, 8.5s cast, range 60+R width 5 rect + Burn = 18035, // 2BE6->self, 4.5s cast, range 8 circle + UnbridledWrath = 18036, // 299E->self, 5.5s cast, range 90 width 90 rect +} + +class HotPursuit(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); +class NexusOfThunder1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder1), new AOEShapeRect(60, 2.5f)); +class NexusOfThunder2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder2), new AOEShapeRect(60, 2.5f)); +class Burn(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Burn), new AOEShapeCircle(8), maxCasts: 8); +class UnbridledWrath(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.UnbridledWrath), 20, kind: Kind.DirForward, stopAtWall: true); + +class RanjitStates : StateMachineBuilder +{ + public RanjitStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68841, NameID = 8374)] +public class Ranjit(WorldState ws, Actor primary) : BossModule(ws, primary, new(126.75f, -311.25f), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs b/BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs new file mode 100644 index 0000000000..6de41c42a6 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs @@ -0,0 +1,76 @@ +namespace BossMod.Shadowbringers.Quest.TheSoulOfTemperance; + +public enum OID : uint +{ + Boss = 0x29CE, + BossP2 = 0x29D0, + Helper = 0x233C, +} + +public enum AID : uint +{ + SanctifiedAero1 = 16911, // 2A0C->self, 4.0s cast, range 40+R width 6 rect + SanctifiedStone = 17322, // 29D0->self, 5.0s cast, single-target + HolyBlur = 17547, // 2969/29CF/274F/296A/2996->self, 5.0s cast, range 40 circle + Focus = 17548, // 29CF/296A/2996/2969->players, 5.0s cast, width 4 rect charge + TemperedVirtue = 15928, // BossP2->self, 6.0s cast, range 15 circle + WaterAndWine = 15604, // 2AF1->self, 5.0s cast, range 12 circle + ForceOfRestraint = 15603, // 2AF1->self, 5.0s cast, range 60+R width 4 rect + SanctifiedHoly1 = 16909, // BossP2->self, 4.0s cast, range 8 circle + SanctifiedHoly2 = 17604, // 2A0C->location, 4.0s cast, range 6 circle +} + +class SanctifiedHoly1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedHoly1), new AOEShapeCircle(8)); +class SanctifiedHoly2(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedHoly2), 6); +class ForceOfRestraint(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ForceOfRestraint), new AOEShapeRect(60, 2)); +class HolyBlur(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.HolyBlur)); +class Focus(BossModule module) : Components.BaitAwayChargeCast(module, ActionID.MakeSpell(AID.Focus), 2); +class TemperedVirtue(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TemperedVirtue), new AOEShapeCircle(15)); +class WaterAndWine(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WaterAndWine), new AOEShapeDonut(6, 12)); +class SanctifiedStone(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.SanctifiedStone), 5, 1); + +class SanctifiedAero(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedAero1), new AOEShapeRect(40.5f, 3)); + +class Repose(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + static bool SleepProof(Actor a) + { + if (a.Statuses.Any(x => x.ID is 1967 or 1968)) + return true; + + return a.PendingStatuses.Any(s => s.StatusId == 3); + } + + if (WorldState.Actors.FirstOrDefault(x => x.IsTargetable && !x.IsAlly && x.OID != (uint)OID.Boss && !SleepProof(x)) is Actor e) + hints.ActionsToExecute.Push(ActionID.MakeSpell(WHM.AID.Repose), e, ActionQueue.Priority.VeryHigh); + } +} + +class SophrosyneStates : StateMachineBuilder +{ + public SophrosyneStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.Enemies(OID.BossP2).Any(x => x.IsTargetable) || module.WorldState.CurrentCFCID != 673; + TrivialPhase(1) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.Enemies(OID.BossP2).All(x => x.IsDeadOrDestroyed) || module.WorldState.CurrentCFCID != 673; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68808, NameID = 8777)] +public class Sophrosyne(WorldState ws, Actor primary) : BossModule(ws, primary, new(-651.8f, -127.25f), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs b/BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs new file mode 100644 index 0000000000..d332164bac --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs @@ -0,0 +1,79 @@ +namespace BossMod.Shadowbringers.Quest.ToHaveLovedAndLost; + +public enum OID : uint +{ + Boss = 0x2927, + Helper = 0x233C, +} + +public enum AID : uint +{ + Bloodstain = 4747, // Boss->self, 2.5s cast, range 5 circle + BrandOfSin = 16132, // Boss->self, 3.0s cast, range 80 circle + BladeOfJustice = 16134, // Boss->players, 8.0s cast, range 6 circle + SanctifiedHolyII = 17427, // Boss->self, 3.0s cast, range 5 circle + SanctifiedHolyIII = 17430, // 2AB3/2AB2->location, 3.0s cast, range 6 circle + HereticsFork = 17552, // 2779->self, 4.0s cast, range 40 width 6 cross + SpiritsWithout = 4746, // Boss->self, 2.5s cast, range 3+R width 3 rect + SeraphBlade = 16131, // Boss->self, 5.0s cast, range 40+R ?-degree cone + Fracture = 15576, // 2612->location, 5.0s cast, range 3 circle + Fracture1 = 13208, // 2612->location, 5.0s cast, range 3 circle + Fracture2 = 13207, // 2612->location, 5.0s cast, range 3 circle + Fracture3 = 15374, // 2612->location, 5.0s cast, range 3 circle + Fracture4 = 16612, // 2612->location, 5.0s cast, range 3 circle + Fracture5 = 13209, // 2612->location, 5.0s cast, range 3 circle + HereticsQuoit = 17470, // 2968->self, 5.0s cast, range -15 donut + SanctifiedHoly1 = 17431, // 2AB3/2AB2->players/2928, 5.0s cast, range 6 circle +} + +class HereticsFork(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HereticsFork), new AOEShapeCross(40, 3)); +class SpiritsWithout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SpiritsWithout), new AOEShapeRect(3.5f, 1.5f)); +class SeraphBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SeraphBlade), new AOEShapeCone(40, 90.Degrees())); +class HereticsQuoit(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HereticsQuoit), new AOEShapeDonut(5, 15)); +class SanctifiedHoly(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SanctifiedHoly1), 6); + +class Fracture(BossModule module) : Components.GenericTowers(module) +{ + private readonly AID[] TowerCasts = [AID.Fracture, AID.Fracture1, AID.Fracture2, AID.Fracture3, AID.Fracture4, AID.Fracture5]; + + private bool IsTower(ActionID act) => TowerCasts.Contains((AID)act.ID); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (IsTower(spell.Action)) + Towers.Add(new(spell.LocXZ, 3, activation: Module.CastFinishAt(spell))); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (IsTower(spell.Action)) + Towers.RemoveAll(t => t.Position.AlmostEqual(spell.LocXZ, 1)); + } +} +class Bloodstain(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Bloodstain), new AOEShapeCircle(5)); +class BrandOfSin(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.BrandOfSin), 10); +class BladeOfJustice(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.BladeOfJustice), 6, minStackSize: 1); +class SanctifiedHolyII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedHolyII), new AOEShapeCircle(5)); +class SanctifiedHolyIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedHolyIII), 6); + +class DikaiosyneStates : StateMachineBuilder +{ + public DikaiosyneStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68784, NameID = 8922)] +public class Dikaiosyne(WorldState ws, Actor primary) : BossModule(ws, primary, new(-798.6f, -40.49f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs b/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs new file mode 100644 index 0000000000..cd20c4d00d --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs @@ -0,0 +1,139 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.VowsOfVirtueDeedsOfCruelty; + +public enum OID : uint +{ + Boss = 0x2C85, // R6.000, x1 + TerminusEstVisual = 0x2C98, // R1.000, x3 + BossHelper = 0x233C, // R0.500, x15, 523 type + SigniferPraetorianus = 0x2C9A, // R0.500, x0 (spawn during fight), the adds on the catwalk that just rain down Fire II + LembusPraetorianus = 0x2C99, // R2.400, x0 (spawn during fight), two large magitek ships + MagitekBit = 0x2C9C, // R0.600, x0 (spawn during fight) +} + +public enum AID : uint +{ + LoadData = 18786, // Boss->self, 3.0s cast, single-target + AutoAttack = 870, // Boss/LembusPraetorianus->player, no cast, single-target + MagitekRayRightArm = 18783, // Boss->self, 3.2s cast, range 45+R width 8 rect + MagitekRayLeftArm = 18784, // Boss->self, 3.2s cast, range 45+R width 8 rect + SystemError = 18785, // Boss->self, 1.0s cast, single-target + AngrySalamander = 18787, // Boss->self, 3.0s cast, range 40+R width 6 rect + FireII = 18959, // SigniferPraetorianus->location, 3.0s cast, range 5 circle + TerminusEstBossCast = 18788, // Boss->self, 3.0s cast, single-target + TerminusEstLocationHelper = 18889, // BossHelper->self, 4.0s cast, range 3 circle + TerminusEstVisual = 18789, // TerminusEstVisual->self, 1.0s cast, range 40+R width 4 rect + HorridRoar = 18779, // 2CC5->location, 2.0s cast, range 6 circle, this is your own attack. It spawns an aoe at the location of any enemy it initally hits + GarleanFire = 4007, // LembusPraetorianus->location, 3.0s cast, range 5 circle + MagitekBit = 18790, // Boss->self, no cast, single-target + MetalCutterCast = 18793, // Boss->self, 6.0s cast, single-target + MetalCutter = 18794, // BossHelper->self, 6.0s cast, range 30+R 20-degree cone + AtomicRayCast = 18795, // Boss->self, 6.0s cast, single-target + AtomicRay = 18796, // BossHelper->location, 6.0s cast, range 10 circle + MagitekRayBit = 18791, // MagitekBit->self, 6.0s cast, range 50+R width 2 rect + SelfDetonate = 18792, // MagitekBit->self, 7.0s cast, range 40+R circle, enrage if bits are not killed before cast +} + +class MagitekRayRightArm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRayRightArm), new AOEShapeRect(45, 4)); +class MagitekRayLeftArm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRayLeftArm), new AOEShapeRect(45, 4)); +class AngrySalamander(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AngrySalamander), new AOEShapeRect(40, 3)); +class TerminusEstRects(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + private static readonly AOEShapeRect _shape = new(40, 2); + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TerminusEstLocationHelper) + { + _aoes.AddRange( + [ + new(_shape, caster.Position, spell.Rotation, Module.CastFinishAt(spell)), + new(_shape, caster.Position, spell.Rotation - 90.Degrees(), Module.CastFinishAt(spell)), + new(_shape, caster.Position, spell.Rotation + 90.Degrees(), Module.CastFinishAt(spell)) + ]); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.TerminusEstVisual) + { + _aoes.Clear(); + ++NumCasts; + } + } +} +class TerminusEstCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TerminusEstLocationHelper), new AOEShapeCircle(3)); +class FireII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FireII), 5); +class GarleanFire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.GarleanFire), 5); +class MetalCutter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MetalCutter), new AOEShapeCone(30, 10.Degrees())); +class MagitekRayBits(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRayBit), new AOEShapeRect(50, 1)); +class AtomicRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AtomicRay), new AOEShapeCircle(10)); +class SelfDetonate(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SelfDetonate), "Enrage if bits are not killed before cast"); + +class EstinienAI(WorldState ws) : UnmanagedRotation(ws, 3) +{ + protected override void Exec(Actor? primaryTarget) + { + if (primaryTarget == null) + return; + + if (Hints.PotentialTargets.Any(x => (OID)x.Actor.OID is OID.SigniferPraetorianus or OID.MagitekBit)) + UseAction(Roleplay.AID.HorridRoar, Player); + + if (World.Party.LimitBreakCur == 10000) + UseAction(Roleplay.AID.DragonshadowDive, primaryTarget, 100); + + if (primaryTarget.OID == (uint)OID.Boss) + { + var dotRemaining = StatusDetails(primaryTarget, Roleplay.SID.StabWound, Player.InstanceID).Left; + if (dotRemaining < 2.3f) + UseAction(Roleplay.AID.Drachenlance, primaryTarget); + } + + UseAction(Roleplay.AID.AlaMorn, primaryTarget); + UseAction(Roleplay.AID.Stardiver, primaryTarget, -10); + } +} + +class AutoEstinien(BossModule module) : Components.RotationModule(module); + +class ArchUltimaStates : StateMachineBuilder +{ + public ArchUltimaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "croizat", GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69218, NameID = 9189)] +public class ArchUltima(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(20)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = (OID)h.Actor.OID switch + { + OID.MagitekBit => 2, + OID.LembusPraetorianus => 1, + _ => 0 + }; + } + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs new file mode 100644 index 0000000000..3b69ab9773 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs @@ -0,0 +1,29 @@ +namespace BossMod.Stormblood.Quest.ARequiemForHeroes; + +public enum OID : uint +{ + BossP1 = 0x268A, + BossP2 = 0x268C, + Helper = 0x233C, + AmeNoHabakiri = 0x2692, // R3.000, x0 (spawn during fight) + TheStorm = 0x2760, // R3.000, x0 (spawn during fight) + TheSwell = 0x275F, // R3.000, x0 (spawn during fight) + DarkAether = 0x2694, // R1.200, x0 (spawn during fight) +} + +public enum AID : uint +{ + FloodOfDarkness = 14808, // Helper->self, 3.5s cast, range 6 circle + VeinSplitter = 14839, // Boss->self, 4.0s cast, range 10 circle + LightlessSpark = 14838, // Boss->self, 4.0s cast, range 40+R 90-degree cone + LightlessSparkAdds = 14824, // 268D->self, 4.0s cast, range 40+R 90-degree cone + ArtOfTheSwell = 14812, // Boss->self, 4.0s cast, range 33 circle + TheSwellUnbound = 14813, // Helper->self, 8.0s cast, range 8-20 donut + ArtOfTheSword1 = 14819, // Helper->self, 4.0s cast, range 40+R width 6 rect + ArtOfTheSword2 = 14818, // Helper->self, 6.0s cast, range 40+R width 6 rect + ArtOfTheSword3 = 14820, // Helper->self, 2.0s cast, range 40+R width 6 rect + ArtOfTheStorm = 14814, // Boss->self, 4.0s cast, range 8 circle + TheStormUnboundCast = 14815, // Helper->self, 3.0s cast, range 5 circle + TheStormUnboundRepeat = 14816, // Helper->self, no cast, range 5 circle + EntropicFlame = 14833, // Helper->self, 4.0s cast, range 50+R width 8 rect +} diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs new file mode 100644 index 0000000000..78509c493d --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs @@ -0,0 +1,51 @@ +using BossMod.QuestBattle; + +namespace BossMod.Stormblood.Quest.ARequiemForHeroes; + +class AutoHien(WorldState ws) : UnmanagedRotation(ws, 3) +{ + protected override void Exec(Actor? primaryTarget) + { + if (primaryTarget == null) + return; + + Hints.GoalZones.Add(Hints.GoalSingleTarget(primaryTarget, 3)); + + var ajisai = StatusDetails(primaryTarget, Roleplay.SID.Ajisai, Player.InstanceID); + + switch (ComboAction) + { + case Roleplay.AID.Gofu: + UseAction(Roleplay.AID.Yagetsu, primaryTarget); + break; + + case Roleplay.AID.Kyokufu: + UseAction(Roleplay.AID.Gofu, primaryTarget); + break; + + default: + if (ajisai.Left < 5) + UseAction(Roleplay.AID.Ajisai, primaryTarget); + UseAction(Roleplay.AID.Kyokufu, primaryTarget); + break; + } + + if (Player.HPMP.CurHP < 5000) + UseAction(Roleplay.AID.SecondWind, Player, -10); + + UseAction(Roleplay.AID.HissatsuGyoten, primaryTarget, -10); + } +} + +class HienAI(BossModule module) : Components.RotationModule(module); + +public class ZenosP1States : StateMachineBuilder +{ + public ZenosP1States(BossModule module) : base(module) + { + TrivialPhase().ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68721, NameID = 6039, PrimaryActorOID = (uint)OID.BossP1)] +public class ZenosP1(WorldState ws, Actor primary) : BossModule(ws, primary, new(233, -93.25f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs new file mode 100644 index 0000000000..c42f8422a5 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs @@ -0,0 +1,89 @@ +namespace BossMod.Stormblood.Quest.ARequiemForHeroes; + +class StormUnbound(BossModule module) : Components.Exaflare(module, 5) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TheStormUnboundCast) + { + Lines.Add(new() + { + Next = caster.Position, + Advance = caster.Rotation.ToDirection() * 5, + NextExplosion = Module.CastFinishAt(spell), + TimeToMove = 1, + ExplosionsLeft = 4, + MaxShownExplosions = 2 + }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.TheStormUnboundCast or AID.TheStormUnboundRepeat) + { + foreach (var l in Lines.Where(l => l.Next.AlmostEqual(caster.Position, 1))) + AdvanceLine(l, caster.Position); + ++NumCasts; + } + } +} + +class LightlessSpark2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LightlessSparkAdds), new AOEShapeCone(40, 45.Degrees())); + +class ArtOfTheStorm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheStorm), new AOEShapeCircle(8)); +class EntropicFlame(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EntropicFlame), new AOEShapeRect(50, 4)); + +class FloodOfDarkness(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FloodOfDarkness), new AOEShapeCircle(6), maxCasts: 6); +class VeinSplitter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), new AOEShapeCircle(10)); +class LightlessSpark(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LightlessSpark), new AOEShapeCone(40, 45.Degrees())); +class SwellUnbound(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TheSwellUnbound), new AOEShapeDonut(8, 20)); +class Swell(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.ArtOfTheSwell), 8) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Casters.Count > 0) + hints.AddForbiddenZone(new AOEShapeDonut(8, 50), Arena.Center); + } +} +class ArtOfTheSword1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword1), new AOEShapeRect(40, 3)); +class ArtOfTheSword2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword2), new AOEShapeRect(40, 3)); +class ArtOfTheSword3(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword3), new AOEShapeRect(40, 3)); + +class DarkAether(BossModule module) : Components.GenericAOEs(module) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Module.Enemies(OID.DarkAether).Select(e => new AOEInstance(new AOEShapeCircle(1.5f), e.Position, e.Rotation)); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var c in ActiveAOEs(slot, actor)) + hints.AddForbiddenZone(new AOEShapeRect(3, 1.5f, 1.5f), c.Origin, c.Rotation, c.Activation); + } +} + +class Adds(BossModule module) : Components.AddsMulti(module, [(uint)OID.TheStorm, (uint)OID.TheSwell, (uint)OID.AmeNoHabakiri]); + +public class ZenosP2States : StateMachineBuilder +{ + public ZenosP2States(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68721, NameID = 6039, PrimaryActorOID = (uint)OID.BossP2)] +public class ZenosP2(WorldState ws, Actor primary) : BossModule(ws, primary, new(233, -93.25f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs b/BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs new file mode 100644 index 0000000000..a050938ed8 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs @@ -0,0 +1,114 @@ +namespace BossMod.Stormblood.Quest.AnArtForTheLiving; + +public enum OID : uint +{ + Boss = 0x1CBA, + Helper = 0x233C, + ExplosiveIndicator = 0x1CD7, // R0.500, x0 (spawn during fight) + AetherochemicalExplosive = 0x1CD5, // R1.000, x1 (spawn during fight) +} + +public enum AID : uint +{ + PiercingLaser = 8683, // Boss->self, 3.0s cast, range 30+R width 6 rect + NerveGas = 8707, // 1CD8->self, 3.0s cast, range 30+R 120-degree cone + NerveGasLeft = 8708, // FX1979->self, 3.0s cast, range 30+R 180-degree cone + NerveGasRight = 8709, // 1CD8->self, 3.0s cast, range 30+R 180-degree cone + W111TonzeSwing = 8697, // 1CD1->self, 4.0s cast, range 8+R circle + W11TonzeSwipe = 8699, // 1CD1->self, 3.0s cast, range 5+R ?-degree cone +} + +public enum SID : uint +{ + Invincibility = 325 +} + +class OneOneOneTonzeSwing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W111TonzeSwing), new AOEShapeCircle(12)); +class OneOneTonzeSwipe(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W11TonzeSwipe), new AOEShapeCone(9, 45.Degrees())); // may be the wrong angle + +class NerveGas1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NerveGas), new AOEShapeCone(35, 60.Degrees())); +class NerveGas2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NerveGasRight), new AOEShapeCone(35, 90.Degrees())); +class NerveGas3(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NerveGasLeft), new AOEShapeCone(35, 90.Degrees())); + +class PiercingLaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PiercingLaser), new AOEShapeRect(33.68f, 3)); + +class AetherochemicalExplosive(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor Actor, bool Primed, DateTime Activation)> Explosives = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Explosives.Where(e => !e.Actor.IsDead || !e.Primed).Select(e => new AOEInstance(new AOEShapeCircle(5), e.Actor.Position, Activation: e.Activation)); + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID is OID.ExplosiveIndicator) + { + Explosives.Add((actor, false, WorldState.CurrentTime.AddSeconds(3))); + } + + if ((OID)actor.OID is OID.AetherochemicalExplosive) + { + var slot = Explosives.FindIndex(e => e.Actor.Position.AlmostEqual(actor.Position, 1)); + if (slot >= 0) + Explosives[slot] = (actor, true, Explosives[slot].Activation); + else + Module.ReportError(this, $"found explosive {actor} with no matching telegraph"); + } + } + + public override void OnActorDestroyed(Actor actor) + { + if ((OID)actor.OID == OID.AetherochemicalExplosive) + Explosives.RemoveAll(e => e.Actor.Position.AlmostEqual(actor.Position, 1)); + } +} + +class Adds(BossModule module) : Components.AddsMulti(module, [0x1CB6, 0x1CD1, 0x1CD6, 0x1CD8]) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = e.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} + +class SummoningNodeStates : StateMachineBuilder +{ + public SummoningNodeStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68165, NameID = 6695)] +public class SummoningNode(WorldState ws, Actor primary) : BossModule(ws, primary, new(-111, -295), ArenaBounds) +{ + private static readonly List vertices = [ + new(-4.5f, 22.66f), + new(4.5f, 22.66f), + new(18f, 14.75f), + new(22.2f, 7.4f), + new(22.7f, 7.4f), + new(22.7f, -7.4f), + new(22.2f, -7.4f), + new(18.15f, -15.77f), + new(4.5f, -23.68f), + new(-4.5f, -23.68f), + new(-18.15f, -15.77f), + new(-22.2f, -7.4f), + new(-22.7f, -7.4f), + new(-22.7f, 6.4f), + new(-22.2f, 6.4f), + new(-18f, 14.75f) + ]; + + public static readonly ArenaBoundsCustom ArenaBounds = new(30, new(vertices)); +} diff --git a/BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs b/BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs new file mode 100644 index 0000000000..8faa744c35 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs @@ -0,0 +1,156 @@ +namespace BossMod.Stormblood.Quest.BestServedWithColdSteel; + +public enum OID : uint +{ + Boss = 0x1A52, // R2.100f, x1 + Grynewaht = 0x1A53, + Helper = 0x233C, +} + +public enum AID : uint +{ + CermetPile = 8117, // 1A52->self, 3.0fs cast, range 4$1fR width 6 rect + Firebomb = 8495, // Boss->location, 3.0fs cast, range 4 circle + OpenFire1 = 8121, // 19D9->location, 3.0fs cast, range 6 circle + AugmentedSuffering = 8492, // Boss->self, 3.5fs cast, range $1fR circle + AugmentedUprising = 8493, // Boss->self, 3.0fs cast, range $1fR 120-degree cone + SelfDetonate = 8122, // 1A56->self, no cast, range 6 circle + SelfDetonate1 = 9169, // Boss->self, 60.0s cast, range 100 circle +} + +public enum TetherID : uint +{ + Mine = 54, // 1A56->player +} + +class AugmentedUprising(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); +class AugmentedSuffering(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), new AOEShapeCircle(6.5f)); +class OpenFire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.OpenFire1), 6); + +class CermetPile(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CermetPile), new AOEShapeRect(42.1f, 3f)); +class Firebomb(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Firebomb), 4); + +class MagitekTurret(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.SelfDetonate)) +{ + class Mine(Actor source, Actor target, WPos sourcePosLastFrame, DateTime tethered) + { + public Actor source = source; + public Actor target = target; + public WPos sourcePosLastFrame = sourcePosLastFrame; + public DateTime tethered = tethered; + + public float DistanceLeft(WorldState ws) + { + var elapsed = (float)(ws.CurrentTime - tethered).TotalSeconds; + // approximation, turret starts moving after about 3.7s on average, but 4 is a nice round number + return Math.Clamp(12 - elapsed, 0, 8) * 3; + } + } + + private readonly List Mines = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var m in Mines.Where(m => m.target == actor)) + { + var mineToPlayer = m.target.Position - m.source.Position; + var projectedExplosion = mineToPlayer.Length() > m.DistanceLeft(WorldState) + ? (m.target.Position - m.source.Position).Normalized() * m.DistanceLeft(WorldState) + // offset danger zone slightly toward mine so that AI can dodge + // if centered on player it doesn't know which direction to go + : mineToPlayer * 0.9f; + yield return new AOEInstance(new AOEShapeCircle(6), m.source.Position + projectedExplosion, default, Activation: m.tethered.AddSeconds(12)); + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.Mine && WorldState.Actors.Find(tether.Target) is Actor target) + Mines.Add(new(source, target, source.Position, WorldState.CurrentTime)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.SelfDetonate) + Mines.RemoveAll(m => m.source == caster); + } + + public override void OnActorDestroyed(Actor actor) + { + Mines.RemoveAll(m => m.source == actor); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var m in Mines.Where(m => m.target == pc)) + Arena.AddLine(m.source.Position, pc.Position, ArenaColor.Danger); + } +} + +class MagitekSelfDetonate(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.SelfDetonate1)) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + NumCasts++; + } +} + +class MagitekVanguardIPrototypeStates : StateMachineBuilder +{ + private readonly MagitekVanguardIPrototype _module; + + private float BossHPRatio => (float)_module.PrimaryActor.HPMP.CurHP / _module.PrimaryActor.HPMP.MaxHP; + + public MagitekVanguardIPrototypeStates(MagitekVanguardIPrototype module) : base(module) + { + _module = module; + DeathPhase(0, P1); + } + + private void P1(uint id) + { + Condition(id, 300, () => BossHPRatio < 0.9f, "Adds 1") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + + Condition(id + 2, 300, () => BossHPRatio < 0.75f, "Adds 2"); + Condition(id + 4, 300, () => BossHPRatio < 0.65f, "Turrets").ActivateOnEnter(); + Condition(id + 6, 300, () => BossHPRatio < 0.55f, "Adds 3"); + Condition(id + 8, 300, () => BossHPRatio < 0.4f, "Cutscene").ActivateOnEnter(); + ComponentCondition(id + 10, 18, m => m.NumCasts > 0); + CastEnd(id + 12, 60, "Self-detonate").SetHint(StateMachine.StateHint.DowntimeStart); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67989, NameID = 5650)] +public class MagitekVanguardIPrototype(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, CustomBounds) +{ + private static readonly List vertices = [ + new(-487.40f, -230.79f), new(-487.56f, -188.08f), new(-478.75f, -181.25f), new(-439.37f, -183.46f), new(-457.85f, -211.90f), new(-461.13f, -228.75f) + ]; + + public static readonly WPos ArenaCenter = new(-465.40f, -202.09f); + public static readonly ArenaBoundsCustom CustomBounds = new(30, new(vertices.Select(v => v - ArenaCenter.ToWDir()))); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + { + if (h.Actor.OID == 0x1A52) + h.Priority = 1; + else if (h.Actor.TargetID == actor.InstanceID) + h.Priority = 2; + else + h.Priority = 0; + } + } +} diff --git a/BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs b/BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs new file mode 100644 index 0000000000..e78d012603 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs @@ -0,0 +1,42 @@ +namespace BossMod.Stormblood.Quest; +public enum OID : uint +{ + Boss = 0x1BED, + Helper = 0x233C, + ShamShinobi = 0x1BE8, // R0.500, x4 (spawn during fight) + AdjunctOstyrgreinHelper = 0x1BEB, // R0.500, x0 (spawn during fight), Helper type + AdjunctOstyrgrein = 0x1BEA, // R0.500, x0 (spawn during fight) + Vanara = 0x1BE9, // R3.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + ScytheTail = 8407, // Vanara->self, 5.0s cast, range 4+R circle + Butcher = 8405, // Vanara->self, 5.0s cast, range 6+R ?-degree cone + TenkaGoken = 8408, // AdjunctOstyrgrein->self, 5.0s cast, range 8+R 120-degree cone + Bombslinger1 = 8411, // AdjunctOstyrgreinHelper->location, 3.0s cast, range 6 circle +} + +class ScytheTail(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ScytheTail), new AOEShapeCircle(7)); +class Butcher(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Butcher), new AOEShapeCone(9, 45.Degrees())); +class TenkaGoken(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TenkaGoken), new AOEShapeCone(8.5f, 60.Degrees())); +class Bombslinger(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Bombslinger1), 6); + +class GurumiBorlumiStates : StateMachineBuilder +{ + public GurumiBorlumiStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68098, NameID = 6289)] +public class GurumiBorlumi(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 15.8f), new ArenaBoundsRect(8, 7.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Stormblood/Quest/DragonSound.cs b/BossMod/Modules/Stormblood/Quest/DragonSound.cs new file mode 100644 index 0000000000..80b77ec816 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/DragonSound.cs @@ -0,0 +1,72 @@ +namespace BossMod.Stormblood.Quest.DragonSound; + +public enum OID : uint +{ + Boss = 0x1CDD, // R6.840, x1 + Faunehm = 0x18D6, // R0.500, x9 +} + +public enum AID : uint +{ + AbyssicBuster = 8929, // Boss->self, 2.0s cast, range 25+R 90-degree cone + Heavensfall1 = 8935, // 18D6->location, 2.0s cast, range 5 circle + DarkStar = 8931, // Boss->self, 2.0s cast, range 50+R circle +} + +public enum SID : uint +{ + Enervation = 1401, // Boss->1CDE/player, extra=0x0 +} + +class AbyssicBuster(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AbyssicBuster), new AOEShapeCone(31.84f, 45.Degrees())); +class Heavensfall(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Heavensfall1), 5); +class DarkStar(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DarkStar)); + +// scripted interaction, no idea if it's required to complete the duty but might as well do it +class Enervation(BossModule module) : BossComponent(module) +{ + private bool Active; + private Actor? OrnKhai; + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if (actor.OID == 0 && status.ID == (uint)SID.Enervation) + Active = true; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if (actor.OID == 0 && status.ID == (uint)SID.Enervation) + Active = false; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (!Active) + return; + + OrnKhai ??= WorldState.Actors.FirstOrDefault(x => x.OID == 0x1CDF); + if (OrnKhai == null) + return; + + hints.ActionsToExecute.Push(ActionID.MakeSpell(DRG.AID.ElusiveJump), actor, ActionQueue.Priority.Medium, facingAngle: -actor.AngleTo(OrnKhai)); + + hints.GoalZones.Add(p => p.InCircle(OrnKhai.Position, 3) ? 100 : 0); + } +} + +class FaunehmStates : StateMachineBuilder +{ + public FaunehmStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68450, NameID = 6347)] +public class Faunehm(WorldState ws, Actor primary) : BossModule(ws, primary, new(4, 248.5f), new ArenaBoundsCircle(25)); + diff --git a/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs b/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs new file mode 100644 index 0000000000..294ed5aa69 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs @@ -0,0 +1,38 @@ +using BossMod.QuestBattle.Stormblood.MSQ; + +namespace BossMod.Stormblood.Quest.EmissaryOfTheDawn; + +public enum OID : uint +{ + Boss = 0x234B, + Helper = 0x233C, +} + +class AlphiAI(BossModule module) : Components.RotationModule(module); + +class LB(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (WorldState.Actors.Any(x => x.OID == 0x2340 && x.FindStatus(1497) != null)) + hints.ActionsToExecute.Push(ActionID.MakeSpell(Roleplay.AID.Starstorm), null, ActionQueue.Priority.VeryHigh, targetPos: new Vector3(Arena.Center.X, 0, Arena.Center.Z)); + } +} + +class HostileSkyArmorStates : StateMachineBuilder +{ + public HostileSkyArmorStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.WorldState.CurrentCFCID != 582; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68612, NameID = 7257)] +public class HostileSkyArmor(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs b/BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs new file mode 100644 index 0000000000..67963a398a --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs @@ -0,0 +1,70 @@ +namespace BossMod.Stormblood.Quest.HisForgottenHome; +public enum OID : uint +{ + Boss = 0x213A, + Helper = 0x233C, + SoftshellOfTheRed = 0x213B, // R1.600, x4 (spawn during fight) + SoftshellOfTheRed1 = 0x213C, // R1.600, x0 (spawn during fight) + SoftshellOfTheRed2 = 0x213D, // R1.600, x0 (spawn during fight) +} + +public enum AID : uint +{ + Kasaya = 8585, // SoftshellOfTheRed->self, 2.5s cast, range 6+R 120-degree cone + WaterIII = 5831, // Boss->location, 3.0s cast, range 8 circle + BlizzardIII = 10874, // Boss->location, 3.0s cast, range 5 circle +} + +class Kasaya(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Kasaya), new AOEShapeCone(7.6f, 60.Degrees())); +class WaterIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.WaterIII), 8); + +class BlizzardIIIIcon(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(5), 26, centerAtTarget: true) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.BlizzardIII) + CurrentBaits.Clear(); + } + + public override void OnActorDestroyed(Actor actor) + { + if (actor == Module.PrimaryActor) + CurrentBaits.Clear(); + } +} +class BlizzardIIICast(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 6, ActionID.MakeSpell(AID.BlizzardIII), m => m.Enemies(0x1E8D9C).Where(x => x.EventState != 7), 0); + +class SlickshellCaptainStates : StateMachineBuilder +{ + public SlickshellCaptainStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.Raid.Player()?.IsDeadOrDestroyed ?? true; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68563, NameID = 6891)] +public class SlickshellCaptain(WorldState ws, Actor primary) : BossModule(ws, primary, BoundsCenter, CustomBounds) +{ + public static readonly WPos BoundsCenter = new(468.92f, 301.30f); + + private static readonly List vertices = [ + new(464.25f, 320.19f), new(455.65f, 313.35f), new(457.72f, 308.20f), new(445.00f, 292.92f), new(468.13f, 283.56f), new(495.55f, 299.63f), new(487.19f, 313.73f) + ]; + + public static readonly ArenaBoundsCustom CustomBounds = new(30, new(vertices.Select(v => v - BoundsCenter))); + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // attack anyone targeting isse + foreach (var h in hints.PotentialTargets) + h.Priority = WorldState.Actors.Find(h.Actor.TargetID)?.OID == 0x2138 ? 1 : 0; + } +} + diff --git a/BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs b/BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs new file mode 100644 index 0000000000..036208cf37 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs @@ -0,0 +1,80 @@ +namespace BossMod.Stormblood.Quest.HopeOnTheWaves; + +public enum OID : uint +{ + Boss = 0x21B1, + Helper = 0x233C, +} + +public enum AID : uint +{ + CermetPile = 9425, // Boss->self, 2.5s cast, range 40+R width 6 rect + SelfDetonate = 10928, // Boss->self, 30.0s cast, range 100 circle + CircleOfDeath = 9428, // 2115->self, 3.0s cast, range 6+R circle + W2TonzeMagitekMissile = 10929, // 2115->location, 3.0s cast, range 6 circle + SelfDetonate1 = 10930, // 21B6->self, 5.0s cast, range 6 circle + MagitekMissile1 = 10893, // 21B7->location, 10.0s cast, range 60 circle + AssaultCannon = 10823, // 21B5->self, 2.5s cast, range 75+R width 2 rect +} + +class AssaultCannon(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AssaultCannon), new AOEShapeRect(75, 1)); +class CircleOfDeath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CircleOfDeath), new AOEShapeCircle(10.24f)); +class TwoTonzeMagitekMissile(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.W2TonzeMagitekMissile), 6); +class MagitekMissileProximity(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekMissile1), 11.75f); +class CermetPile(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CermetPile), new AOEShapeRect(42, 3)); +class SelfDetonate(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SelfDetonate), "Kill before detonation!", true); +class MineSelfDetonate(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SelfDetonate1), new AOEShapeCircle(6)); + +class Adds(BossModule module) : BossComponent(module) +{ + private Actor? Alphinaud => WorldState.Actors.FirstOrDefault(a => a.OID == 0x21AC); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + WPos? lbCenter = Alphinaud?.CastInfo is { Action.ID: 10894 } castInfo + ? castInfo.LocXZ + : null; + + foreach (var e in hints.PotentialTargets) + { + if (lbCenter != null && e.Actor.OID == 0x2114) + { + e.ShouldBeTanked = true; + e.DesiredPosition = lbCenter.Value; + e.Priority = 5; + } + else if (e.Actor.CastInfo?.Action.ID == (uint)AID.SelfDetonate) + e.Priority = 5; + else + e.Priority = 0; + } + } +} + +class ImperialCenturionStates : StateMachineBuilder +{ + public ImperialCenturionStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.WorldState.CurrentCFCID != 472; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68560, NameID = 4148)] +public class ImperialCenturion(WorldState ws, Actor primary) : BossModule(ws, primary, new(473.25f, 751.75f), BoundsP2) +{ + public static readonly ArenaBoundsCustom BoundsP2 = new(30, new(CurveApprox.Ellipse(34, 21, 0.05f).Select(p => p.Rotate(140.Degrees())))); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Stormblood/Quest/Naadam.cs b/BossMod/Modules/Stormblood/Quest/Naadam.cs new file mode 100644 index 0000000000..0f2be8492c --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Naadam.cs @@ -0,0 +1,142 @@ +namespace BossMod.Stormblood.Quest.Naadam; + +public enum OID : uint +{ + Boss = 0x1B31, + Helper = 0x233C, + MagnaiTheOlder = 0x1B38, // R0.500, x0 (spawn during fight) + StellarChuluu = 0x1B3F, // R1.800, x0 (spawn during fight) + StellarChuluu1 = 0x1B40, // R1.800, x0 (spawn during fight) + Grynewaht = 0x1B3A, // R0.500, x0 (spawn during fight) + Ovoo = 0x1EA4E1 +} + +public enum AID : uint +{ + ViolentEarth = 8389, // MagnaiTheOlder1->location, 3.0s cast, range 6 circle + DispellingWind = 8394, // SaduHeavensflame->self, 3.0s cast, range 40+R width 8 rect + Epigraph = 8339, // 1A58->self, 3.0s cast, range 45+R width 8 rect + DiffractiveLaser = 9122, // ArmoredWeapon->location, 3.0s cast, range 5 circle + AugmentedSuffering = 8492, // Grynewaht->self, 3.5s cast, range 6+R circle + AugmentedUprising = 8493, // Grynewaht->self, 3.0s cast, range 8+R 120-degree cone +} + +public enum SID : uint +{ + EarthenAccord = 778 +} + +class DiffractiveLaser(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), 5); +class AugmentedSuffering(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), new AOEShapeCircle(6.5f)); +class AugmentedUprising(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); + +class ViolentEarth(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ViolentEarth), 6); +class DispellingWind(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DispellingWind), new AOEShapeRect(40.5f, 4)); +class Epigraph(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Epigraph), new AOEShapeRect(45, 4)); + +class DrawOvoo : BossComponent +{ + private Actor? Ovoo => WorldState.Actors.FirstOrDefault(o => o.OID == 0x1EA4E1); + + public DrawOvoo(BossModule module) : base(module) + { + KeepOnPhaseChange = true; + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actor(Ovoo, ArenaColor.Object, true); + } +} + +class ActivateOvoo(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.MountId == 117) + hints.WantDismount = true; + + var beingAttacked = false; + + foreach (var e in hints.PotentialTargets) + { + if (e.Actor.TargetID == actor.InstanceID) + beingAttacked = true; + else + e.Priority = AIHints.Enemy.PriorityForbidden; + } + + var ovoo = WorldState.Actors.FirstOrDefault(x => x.OID == 0x1EA4E1); + if (!beingAttacked && (ovoo?.IsTargetable ?? false)) + hints.InteractWithTarget = ovoo; + } +} + +class ProtectOvoo(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + { + if (e.Actor.FindStatus(SID.EarthenAccord) != null) + e.Priority = 5; + else if (e.Actor.OID == (uint)OID.StellarChuluu) + e.Priority = 1; + else + e.Priority = 0; + } + } +} + +class ProtectSadu(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var chuluu = WorldState.Actors.Where(x => (OID)x.OID == OID.StellarChuluu1).Select(x => x.InstanceID).ToList(); + + foreach (var e in hints.PotentialTargets) + { + if (chuluu.Contains(e.Actor.TargetID)) + e.Priority = 5; + else if ((OID)e.Actor.OID == OID.Grynewaht) + e.Priority = 1; + else + e.Priority = 0; + } + } +} + +class OvooStates : StateMachineBuilder +{ + public OvooStates(BossModule module) : base(module) + { + bool DutyEnd() => module.WorldState.CurrentCFCID != 246; + + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.WorldState.Actors.Any(x => x.OID == (uint)OID.MagnaiTheOlder && x.IsTargetable) || DutyEnd(); + TrivialPhase(1) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.WorldState.Actors.Any(x => x.OID == (uint)OID.Grynewaht && x.IsTargetable) || DutyEnd(); + TrivialPhase(2) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = DutyEnd; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68051, PrimaryActorOID = (uint)OID.Ovoo)] +public class Ovoo(WorldState ws, Actor primary) : BossModule(ws, primary, new(354, 296.5f), new ArenaBoundsCircle(20)) +{ + protected override bool CheckPull() => Raid.Player()?.Position.InCircle(PrimaryActor.Position, 15) ?? false; + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs b/BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs new file mode 100644 index 0000000000..23fff69301 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs @@ -0,0 +1,57 @@ +namespace BossMod.Stormblood.Quest.OurUnsungHeroes; + +public enum OID : uint +{ + Boss = 0x1CAF, // R2.700, x1 + FallenKuribu = 0x18D6, // R0.500, x5 + ShadowSprite = 0x1CB4, // R0.800, x0 (spawn during fight) +} + +public enum AID : uint +{ + Glory = 5604, // Boss->self, 3.0s cast, range 40+R 90-degree cone + CureIV = 8635, // Boss->self, 5.0s cast, range 40 circle + CureIII1 = 8636, // FallenKuribu->players/1CAD/1CAE, no cast, range 10 circle + CureV1 = 8637, // FallenKuribu->players, no cast, range 6 circle + DarkII = 4366, // ShadowSprite->self, 2.5s cast, range 50+R 60-degree cone +} + +public enum IconID : uint +{ + CureIII = 71, // player/1CAD/1CAE + Stack = 62, // player +} + +public enum SID : uint +{ + Invincibility = 325, // Boss->Boss, extra=0x0 +} + +class CureIV(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CureIV), new AOEShapeCircle(12)); +class Glory(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Glory), new AOEShapeCone(42.7f, 45.Degrees())); +class CureIII(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.CureIII, ActionID.MakeSpell(AID.CureIII1), 10, 5.15f); +class CureV(BossModule module) : Components.StackWithIcon(module, (uint)IconID.Stack, ActionID.MakeSpell(AID.CureV1), 6, 5.15f); +class DarkII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkII), new AOEShapeCone(50.8f, 30.Degrees())); + +class FallenKuribuStates : StateMachineBuilder +{ + public FallenKuribuStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 265, NameID = 6345)] +public class FallenKuribu(WorldState ws, Actor primary) : BossModule(ws, primary, new(232.3f, 407.7f), new ArenaBoundsCircle(20)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} diff --git a/BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs b/BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs new file mode 100644 index 0000000000..06db3c281d --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs @@ -0,0 +1,75 @@ +namespace BossMod.Stormblood.Quest.RaisingTheSword; + +public enum OID : uint +{ + Boss = 0x1B51, + Helper = 0x233C, + AldisSwordOfNald = 0x18D6, // R0.500, x10 + TaintedWindSprite = 0x1B52, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + ShudderingSwipeCast = 8136, // Boss->player, 3.0s cast, single-target + ShudderingSwipeAOE = 8137, // 18D6->self, 3.0s cast, range 60+R 30-degree cone + NaldsWhisper = 8141, // 18D6->self, 9.0s cast, range 4 circle + VictorySlash = 8134, // Boss->self, 3.0s cast, range 6+R 120-degree cone +} + +class VictorySlash(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VictorySlash), new AOEShapeCone(6.5f, 60.Degrees())); +class ShudderingSwipeCone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ShudderingSwipeAOE), new AOEShapeCone(60, 15.Degrees())); +class ShudderingSwipeKB(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.ShudderingSwipeCast), stopAtWall: true) +{ + private TheFourWinds? winds; + private readonly List Casters = []; + + public override IEnumerable Sources(int slot, Actor actor) => Casters.Select(c => new Source(c.Position, 10, Module.CastFinishAt(c.CastInfo), null, c.AngleTo(actor), Kind.DirForward)); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.ShudderingSwipeCast) + Casters.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.ShudderingSwipeCast) + Casters.Remove(caster); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + winds ??= Module.FindComponent(); + + var aoes = (winds?.Sources(Module) ?? []).Select(a => ShapeDistance.Circle(a.Position, 6)).ToList(); + if (aoes.Count == 0) + return; + + var windzone = ShapeDistance.Union(aoes); + if (Casters.FirstOrDefault() is Actor c) + hints.AddForbiddenZone(p => + { + var dir = c.DirectionTo(p); + var projected = p + dir * 10; + return windzone(projected); + }, Module.CastFinishAt(c.CastInfo)); + } +} +class NaldsWhisper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NaldsWhisper), new AOEShapeCircle(20)); +class TheFourWinds(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.TaintedWindSprite).Where(x => x.EventState != 7)); + +class AldisSwordOfNaldStates : StateMachineBuilder +{ + public AldisSwordOfNaldStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 270, NameID = 6311)] +public class AldisSwordOfNald(WorldState ws, Actor primary) : BossModule(ws, primary, new(-89.3f, 0), new ArenaBoundsCircle(20.5f)); diff --git a/BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs b/BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs new file mode 100644 index 0000000000..92d76ac875 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs @@ -0,0 +1,104 @@ +namespace BossMod.Stormblood.Quest.ReturnOfTheBull; +public enum OID : uint +{ + Boss = 0x1FD2, + Helper = 0x233C, + Lakshmi = 0x18D6, // R0.500, x12, Helper type + DreamingKshatriya = 0x1FDD, // R1.000, x0 (spawn during fight) + DreamingFighter = 0x1FDB, // R0.500, x0 (spawn during fight) + Aether = 0x1FD3, // R1.000, x0 (spawn during fight) + FordolaShield = 0x1EA080, +} + +public enum AID : uint +{ + BlissfulSpear = 9872, // Lakshmi->self, 11.0s cast, range 40 width 8 cross + BlissfulHammer = 9874, // Lakshmi->self, no cast, range 7 circle + ThePallOfLight = 9877, // Boss->players/1FD8, 5.0s cast, range 6 circle + ThePathOfLight = 9875, // Boss->self, 5.0s cast, range 40+R 120-degree cone +} + +class PathOfLight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ThePathOfLight), new AOEShapeCone(43.5f, 60.Degrees())); +class BlissfulSpear(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BlissfulSpear), new AOEShapeCross(40, 4)); +class ThePallOfLight(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ThePallOfLight), 6, 1); +class BlissfulHammer(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(7), 109, ActionID.MakeSpell(AID.BlissfulHammer), 12.15f, true); +class FordolaShield(BossModule module) : BossComponent(module) +{ + public Actor? Shield => WorldState.Actors.FirstOrDefault(a => (OID)a.OID == OID.FordolaShield); + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (Shield != null) + Arena.AddCircleFilled(Shield.Position, 4, ArenaColor.SafeFromAOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Shield != null) + hints.AddForbiddenZone(new AOEShapeDonut(4, 100), Shield.Position, default, WorldState.FutureTime(5)); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Shield != null && !actor.Position.InCircle(Shield.Position, 4)) + hints.Add("Go to safe zone!"); + } +} + +class Deflect(BossModule module) : BossComponent(module) +{ + public IEnumerable Spheres => Module.Enemies(OID.Aether).Where(x => !x.IsDeadOrDestroyed); + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Spheres, 0xFFFFA080); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var deflectAction = WorldState.Client.DutyActions[0].Action; + var deflectRadius = deflectAction.ID == 10006 ? 4 : 20; + + var closestSphere = Spheres.MaxBy(x => x.Position.Z); + if (closestSphere != null) + { + var optimalDeflectPosition = closestSphere.Position with { Z = closestSphere.Position.Z + 1 }; + + hints.GoalZones.Add(hints.GoalSingleTarget(optimalDeflectPosition, deflectRadius - 2, 10)); + + if (actor.DistanceToHitbox(closestSphere) < deflectRadius - 1) + hints.ActionsToExecute.Push(deflectAction, actor, ActionQueue.Priority.VeryHigh); + } + } +} + +class LakshmiStates : StateMachineBuilder +{ + public LakshmiStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68508, NameID = 6385)] +public class Lakshmi(WorldState ws, Actor primary) : BossModule(ws, primary, new(250, -353), new ArenaBoundsSquare(23)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = (OID)e.Actor.OID switch + { + OID.Boss => 1, + OID.Aether => -1, + _ => 0 + }; + } +} + diff --git a/BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs b/BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs new file mode 100644 index 0000000000..8410fa5c46 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs @@ -0,0 +1,123 @@ +namespace BossMod.Stormblood.Quest.RhalgrsBeacon; + +public enum OID : uint +{ + Boss = 0x1A88, + Helper = 0x233C, + TerminusEst = 0x1BCA, + MarkXLIIIArtilleryCannon = 0x1B4A, // R2.000, x3 + SkullsSpear = 0x1A8C, // R0.500, x3 + SkullsBlade = 0x1A8B, // R0.500, x3 + MagitekTurretII = 0x1BC7, // R0.600, x0 (spawn during fight) + ChoppingBlock = 0x1EA4D9, // R0.500, x0 (spawn during fight), voidzone event object +} + +public enum AID : uint +{ + TheOrder = 8370, // Boss->self, 3.0s cast, single-target + TerminusEst1 = 8337, // 1BCA->self, no cast, range 40+R width 4 rect + Gunblade = 8310, // Boss->player, 5.0s cast, single-target, 10y knockback + DiffractiveLaser = 8340, // 1BC7->self, 2.5s cast, range 18+R 60-degree cone + ChoppingBlock1 = 8346, // 1A57->location, 3.0s cast, range 5 circle +} + +class DiffractiveLaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), new AOEShapeCone(18.6f, 30.Degrees())); + +class TerminusEst(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.TheOrder)) +{ + private readonly List Termini = []; + private DateTime? CastFinish; + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Module.Enemies(OID.TerminusEst).Where(x => !x.IsDead), ArenaColor.Object, true); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var t in Termini) + yield return new AOEInstance(new AOEShapeRect(41f, 2), t.Position, t.Rotation, Activation: CastFinish ?? WorldState.FutureTime(10)); + } + + public override void OnActorCreated(Actor actor) + { + if (actor.OID == (uint)OID.TerminusEst) + Termini.Add(actor); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + CastFinish = Module.CastFinishAt(spell); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.TerminusEst1) + Termini.Remove(caster); + } +} + +class Gunblade(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.Gunblade), stopAtWall: true) +{ + public readonly List Casters = []; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var caster = Casters.FirstOrDefault(); + if (caster == null) + return; + + var voidzones = Module.Enemies(OID.ChoppingBlock).Where(x => x.EventState != 7).Select(v => ShapeDistance.Circle(v.Position, 5)).ToList(); + if (voidzones.Count == 0) + return; + + var combined = ShapeDistance.Union(voidzones); + + float projectedDist(WPos pos) + { + var direction = (pos - caster.Position).Normalized(); + var projected = pos + 10 * direction; + return combined(projected); + } + + hints.AddForbiddenZone(projectedDist, Module.CastFinishAt(caster.CastInfo)); + } + + public override IEnumerable Sources(int slot, Actor actor) + { + foreach (var c in Casters) + yield return new(c.Position, 10, Module.CastFinishAt(c.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + Casters.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + Casters.Remove(caster); + } +} + +class ChoppingBlock(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 5, ActionID.MakeSpell(AID.ChoppingBlock1), m => m.Enemies(OID.ChoppingBlock).Where(x => x.EventState != 7), 0); + +class FordolaRemLupisStates : StateMachineBuilder +{ + public FordolaRemLupisStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68064, NameID = 5953)] +public class FordolaRemLupis(WorldState ws, Actor primary) : BossModule(ws, primary, new(-195.25f, 147.5f), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs b/BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs new file mode 100644 index 0000000000..a25f292749 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs @@ -0,0 +1,76 @@ +namespace BossMod.Stormblood.Quest.TheBattleOnBekko; + +public enum OID : uint +{ + Boss = 0x1BF8, + Helper = 0x233C, + UgetsuSlayerOfAThousandSouls = 0x1BF9, // R0.500, x20, Helper type + Voidzone = 0x1E8EA9, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + HissatsuKyuten = 8433, // Boss->self, 3.0s cast, range 5+R circle + TenkaGoken = 9145, // Boss->self, 3.0s cast, range 8+R 120-degree cone + ShinGetsubaku = 8437, // 1BF9->location, 3.0s cast, range 6 circle + MijinGiri = 8435, // 1BF9->self, 2.5s cast, range 80+R width 10 rect + Ugetsuzan = 8439, // 1BF9->self, 2.5s cast, range -7 donut + Ugetsuzan2 = 8440, // 1BF9->self, 2.5s cast, range -12 donut + Ugetsuzan3 = 8441, // 1BF9->self, 2.5s cast, range -17 donut + KuruiYukikaze = 8446, // UgetsuSlayerOfAThousandSouls->self, 2.5s cast, range 44+R width 4 rect + KuruiGekko1 = 8447, // UgetsuSlayerOfAThousandSouls->self, 2.0s cast, range 30 circle + KuruiKasha1 = 8448, // UgetsuSlayerOfAThousandSouls->self, 2.5s cast, range 8+R ?-degree cone + Ugetsuzan4 = 8442, // UgetsuSlayerOfAThousandSouls->self, 2.5s cast, range -22 donut +} + +class KuruiGekko(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.KuruiGekko1)); +class KuruiKasha(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KuruiKasha1), new AOEShapeDonutSector(4.5f, 8.5f, 45.Degrees())); +class KuruiYukikaze(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KuruiYukikaze), new AOEShapeRect(44, 2), 8); +class HissatsuKyuten(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HissatsuKyuten), new AOEShapeCircle(5.5f)); +class TenkaGoken(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TenkaGoken), new AOEShapeCone(8.5f, 60.Degrees())); +class ShinGetsubaku(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ShinGetsubaku), 6); +class ShinGetsubakuVoidzone(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.Voidzone).Where(e => e.EventState != 7)); +class MijinGiri(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MijinGiri), new AOEShapeRect(80, 5, 2)); +class Ugetsuzan(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeDonutSector(2, 7, 90.Degrees()), new AOEShapeDonutSector(7, 12, 90.Degrees()), new AOEShapeDonutSector(12, 17, 90.Degrees())]) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Ugetsuzan) + AddSequence(caster.Position - caster.Rotation.ToDirection() * 4, Module.CastFinishAt(spell), caster.Rotation); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var idx = (AID)spell.Action.ID switch + { + AID.Ugetsuzan => 0, + AID.Ugetsuzan2 => 1, + AID.Ugetsuzan3 => 2, + AID.Ugetsuzan4 => 3, + _ => -1 + }; + AdvanceSequence(idx, caster.Position - caster.Rotation.ToDirection() * 4, WorldState.FutureTime(2.5f), caster.Rotation); + } +} + +class UgetsuSlayerOfAThousandSoulsStates : StateMachineBuilder +{ + public UgetsuSlayerOfAThousandSoulsStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68106, NameID = 6096)] +public class UgetsuSlayerOfAThousandSouls(WorldState ws, Actor primary) : BossModule(ws, primary, new(808.8f, 69.5f), new ArenaBoundsSquare(14)); + diff --git a/BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs b/BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs new file mode 100644 index 0000000000..49774827d1 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs @@ -0,0 +1,74 @@ +namespace BossMod.Stormblood.Quest.TheFaceOfTrueEvil; + +public enum OID : uint +{ + Boss = 0x1BEE, + Helper = 0x233C, + Musosai = 0x1BEF, // R0.500, x12, Helper type + Musosai1 = 0x1BF0, // R1.000, x0 (spawn during fight) + ViolentWind = 0x1BF1, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + HissatsuTo1 = 8415, // 1BEF->self, 3.0s cast, range 44+R width 4 rect + HissatsuKyuten = 8412, // Boss->self, 3.0s cast, range 5+R circle + Arashi = 8418, // Boss->self, 4.0s cast, single-target + Arashi1 = 8419, // 1BF0->self, no cast, range 4 circle + HissatsuKiku1 = 8417, // Musosai->self, 4.0s cast, range 44+R width 4 rect + Maiogi1 = 8421, // Musosai->self, 4.0s cast, range 80+R ?-degree cone + Musojin = 8422, // Boss->self, 25.0s cast, single-target + ArashiNoKiku = 8643, // Boss->self, 3.0s cast, single-target + ArashiNoMaiogi = 8642, // Boss->self, 3.0s cast, single-target +} + +class Musojin(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Musojin)); +class HissatsuKiku(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HissatsuKiku1), new AOEShapeRect(44.5f, 2)); +class Maiogi(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Maiogi1), new AOEShapeCone(80, 25.Degrees())); +class HissatsuTo(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HissatsuTo1), new AOEShapeRect(44.5f, 2)); +class HissatsuKyuten(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HissatsuKyuten), new AOEShapeCircle(5.5f)); +class Arashi(BossModule module) : Components.GenericAOEs(module) +{ + private DateTime? Activation; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Activation == null) + yield break; + + foreach (var e in Module.Enemies(OID.Musosai1)) + yield return new AOEInstance(new AOEShapeCircle(4), e.Position, default, Activation.Value); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.Arashi or AID.ArashiNoKiku or AID.ArashiNoMaiogi) + Activation = Module.CastFinishAt(spell); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.Arashi1) + Activation = null; + } +} +class ViolentWind(BossModule module) : Components.Adds(module, (uint)OID.ViolentWind); + +class MusosaiStates : StateMachineBuilder +{ + public MusosaiStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68101, NameID = 6111)] +public class Musosai(WorldState ws, Actor primary) : BossModule(ws, primary, new(-217.27f, -158.31f), new ArenaBoundsSquare(15)); + diff --git a/BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs b/BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs new file mode 100644 index 0000000000..4d392dfffc --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs @@ -0,0 +1,47 @@ +namespace BossMod.Stormblood.Quest.TheMeasureOfHisReach; + +public enum OID : uint +{ + Boss = 0x1C48, + Helper = 0x233C, + Whitefang = 0x1C5A +} + +public enum AID : uint +{ + HowlingIcewind = 8397, // 1C4F->self, 2.5s cast, range 44+R width 4 rect + Dragonspirit = 8450, // 1C5A/1C5B->self, 3.0s cast, range 6+R circle + HowlingMoonlight = 8398, // 1C59->self, 7.0s cast, range 22+R circle + HowlingBloomshower = 8399, // 1C4F->self, 2.5s cast, range 8+R ?-degree cone +} + +class Moonlight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HowlingMoonlight), new AOEShapeCircle(10)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + // hits everyone (proximity damage) + foreach (var c in Casters) + hints.PredictedDamage.Add((Raid.WithSlot().Mask(), Module.CastFinishAt(c.CastInfo))); + } +} +class Icewind(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HowlingIcewind), new AOEShapeRect(44, 2)); +class Dragonspirit(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Dragonspirit), new AOEShapeCircle(7.5f)); +class Bloomshower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HowlingBloomshower), new AOEShapeDonutSector(4, 8, 45.Degrees())); + +class HakuroWhitefangStates : StateMachineBuilder +{ + public HakuroWhitefangStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68088, NameID = 5975)] +public class HakuroWhitefang(WorldState ws, Actor primary) : BossModule(ws, primary, new(504, -133), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs b/BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs new file mode 100644 index 0000000000..0bc2115ab8 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs @@ -0,0 +1,57 @@ +namespace BossMod.Stormblood.Quest.TheOrphansAndTheBrokenBlade; + +public enum OID : uint +{ + Boss = 0x1C5E, + Helper = 0x233C, +} + +public enum AID : uint +{ + ShadowOfDeath1 = 8459, // 1C5F->location, 3.0s cast, range 5 circle + HeadsmansDelight = 8457, // Boss->1C5C, 5.0s cast, range 5 circle + SpiralHell = 8453, // 1C5F->self, 3.0s cast, range 40+R width 4 rect + HeadmansDelight = 9298, // 1C5F->player/1C5C, no cast, single-target +} + +class SpiralHell(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SpiralHell), new AOEShapeRect(40, 2)); +class HeadsmansDelight(BossModule module) : Components.GenericStackSpread(module) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.HeadsmansDelight && WorldState.Actors.Find(spell.TargetID) is Actor tar) + Stacks.Add(new(tar, 5, activation: Module.CastFinishAt(spell))); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.HeadmansDelight) + Stacks.Clear(); + } +} +class ShadowOfDeath(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ShadowOfDeath1), 5); +class DarkChain(BossModule module) : Components.Adds(module, 0x1C60) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + hints.PrioritizeTargetsByOID(0x1C60, 5); + } +} + +class OmpagneDeepblackStates : StateMachineBuilder +{ + public OmpagneDeepblackStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68453, NameID = 6300)] +public class OmpagneDeepblack(WorldState ws, Actor primary) : BossModule(ws, primary, new(-166.8f, 290), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs b/BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs new file mode 100644 index 0000000000..ce71d1b239 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs @@ -0,0 +1,77 @@ +namespace BossMod.Stormblood.Quest.ThePowerToProtect; + +public enum OID : uint +{ + Boss = 0x1BCB, // R5.400, x1 + CorpseBrigadeKnuckledancer = 0x1C0C, // R0.500, x2 (spawn during fight) + CorpseBrigadeBowdancer = 0x1C0D, // R0.500, x2 (spawn during fight) + HeweraldIronaxe = 0x1C01, // R0.500, x1 + CorpseBrigadeFiredancer = 0x1C00, // R0.500, x0 (spawn during fight) + CorpseBrigadeBowdancer1 = 0x1BFF, // R0.500, x0 (spawn during fight) + CorpseBrigadeKnuckledancer1 = 0x1BFE, // R0.500, x0 (spawn during fight) + CorpseBrigadeBarber = 0x1BFD, // R0.500, x0 (spawn during fight) + SalvagedSlasher = 0x1C1F, // R1.050, x0 (spawn during fight) + CorpseBrigadeVanguard = 0x1C02, // R2.000, x0 (spawn during fight) + FireII = 0x1EA4C6, +} + +public enum AID : uint +{ + IronTempest = 1003, // HeweraldIronaxe->self, 3.5s cast, range 5+R circle + FireII = 2175, // CorpseBrigadeFiredancer->location, 2.5s cast, range 5 circle + Overpower = 720, // HeweraldIronaxe->self, 2.5s cast, range 6+R 90-degree cone + Rive = 1135, // HeweraldIronaxe->self, 2.5s cast, range 30+R width 2 rect + DiffractiveLaser = 8348, // Boss->location, 4.0s cast, range 5 circle +} + +public enum SID : uint +{ + ExtremeCaution = 1269, // Boss->player, extra=0x0 + +} + +class ExtremeCaution(BossModule module) : Components.StayMove(module) +{ + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.ExtremeCaution && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + PlayerStates[slot] = new(Requirement.Stay, status.ExpireAt); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.ExtremeCaution && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + PlayerStates[slot] = default; + } +} +class IronTempest(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IronTempest), new AOEShapeCircle(5.5f)); +class FireII(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 5, ActionID.MakeSpell(AID.FireII), m => m.Enemies(OID.FireII).Where(x => x.EventState != 7), 0); +class Overpower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6.5f, 45.Degrees())); +class Rive(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Rive), new AOEShapeRect(30.5f, 1)); +class DiffractiveLaser(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), 5); + +class IoStates : StateMachineBuilder +{ + public IoStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67966, NameID = 5667)] +public class Io(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, B) +{ + public static readonly WPos ArenaCenter = new(76.28f, -659.47f); + public static readonly WPos[] Corners = [new(101.93f, -666.63f), new(94.49f, -639.63f), new(50.64f, -652.38f), new(57.58f, -679.32f)]; + + public static readonly ArenaBoundsCustom B = new(25, new(Corners.Select(c => c - ArenaCenter))); + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Stormblood/Quest/TheResonant.cs b/BossMod/Modules/Stormblood/Quest/TheResonant.cs new file mode 100644 index 0000000000..713d29fdec --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheResonant.cs @@ -0,0 +1,86 @@ +namespace BossMod.Stormblood.Quest.TheResonant; + +public enum OID : uint +{ + Boss = 0x1B7D, + Helper = 0x233C, + FordolaRemLupis = 0x18D6, // R0.500, x4, Helper type + MarkXLIIIArtilleryCannon = 0x1B7E, // R0.600, x0 (spawn during fight) + FordolaRemLupis1 = 0x1BCA, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + MagitekRay = 9104, // 1B7E->self, 2.5s cast, range 45+R width 2 rect + ChoppingBlock1 = 9110, // 18D6->location, 3.0s cast, range 5 circle + TheOrder = 9106, // Boss->self, 5.0s cast, single-target + TerminusEst1 = 9108, // FordolaRemLupis1->self, no cast, range 40+R width 4 rect + Skullbreaker1 = 9112, // FordolaRemLupis->self, 6.0s cast, range 40 circle +} + +public enum SID : uint +{ + Resonant = 780, +} + +class Skullbreaker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Skullbreaker1), new AOEShapeCircle(12)); + +class TerminusEst(BossModule module) : Components.GenericAOEs(module) +{ + private DateTime? Activation; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Activation == null) + yield break; + + var casters = Module.Enemies(0x1BCA).Where(e => e.Position.AlmostEqual(Arena.Center, 0.5f)); + foreach (var c in casters) + yield return new AOEInstance(new AOEShapeRect(41, 2), c.Position, c.Rotation, Activation: Activation.Value); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.TheOrder) + Activation = Module.CastFinishAt(spell).AddSeconds(0.8f); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (int)AID.TerminusEst1) + Activation = null; + } +} +class MagitekRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRay), new AOEShapeRect(45.6f, 1)); +class ChoppingBlock(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ChoppingBlock1), 5); + +class Siphon(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + { + if (h.Actor.FindStatus(SID.Resonant) != null) + { + h.Priority = AIHints.Enemy.PriorityForbidden; + hints.ActionsToExecute.Push(WorldState.Client.DutyActions[0].Action, h.Actor, ActionQueue.Priority.ManualEmergency); // use emergency mode to bypass forbidden state - duty action is the only thing we can use on fordola without being stunned + } + } + } +} + +public class FordolaRemLupisStates : StateMachineBuilder +{ + public FordolaRemLupisStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68086, NameID = 6104)] +public class FordolaRemLupis(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsSquare(19.5f)); diff --git a/BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs b/BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs new file mode 100644 index 0000000000..3e51ac891c --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs @@ -0,0 +1,90 @@ +namespace BossMod.Stormblood.Quest.TheTimeBetweenTheSeconds; + +public enum OID : uint +{ + Boss = 0x1A36, + Helper = 0x233C, + ZenosYaeGalvus = 0x1CEE, // R0.500, x9 + DomanSignifer = 0x1A3A, // R0.500, x3 + DomanHoplomachus = 0x1A39, // R0.500, x2 + ZenosYaeGalvus1 = 0x1EBC, // R0.920, x1 + DarkReflection = 0x1A37, // R0.920, x2 + LightlessFlame = 0x1CED, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + VeinSplitter = 8987, // 1A36->self, 3.5s cast, range 10 circle + Concentrativity = 8986, // 1A36->self, 3.0s cast, range 80 circle + LightlessFlame = 8988, // 1CED->self, 1.0s cast, range 10+R circle + LightlessSpark = 8985, // 1A36->self, 3.0s cast, range 40+R 90-degree cone + ArtOfTheSword1 = 8993, // 1CEE->self, 3.0s cast, range 40+R width 6 rect +} + +class ArtOfTheSword(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword1), new AOEShapeRect(41, 3)); +class VeinSplitter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), new AOEShapeCircle(10)); +class Concentrativity(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Concentrativity)); +class LightlessFlame(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.LightlessFlame)) +{ + private readonly Dictionary Flames = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Flames.Values.Select(p => new AOEInstance(new AOEShapeCircle(11), p.position, Activation: p.activation)); + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.LightlessFlame) + Flames.Add(actor.InstanceID, (actor.Position, WorldState.CurrentTime.AddSeconds(7))); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.LightlessFlame) + Flames[caster.InstanceID] = (caster.Position, Module.CastFinishAt(spell)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.LightlessFlame) + Flames.Remove(caster.InstanceID); + } +} +class LightlessSpark(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LightlessSpark), new AOEShapeCone(40.92f, 45.Degrees())); +class P2Boss(BossModule module) : BossComponent(module) +{ + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Module.Enemies(OID.ZenosYaeGalvus1), ArenaColor.Enemy); + Arena.Actors(Module.Enemies(OID.DarkReflection), ArenaColor.Enemy); + } +} + +class ZenosYaeGalvusStates : StateMachineBuilder +{ + public ZenosYaeGalvusStates(BossModule module) : base(module) + { + SimplePhase(0, id => BuildState(id, "P1 enrage", 1800), "P1") + .Raw.Update = () => !Module.PrimaryActor.IsTargetable; + SimplePhase(1, id => BuildState(id, "P2 enrage", 1800).ActivateOnEnter().ActivateOnEnter(), "P2") + .Raw.Update = () => !Module.Enemies(OID.ZenosYaeGalvus1).Any(); + } + + private State BuildState(uint id, string name, float duration = 10000) + { + return SimpleState(id, duration, name) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68034, NameID = 5954)] +public class ZenosYaeGalvus(WorldState ws, Actor primary) : BossModule(ws, primary, new(-247, 546.5f), CustomBounds) +{ + private static readonly List vertices = [ + new(-226.91f, 523.65f), new(-254.46f, 524.46f), new(-254.66f, 541.06f), new(-269.99f, 544.12f), new(-269.58f, 565.97f), new(-254.58f, 565.89f), new(-249.05f, 554.06f), new(-229.18f, 562.35f) +]; + + public static readonly ArenaBoundsCustom CustomBounds = new(25, new(vertices.Select(v => v - new WDir(-247, 546.5f)))); +} + diff --git a/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs b/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs new file mode 100644 index 0000000000..800ecc9340 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs @@ -0,0 +1,169 @@ +using BossMod.QuestBattle; +using RPID = BossMod.Roleplay.AID; + +namespace BossMod.Stormblood.Quest.TheWillOfTheMoon; + +public enum OID : uint +{ + Boss = 0x24A0, + Magnai = 0x24A1, + Helper = 0x233C, + KhunShavar = 0x252F, // R1.820, x0 (spawn during fight) + Hien = 0x24A3, + Daidukul = 0x24A2, // R0.500, x1 + TheScaleOfTheFather = 0x2532, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + DispellingWind = 13223, // Boss->self, 3.0s cast, range 40+R width 8 rect + Epigraph = 13225, // 252D->self, 3.0s cast, range 45+R width 8 rect + WhisperOfLivesPast = 13226, // 252E->self, 3.5s cast, range -12 donut + AncientBlizzard = 13227, // 252F->self, 3.0s cast, range 40+R 45-degree cone + Tornado = 13228, // 252F->location, 5.0s cast, range 6 circle + Epigraph2 = 13222, // 2530->self, 3.0s cast, range 45+R width 8 rect + FlatlandFury = 13244, // 2532->self, 17.0s cast, range 10 circle + FlatlandFuryEnrage = 13329, // 249F->self, 25.0s cast, range 10 circle + ViolentEarth = 13236, // 233C->location, 3.0s cast, range 6 circle + WindChisel = 13518, // 233C->self, 2.0s cast, range 34+R 20-degree cone + TranquilAnnihilation = 13233, // _Gen_DaidukulTheMirthful->24A3, 15.0s cast, single-target +} + +public enum SID : uint +{ + Invincibility = 775, // none->Boss, extra=0x0 +} + +class DispellingWind(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DispellingWind), new AOEShapeRect(40, 4)); +class Epigraph(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Epigraph), new AOEShapeRect(45, 4)); +class Whisper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WhisperOfLivesPast), new AOEShapeDonut(6, 12)); +class Blizzard(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AncientBlizzard), new AOEShapeCone(40, 22.5f.Degrees())); +class Tornado(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Tornado), 6); +class Epigraph1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Epigraph2), new AOEShapeRect(45, 4)); + +public class FlatlandFury(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FlatlandFury), new AOEShapeCircle(10)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // if all 9 adds are alive, instead of drawing forbidden zones (which would fill the whole arena), force AI to target nearest one to kill it + if (ActiveCasters.Count() == 9) + hints.ForcedTarget = ActiveCasters.MinBy(actor.DistanceToHitbox); + else + base.AddAIHints(slot, actor, assignment, hints); + } +} + +public class FlatlandFuryEnrage(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FlatlandFuryEnrage), new AOEShapeCircle(10)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (ActiveCasters.Count() < 9) + base.AddAIHints(slot, actor, assignment, hints); + } +} + +public class ViolentEarth(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ViolentEarth), 6); +public class WindChisel(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WindChisel), new AOEShapeCone(34, 10.Degrees())); + +public class Scales(BossModule module) : Components.Adds(module, (uint)OID.TheScaleOfTheFather); + +class AutoYshtola(WorldState ws) : UnmanagedRotation(ws, 25) +{ + private Actor Magnai => World.Actors.First(x => (OID)x.OID == OID.Magnai); + private Actor Hien => World.Actors.First(x => (OID)x.OID == OID.Hien); + private Actor Daidukul => World.Actors.First(x => (OID)x.OID == OID.Daidukul); + + protected override void Exec(Actor? primaryTarget) + { + var hienMinHP = Daidukul.CastInfo?.Action.ID == (uint)AID.TranquilAnnihilation + ? 28000 + : 10000; + + if (Hien.PredictedHPRaw < hienMinHP) + { + if (Player.DistanceToHitbox(Hien) > 25) + Hints.ForcedMovement = Player.DirectionTo(Hien).ToVec3(); + + UseAction(RPID.CureIISeventhDawn, Hien); + } + + if (Hien.CastInfo?.Action.ID == 13234) + Hints.GoalZones.Add(Hints.GoalSingleTarget(Hien.Position, 2, 5)); + + var aero = StatusDetails(Magnai, WHM.SID.Aero2, Player.InstanceID); + if (aero.Left < 4.6f) + UseAction(RPID.AeroIISeventhDawn, Magnai); + + UseAction(RPID.StoneIVSeventhDawn, primaryTarget); + + if (Player.HPMP.CurMP < 5000) + UseAction(RPID.Aetherwell, Player); + } +} + +class YshtolaAI(BossModule module) : Components.RotationModule(module); + +class P1Hints(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + { + if (e.Actor.FindStatus(SID.Invincibility) != null) + e.Priority = AIHints.Enemy.PriorityInvincible; + + // they do very little damage and sadu will raise them after a short delay, no point in attacking + if ((OID)e.Actor.OID == OID.KhunShavar) + e.Priority = AIHints.Enemy.PriorityPointless; + } + } +} + +class P2Hints(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + { + e.Priority = e.Actor.OID == (uint)OID.Magnai ? 1 : 0; + } + } +} + +class SaduHeavensflameStates : StateMachineBuilder +{ + public SaduHeavensflameStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.Enemies(OID.Magnai).Any(); + TrivialPhase(1) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .OnEnter(() => + { + Module.Arena.Center = new(-186.5f, 550.5f); + }) + .Raw.Update = () => Module.Raid.Player()?.IsDeadOrDestroyed ?? true; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68683, NameID = 6152)] +public class SaduHeavensflame(WorldState ws, Actor primary) : BossModule(ws, primary, new(-223, 519), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs b/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs new file mode 100644 index 0000000000..d895bae6f6 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs @@ -0,0 +1,119 @@ +namespace BossMod.Stormblood.Quest.TortoiseInTime; + +public enum OID : uint +{ + Boss = 0x2339, + Helper = 0x233C, + Soroban = 0x2351, // R0.500, x8 + MonkeyMagick = 0x23C2, // R1.000, x0 (spawn during fight) + Font = 0x233B, // R4.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + Eddy1 = 11511, // 2351->location, 3.0s cast, range 6 circle + GreatFlood1 = 11513, // 2351->self, no cast, range 60 circle + SpiritBurst = 11706, // 23C2->self, 1.0s cast, range 6 circle + WaterDrop = 11301, // 2351->234F, 8.0s cast, range 6 circle + Whitewater1 = 11521, // 2351->self, 3.0s cast, range 40+R width 7 rect + Upwell = 11515, // 233B->self, 3.0s cast, range 37+R ?-degree cone +} + +class Whitewater(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Whitewater1), new AOEShapeRect(40.5f, 3.5f)); +class Upwell(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Upwell), new AOEShapeCone(41, 15.Degrees())); +class SpiritBurst(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SpiritBurst), new AOEShapeCircle(6)); +class WaterDrop(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.WaterDrop), 6); + +class ExplosiveTataru(BossModule module) : BossComponent(module) +{ + private readonly List Balls = []; + private Actor? Tataru = null; + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == 3) + { + Balls.Add(source); + Tataru ??= WorldState.Actors.Find(tether.Target); + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.SpiritBurst) + { + Balls.Remove(caster); + if (Balls.Count == 0) + Tataru = null; + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (Tataru != null) + Arena.AddCircle(Tataru.Position, 6, ArenaColor.Danger); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Tataru != null) + hints.AddForbiddenZone(ShapeDistance.Circle(Tataru.Position, 6)); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Tataru != null && actor.Position.InCircle(Tataru.Position, 6)) + hints.Add("GTFO from Tataru!"); + } +} + +class Eddy(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Eddy1), 6); + +class ShieldHint(BossModule module) : BossComponent(module) +{ + private const float Radius = 7; + private Actor? Shield; + + public override void OnActorEState(Actor actor, ushort state) + { + if (actor.OID == 0x1EA9C7 && state == 2) + Shield = actor; + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (Shield is Actor s) + Arena.ZoneCircle(s.Position, Radius, ArenaColor.SafeFromAOE); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.GreatFlood1) + Shield = null; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Shield is Actor s) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(s.Position, Radius), Module.CastFinishAt(Module.PrimaryActor.CastInfo)); + } +} + +class SorobanStates : StateMachineBuilder +{ + public SorobanStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68552, NameID = 7240)] +public class Soroban(WorldState ws, Actor primary) : BossModule(ws, primary, new(62, -372), new ArenaBoundsSquare(19)); + From d2537eb4f7e9b6c65cdaebcced282929197a8a36 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:26:17 -0500 Subject: [PATCH 17/33] oop --- .../Dawntrail/Quest/SomewhereOnlySheKnows.cs | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 BossMod/Modules/Dawntrail/Quest/SomewhereOnlySheKnows.cs diff --git a/BossMod/Modules/Dawntrail/Quest/SomewhereOnlySheKnows.cs b/BossMod/Modules/Dawntrail/Quest/SomewhereOnlySheKnows.cs deleted file mode 100644 index 3de3f4e56b..0000000000 --- a/BossMod/Modules/Dawntrail/Quest/SomewhereOnlySheKnows.cs +++ /dev/null @@ -1,230 +0,0 @@ -/* -namespace BossMod.Dawntrail.Quest.SomewhereOnlySheKnows; - -public enum OID : uint -{ - _Gen_SonOfTheKingdom = 0x4295, // R0.750, x? - _Gen_SonOfTheKingdom1 = 0x4294, // R0.750, x? - _Gen_TheWingedSteed = 0x4293, // R1.300, x? - _Gen_TheBirdOfPrey = 0x4297, // R1.960, x? - _Gen_FlightOfTheGriffin = 0x4296, // R9.200, x? - Boss = 0x4298, // R4.000, x0 (spawn during fight) - Helper = 0x233C, // R0.500, x0 (spawn during fight), Helper type - _Gen_AFlowerInTheSun = 0x4299, // R2.720, x0 (spawn during fight) -} - -public enum AID : uint -{ - _AutoAttack_Attack = 6498, // 4295->player, no cast, single-target - _AutoAttack_Attack1 = 6497, // 4294/4297/4296->player, no cast, single-target - _AutoAttack_Attack2 = 6499, // 4293->player, no cast, single-target - _Weaponskill_BurningBright = 37517, // 4293->self, 3.0s cast, range 47 width 6 rect - _Weaponskill_SwoopingFrenzy = 37519, // 4296->location, 4.0s cast, range 12 circle - _Weaponskill_Feathercut = 37522, // 4297->self, 3.0s cast, range 10 width 5 rect - _Weaponskill_FrigidPulse = 37520, // 4296->self, 5.0s cast, range 4-60 donut - _Weaponskill_EyeOfTheFierce = 37523, // 4297->self, 5.0s cast, range 40 circle - _Weaponskill_FervidPulse = 37521, // 4296->self, 5.0s cast, range 50 width 14 cross - _AutoAttack_ = 37542, // 4298->player, no cast, single-target - _Weaponskill_FlowerMotif = 37524, // 4298->self, 5.0s cast, single-target - _Weaponskill_BloodyCaress = 37527, // 4299->self, 5.0s cast, range 60 180-degree cone - _Weaponskill_ = 37541, // 4298->location, no cast, single-target - _Weaponskill_FloodInBlue = 37535, // 233C->self, 5.0s cast, range 50 width 10 rect - _Weaponskill_FloodInBlue1 = 37534, // 4298->self, 5.0s cast, single-target - _Weaponskill_FloodInBlue2 = 37536, // 233C->self, no cast, range 50 width 5 rect - _Weaponskill_BlazeInRed = 37539, // Boss->location, 6.0s cast, range 40 circle - _Weaponskill_ArborMotif = 37525, // Boss->self, 5.0s cast, single-target - _Weaponskill_TornadoInGreen = 37538, // Boss->self, 5.0s cast, range -40 donut - _Weaponskill_NineIvies = 37528, // 429A->self, 3.0s cast, single-target - _Weaponskill_NineIvies1 = 37529, // Helper->self, 3.0s cast, range 50 20-degree cone - _Weaponskill_1 = 39744, // 429A->self, no cast, single-target - _Weaponskill_SculptureCast = 37537, // Boss->self, 5.0s cast, range 45 circle - _Weaponskill_MountainMotif = 37526, // Boss->self, 5.0s cast, single-target - _Weaponskill_Earthquake = 37531, // Helper->self, 5.0s cast, range 10 circle - _Weaponskill_Earthquake1 = 37530, // 429B->self, 5.0s cast, single-target - _Weaponskill_FreezeInCyan = 37540, // Boss->self, 5.0s cast, range 40 45-degree cone - _Weaponskill_Earthquake2 = 37532, // Helper->self, 7.0s cast, range 10-20 donut - _Weaponskill_Earthquake3 = 37533, // Helper->self, 9.0s cast, range 20-30 donut -} - -class BurningBright(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_BurningBright), new AOEShapeRect(47, 3)); -class SwoopingFrenzy(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_SwoopingFrenzy), 12); -class Feathercut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Feathercut), new AOEShapeRect(10, 2.5f)); -class FrigidPulse(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_FrigidPulse), new AOEShapeDonut(11.9f, 60)); -class FervidPulse(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_FervidPulse), new AOEShapeCross(50, 7)); -class EyeOfTheFierce(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID._Weaponskill_EyeOfTheFierce)); -class BloodyCaress(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_BloodyCaress), new AOEShapeCone(60, 90.Degrees())) -{ - private DateTime? Predicted; - - public override IEnumerable ActiveAOEs(int slot, Actor actor) - { - if (ActiveCasters.Any()) - { - foreach (var e in base.ActiveAOEs(slot, actor)) - yield return e; - } - else if (Module.Enemies(OID._Gen_AFlowerInTheSun).FirstOrDefault() is Actor flower && Predicted is DateTime dt) - yield return new AOEInstance(Shape, flower.Position, flower.Rotation, dt); - } - - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - base.OnCastStarted(caster, spell); - if (spell.Action == WatchedAction) - Predicted = null; - } - - public override void OnActorCreated(Actor actor) - { - base.OnActorCreated(actor); - if ((OID)actor.OID == OID._Gen_AFlowerInTheSun) - Predicted = WorldState.FutureTime(10); - } -} -class Flood(BossModule module) : Components.Exaflare(module, new AOEShapeRect(50, 2.5f, 50)) -{ - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID == AID._Weaponskill_FloodInBlue) - { - Lines.Add(new Line() - { - Next = caster.Position + new WDir(-2.5f, 0), - Advance = new(-5, 0), - Rotation = default, - NextExplosion = Module.CastFinishAt(spell), - TimeToMove = 2, - ExplosionsLeft = 5, - MaxShownExplosions = 1 - }); - Lines.Add(new Line() - { - Next = caster.Position + new WDir(2.5f, 0), - Advance = new(5, 0), - Rotation = default, - NextExplosion = Module.CastFinishAt(spell), - TimeToMove = 2, - ExplosionsLeft = 5, - MaxShownExplosions = 1 - }); - } - } - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID._Weaponskill_FloodInBlue) - { - AdvanceLine(Lines[0], caster.Position + new WDir(-2.5f, 0)); - AdvanceLine(Lines[1], caster.Position + new WDir(2.5f, 0)); - } - - if ((AID)spell.Action.ID == AID._Weaponskill_FloodInBlue2) - { - var rectCenter = caster.Position + caster.Rotation.ToDirection().OrthoR() * 2.5f; - if (Lines.FirstOrDefault(l => l.Next.AlmostEqual(rectCenter, 0.1f)) is Line l) - { - AdvanceLine(l, rectCenter); - if (l.ExplosionsLeft == 0) - Lines.Remove(l); - } - } - } -} - -class P1Bounds(BossModule module) : BossComponent(module) -{ - public override void Update() - { - Arena.Center = Raid.Player()?.Position ?? Arena.Center; - } -} - -class BlazeInRed(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_BlazeInRed)); -class TornadoInGreen(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_TornadoInGreen), new AOEShapeDonut(12, 40)); -class NineIvies(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_NineIvies1), new AOEShapeCone(50, 10.Degrees()), 9); -class SculptureCast(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID._Weaponskill_SculptureCast)); -class Earthquake(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeCircle(10), new AOEShapeDonut(10, 20), new AOEShapeDonut(20, 30)]) -{ - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID == AID._Weaponskill_Earthquake) - AddSequence(caster.Position, Module.CastFinishAt(spell)); - } - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - var idx = (AID)spell.Action.ID switch - { - AID._Weaponskill_Earthquake => 0, - AID._Weaponskill_Earthquake2 => 1, - AID._Weaponskill_Earthquake3 => 2, - _ => -1 - }; - AdvanceSequence(idx, caster.Position, WorldState.FutureTime(2)); - } -} -class Freeze(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_FreezeInCyan), new AOEShapeCone(40, 22.5f.Degrees())); - -public class QuestStates : StateMachineBuilder -{ - public QuestStates(BossModule module) : base(module) - { - bool DutyEnd() => module.WorldState.CurrentCFCID != 966; - bool P1End() => module.Enemies(OID._Gen_FlightOfTheGriffin).Any(x => x.IsTargetable) || P2End(); - bool P2End() => module.Enemies(OID.Boss).Any(x => x.IsTargetable) || DutyEnd(); - - TrivialPhase() - .ActivateOnEnter() - .OnEnter(() => - { - Module.Arena.Center = new(54, -219); - Module.Arena.Bounds = new ArenaBoundsRect(26, 9); - }) - .Raw.Update = P1End; - TrivialPhase(1) - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .OnEnter(() => - { - Module.Arena.Center = new(0, -250); - Module.Arena.Bounds = new ArenaBoundsRect(20, 40); - }) - .Raw.Update = P2End; - TrivialPhase(2) - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .OnEnter(() => - { - Module.Arena.Center = new(0, -340); - Module.Arena.Bounds = new ArenaBoundsSquare(25); - }) - .Raw.Update = DutyEnd; - } -} - -[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 966, PrimaryActorOID = BossModuleInfo.PrimaryActorNone)] -public class Quest(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(20)) -{ - protected override bool CheckPull() => true; - - protected override void DrawArenaForeground(int pcSlot, Actor pc) - { - Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); - Arena.Actors(WorldState.Actors.Where(x => x.IsAlly), ArenaColor.PlayerGeneric); - } - - protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - foreach (var e in hints.PotentialTargets) - e.Priority = 0; - } -} -*/ From af1f1b6e847d533a20c41596dd3d701850e6c8f1 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:31:50 -0500 Subject: [PATCH 18/33] add missing crap --- BossMod/Components/RotationModule.cs | 9 +++++++++ BossMod/Data/Actor.cs | 4 +++- BossMod/Util/CurveApprox.cs | 12 ++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 BossMod/Components/RotationModule.cs diff --git a/BossMod/Components/RotationModule.cs b/BossMod/Components/RotationModule.cs new file mode 100644 index 0000000000..9f8084d5eb --- /dev/null +++ b/BossMod/Components/RotationModule.cs @@ -0,0 +1,9 @@ +using BossMod.QuestBattle; + +namespace BossMod.Components; + +public abstract class RotationModule(BossModule module) : BossComponent(module) where R : UnmanagedRotation +{ + private readonly R _rotation = New.Constructor()(module.WorldState); + public sealed override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) => _rotation.Execute(actor, hints); +} diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index a4271152b3..01f5f78376 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -132,6 +132,7 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public int PredictedMPRaw => (int)HPMP.CurMP + PendingMPDiffence; public int PredictedHPClamped => Math.Clamp(PredictedHPRaw, 0, (int)HPMP.MaxHP); public bool PredictedDead => PredictedHPRaw <= 1 && !IsStrikingDummy; + public float PredictedHPRatio => (float)PredictedHPRaw / HPMP.MaxHP; // if expirationForPredicted is not null, search pending first, and return one if found; in that case only low byte of extra will be set public ActorStatus? FindStatus(uint sid, DateTime? expirationForPending = null) @@ -161,7 +162,8 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public ActorStatus? FindStatus(SID sid, DateTime? expirationForPending = null) where SID : Enum => FindStatus((uint)(object)sid, expirationForPending); public ActorStatus? FindStatus(SID sid, ulong source, DateTime? expirationForPending = null) where SID : Enum => FindStatus((uint)(object)sid, source, expirationForPending); - public WDir DirectionTo(Actor other) => (other.Position - Position).Normalized(); + public WDir DirectionTo(WPos other) => (other - Position).Normalized(); + public WDir DirectionTo(Actor other) => DirectionTo(other.Position); public Angle AngleTo(Actor other) => Angle.FromDirection(other.Position - Position); public float DistanceToHitbox(Actor? other) => other == null ? float.MaxValue : (other.Position - Position).Length() - other.HitboxRadius - HitboxRadius; diff --git a/BossMod/Util/CurveApprox.cs b/BossMod/Util/CurveApprox.cs index 7645887018..4e437a2835 100644 --- a/BossMod/Util/CurveApprox.cs +++ b/BossMod/Util/CurveApprox.cs @@ -48,6 +48,18 @@ public static IEnumerable CircleSector(float radius, Angle angleStart, Ang } public static IEnumerable CircleSector(WPos center, float radius, Angle angleStart, Angle angleEnd, float maxError) => CircleSector(radius, angleStart, angleEnd, maxError).Select(off => center + off); + public static IEnumerable Ellipse(float axis1, float axis2, float maxError) + { + int numSegments = CalculateCircleSegments((axis1 + axis2) / 2f, (2 * MathF.PI).Radians(), maxError); + var angle = (2 * MathF.PI / numSegments).Radians(); + for (int i = 0; i < numSegments; ++i) + { + var t = i * angle; + yield return new WDir(axis1 * t.Cos(), axis2 * t.Sin()); + } + } + public static IEnumerable Ellipse(WPos center, float axis1, float axis2, float maxError) => Ellipse(axis1, axis2, maxError).Select(off => center + off); + // return polygon points approximating full donut; implicitly closed path - outer arc + inner arc public static IEnumerable Donut(float innerRadius, float outerRadius, float maxError) { From 3c25c1b3f58dc44be9c48c32f356465903ae7ae2 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sat, 25 Jan 2025 21:34:05 +0000 Subject: [PATCH 19/33] Initial version of auto-farm module. --- BossMod/AI/AIConfig.cs | 3 - BossMod/Autorotation/Legacy/LegacyBRD.cs | 2 +- BossMod/Autorotation/Legacy/LegacyDNC.cs | 2 +- BossMod/Autorotation/Legacy/LegacyDRG.cs | 2 +- BossMod/Autorotation/Legacy/LegacyGNB.cs | 2 +- BossMod/Autorotation/Legacy/LegacyRPR.cs | 2 +- BossMod/Autorotation/Legacy/LegacyWAR.cs | 2 +- BossMod/Autorotation/MiscAI/AutoFarm.cs | 99 +++++++++++++++++++ .../Autorotation/MiscAI/StayCloseToTarget.cs | 3 +- .../Autorotation/MiscAI/StayWithinLeylines.cs | 2 +- BossMod/Autorotation/RotationModule.cs | 2 +- BossMod/Autorotation/RotationModuleManager.cs | 2 +- BossMod/Autorotation/Standard/StandardWAR.cs | 2 +- .../Autorotation/Utility/ClassASTUtility.cs | 2 +- .../Autorotation/Utility/ClassBLMUtility.cs | 2 +- .../Autorotation/Utility/ClassBLUUtility.cs | 2 +- .../Autorotation/Utility/ClassBRDUtility.cs | 2 +- .../Autorotation/Utility/ClassDNCUtility.cs | 2 +- .../Autorotation/Utility/ClassDRGUtility.cs | 2 +- .../Autorotation/Utility/ClassDRKUtility.cs | 2 +- .../Autorotation/Utility/ClassGNBUtility.cs | 2 +- .../Autorotation/Utility/ClassMCHUtility.cs | 2 +- .../Autorotation/Utility/ClassMNKUtility.cs | 2 +- .../Autorotation/Utility/ClassNINUtility.cs | 2 +- .../Autorotation/Utility/ClassPCTUtility.cs | 2 +- .../Autorotation/Utility/ClassPLDUtility.cs | 2 +- .../Autorotation/Utility/ClassRDMUtility.cs | 2 +- .../Autorotation/Utility/ClassRPRUtility.cs | 2 +- .../Autorotation/Utility/ClassSAMUtility.cs | 2 +- .../Autorotation/Utility/ClassSCHUtility.cs | 2 +- .../Autorotation/Utility/ClassSGEUtility.cs | 2 +- .../Autorotation/Utility/ClassSMNUtility.cs | 2 +- .../Autorotation/Utility/ClassVPRUtility.cs | 2 +- .../Autorotation/Utility/ClassWARUtility.cs | 2 +- .../Autorotation/Utility/RolePvPUtility.cs | 2 +- BossMod/Autorotation/akechi/AkechiBLM.cs | 2 +- BossMod/Autorotation/akechi/AkechiDRG.cs | 2 +- BossMod/Autorotation/akechi/AkechiGNB.cs | 2 +- BossMod/Autorotation/akechi/AkechiGNBPvP.cs | 2 +- BossMod/Autorotation/akechi/AkechiPLD.cs | 2 +- BossMod/Autorotation/akechi/AkechiSCH.cs | 2 +- BossMod/Autorotation/veyn/VeynBRD.cs | 2 +- BossMod/Autorotation/xan/AI/Healer.cs | 2 +- BossMod/Autorotation/xan/AI/Melee.cs | 2 +- BossMod/Autorotation/xan/AI/Ranged.cs | 2 +- BossMod/Autorotation/xan/AI/Tank.cs | 2 +- BossMod/Autorotation/xan/Basexan.cs | 2 +- BossMod/Config/ConfigChangelog.cs | 12 +++ BossMod/Framework/IPCProvider.cs | 15 ++- .../RM02SHoneyBLovely/AI/AIExperiment.cs | 2 +- .../RM04SWickedThunder/AI/AIExperiment.cs | 2 +- .../Modules/Dawntrail/Ultimate/FRU/FRUAI.cs | 2 +- .../Extreme/Ex3Titan/Ex3TitanAI.cs | 2 +- BossMod/Modules/StrikingDummy.cs | 2 +- 54 files changed, 173 insertions(+), 57 deletions(-) create mode 100644 BossMod/Autorotation/MiscAI/AutoFarm.cs diff --git a/BossMod/AI/AIConfig.cs b/BossMod/AI/AIConfig.cs index 05f423bdc3..4cbbc2a734 100644 --- a/BossMod/AI/AIConfig.cs +++ b/BossMod/AI/AIConfig.cs @@ -32,9 +32,6 @@ public enum Slot { One, Two, Three, Four } [PropertyDisplay("Disable auto-target")] public bool ForbidActions = false; - [PropertyDisplay("Automatically engage FATE mobs", since: "0.0.0.253")] - public bool AutoFate = true; - [PropertyDisplay("Focus target master")] public bool FocusTargetMaster = false; diff --git a/BossMod/Autorotation/Legacy/LegacyBRD.cs b/BossMod/Autorotation/Legacy/LegacyBRD.cs index 74af271e7b..c0a22058f7 100644 --- a/BossMod/Autorotation/Legacy/LegacyBRD.cs +++ b/BossMod/Autorotation/Legacy/LegacyBRD.cs @@ -154,7 +154,7 @@ public LegacyBRD(RotationModuleManager manager, Actor player) : base(manager, pl _state = new(this); } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { _state.UpdateCommon(primaryTarget, estimatedAnimLockDelay); if (_state.AnimationLockDelay < 0.1f) diff --git a/BossMod/Autorotation/Legacy/LegacyDNC.cs b/BossMod/Autorotation/Legacy/LegacyDNC.cs index 93be79c890..95562543b4 100644 --- a/BossMod/Autorotation/Legacy/LegacyDNC.cs +++ b/BossMod/Autorotation/Legacy/LegacyDNC.cs @@ -142,7 +142,7 @@ public LegacyDNC(RotationModuleManager manager, Actor player) : base(manager, pl _state = new(this); } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + 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); diff --git a/BossMod/Autorotation/Legacy/LegacyDRG.cs b/BossMod/Autorotation/Legacy/LegacyDRG.cs index ae4fe6cc18..d43c003374 100644 --- a/BossMod/Autorotation/Legacy/LegacyDRG.cs +++ b/BossMod/Autorotation/Legacy/LegacyDRG.cs @@ -89,7 +89,7 @@ public LegacyDRG(RotationModuleManager manager, Actor player) : base(manager, pl _state = new(this); } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { _state.UpdateCommon(primaryTarget, estimatedAnimLockDelay); diff --git a/BossMod/Autorotation/Legacy/LegacyGNB.cs b/BossMod/Autorotation/Legacy/LegacyGNB.cs index 5d446ea43e..9e5276b548 100644 --- a/BossMod/Autorotation/Legacy/LegacyGNB.cs +++ b/BossMod/Autorotation/Legacy/LegacyGNB.cs @@ -102,7 +102,7 @@ public LegacyGNB(RotationModuleManager manager, Actor player) : base(manager, pl _state = new(this); } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + 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; diff --git a/BossMod/Autorotation/Legacy/LegacyRPR.cs b/BossMod/Autorotation/Legacy/LegacyRPR.cs index bae0b00e06..660ae61e3c 100644 --- a/BossMod/Autorotation/Legacy/LegacyRPR.cs +++ b/BossMod/Autorotation/Legacy/LegacyRPR.cs @@ -120,7 +120,7 @@ public LegacyRPR(RotationModuleManager manager, Actor player) : base(manager, pl _state = new(this); } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + 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; diff --git a/BossMod/Autorotation/Legacy/LegacyWAR.cs b/BossMod/Autorotation/Legacy/LegacyWAR.cs index 7dcc54b381..c7ddebce9e 100644 --- a/BossMod/Autorotation/Legacy/LegacyWAR.cs +++ b/BossMod/Autorotation/Legacy/LegacyWAR.cs @@ -122,7 +122,7 @@ public LegacyWAR(RotationModuleManager manager, Actor player) : base(manager, pl _state = new(this); } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + 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; diff --git a/BossMod/Autorotation/MiscAI/AutoFarm.cs b/BossMod/Autorotation/MiscAI/AutoFarm.cs new file mode 100644 index 0000000000..63d8164336 --- /dev/null +++ b/BossMod/Autorotation/MiscAI/AutoFarm.cs @@ -0,0 +1,99 @@ +namespace BossMod.Autorotation.MiscAI; + +public sealed class AutoFarm(RotationModuleManager manager, Actor player) : RotationModule(manager, player) +{ + public enum Track { General, Fate, Specific } + public enum GeneralStrategy { AllowPull, FightBack, Aggressive, Passive } + public enum PriorityStrategy { None, Prioritize } + + public static RotationModuleDefinition Definition() + { + RotationModuleDefinition res = new("Misc AI: Automatic farming", "Make sure this is ordered before standard rotation modules!", "Misc", "veyn", RotationModuleQuality.Basic, new(~0ul), 1000); + + res.Define(Track.General).As("General") + .AddOption(GeneralStrategy.AllowPull, "AllowPull", "Automatically engage any mobs that are in combat with player; if player is not in combat, pull new mobs") + .AddOption(GeneralStrategy.FightBack, "FightBack", "Automatically engage any mobs that are in combat with player, but don't pull new mobs") + .AddOption(GeneralStrategy.Aggressive, "Aggressive", "Aggressively pull all mobs that are not yet in combat") + .AddOption(GeneralStrategy.Passive, "Passive", "Do nothing"); + + res.Define(Track.Fate).As("FATE") + .AddOption(PriorityStrategy.None, "None", "Do not do anything about fate mobs") + .AddOption(PriorityStrategy.Prioritize, "Prioritize", "Prioritize mobs in active fate"); + + res.Define(Track.Specific).As("Specific") + .AddOption(PriorityStrategy.None, "None", "Do not do anything special") + .AddOption(PriorityStrategy.Prioritize, "Prioritize", "Prioritize specific mobs by targeting criterion"); + + return res; + } + + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + { + var generalStrategy = strategy.Option(Track.General).As(); + if (generalStrategy == GeneralStrategy.Passive) + return; + + var allowPulling = generalStrategy switch + { + GeneralStrategy.AllowPull => !Player.InCombat, + GeneralStrategy.Aggressive => true, + _ => false + }; + + Actor? closestTargetToSwitchTo = null; // non-null if we bump any priorities + float closestTargetDistSq = float.MaxValue; + void prioritize(AIHints.Enemy e, int prio) + { + e.Priority = prio; + + var distSq = (e.Actor.Position - Player.Position).LengthSq(); + if (distSq < closestTargetDistSq) + { + closestTargetToSwitchTo = e.Actor; + closestTargetDistSq = distSq; + } + } + + // first deal with pulling new enemies + if (allowPulling) + { + if (World.Client.ActiveFate.ID != 0 && Player.Level <= Service.LuminaRow(World.Client.ActiveFate.ID)?.ClassJobLevelMax && strategy.Option(Track.Fate).As() == PriorityStrategy.Prioritize) + { + foreach (var e in Hints.PotentialTargets) + { + if (e.Actor.FateID == World.Client.ActiveFate.ID && e.Priority == AIHints.Enemy.PriorityUndesirable) + { + prioritize(e, 1); + } + } + } + + var specific = strategy.Option(Track.Specific); + if (specific.As() == PriorityStrategy.Prioritize && Hints.FindEnemy(ResolveTargetOverride(specific.Value)) is var target && target != null) + { + prioritize(target, 2); + } + } + + // if we're not going to pull anyone, but we are already in combat and not targeting aggroed enemy, find one to target + if (closestTargetToSwitchTo == null && Player.InCombat && !(primaryTarget?.AggroPlayer ?? false)) + { + foreach (var e in Hints.PotentialTargets) + { + if (e.Actor.AggroPlayer) + { + prioritize(e, 3); + } + } + } + + // if we have target to attack, do that + if (closestTargetToSwitchTo != null) + { + // if we've updated any priorities, we need to re-sort target array + Hints.PotentialTargets.SortByReverse(x => x.Priority); + Hints.HighestPotentialTargetPriority = Math.Max(0, Hints.PotentialTargets[0].Priority); + primaryTarget = Hints.ForcedTarget = closestTargetToSwitchTo; + } + } +} diff --git a/BossMod/Autorotation/MiscAI/StayCloseToTarget.cs b/BossMod/Autorotation/MiscAI/StayCloseToTarget.cs index 8c22f4aee4..4142def1ad 100644 --- a/BossMod/Autorotation/MiscAI/StayCloseToTarget.cs +++ b/BossMod/Autorotation/MiscAI/StayCloseToTarget.cs @@ -4,7 +4,6 @@ namespace BossMod.Autorotation.MiscAI; public sealed class StayCloseToTarget(RotationModuleManager manager, Actor player) : RotationModule(manager, player) { - public enum Tracks { Range @@ -28,7 +27,7 @@ public static RotationModuleDefinition Definition() return def; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (primaryTarget != null) Hints.GoalZones.Add(Hints.GoalSingleTarget(primaryTarget.Position, (strategy.Option(Tracks.Range).Value.Option + 10f) / 10f + primaryTarget.HitboxRadius, 0.5f)); diff --git a/BossMod/Autorotation/MiscAI/StayWithinLeylines.cs b/BossMod/Autorotation/MiscAI/StayWithinLeylines.cs index 73a4092567..16b73fdb10 100644 --- a/BossMod/Autorotation/MiscAI/StayWithinLeylines.cs +++ b/BossMod/Autorotation/MiscAI/StayWithinLeylines.cs @@ -37,7 +37,7 @@ public static RotationModuleDefinition Definition() return def; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { bool InLeyLines = Player.FindStatus(BLM.SID.CircleOfPower) != null; diff --git a/BossMod/Autorotation/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index f0ae56acb3..1d4aa171fe 100644 --- a/BossMod/Autorotation/RotationModule.cs +++ b/BossMod/Autorotation/RotationModule.cs @@ -89,7 +89,7 @@ public abstract class RotationModule(RotationModuleManager manager, Actor player public AIHints Hints => Manager.Hints; // the main entry point of the module - given a set of strategy values, fill the queue with a set of actions to execute - public abstract void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving); + public abstract void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving); public virtual string DescribeState() => ""; diff --git a/BossMod/Autorotation/RotationModuleManager.cs b/BossMod/Autorotation/RotationModuleManager.cs index 3dab131ca2..dce34c33c1 100644 --- a/BossMod/Autorotation/RotationModuleManager.cs +++ b/BossMod/Autorotation/RotationModuleManager.cs @@ -108,7 +108,7 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) foreach (var m in _activeModules) { var values = Preset?.ActiveStrategyOverrides(m.DataIndex) ?? Planner?.ActiveStrategyOverrides(m.DataIndex) ?? throw new InvalidOperationException("Both preset and plan are null, but there are active modules"); - m.Module.Execute(values, target, estimatedAnimLockDelay, isMoving); + m.Module.Execute(values, ref target, estimatedAnimLockDelay, isMoving); } } diff --git a/BossMod/Autorotation/Standard/StandardWAR.cs b/BossMod/Autorotation/Standard/StandardWAR.cs index 7ec0bb4fe3..ccac8d9b50 100644 --- a/BossMod/Autorotation/Standard/StandardWAR.cs +++ b/BossMod/Autorotation/Standard/StandardWAR.cs @@ -195,7 +195,7 @@ public enum OGCDPriority private bool InMeleeRange(Actor? target) => Player.DistanceToHitbox(target) <= 3; private bool IsFirstGCD() => !Player.InCombat || (World.CurrentTime - Manager.CombatStart).TotalSeconds < 0.1f; - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { Gauge = World.Client.GetGauge().BeastGauge; GCDLength = ActionSpeed.GCDRounded(World.Client.PlayerStats.SkillSpeed, World.Client.PlayerStats.Haste, Player.Level); diff --git a/BossMod/Autorotation/Utility/ClassASTUtility.cs b/BossMod/Autorotation/Utility/ClassASTUtility.cs index f5015f18b6..c53274c404 100644 --- a/BossMod/Autorotation/Utility/ClassASTUtility.cs +++ b/BossMod/Autorotation/Utility/ClassASTUtility.cs @@ -62,7 +62,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.Lightspeed), AST.AID.Lightspeed, Player); diff --git a/BossMod/Autorotation/Utility/ClassBLMUtility.cs b/BossMod/Autorotation/Utility/ClassBLMUtility.cs index 4413261d5e..3e6bb9c02b 100644 --- a/BossMod/Autorotation/Utility/ClassBLMUtility.cs +++ b/BossMod/Autorotation/Utility/ClassBLMUtility.cs @@ -22,7 +22,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.Manaward), BLM.AID.Manaward, Player); diff --git a/BossMod/Autorotation/Utility/ClassBLUUtility.cs b/BossMod/Autorotation/Utility/ClassBLUUtility.cs index 50cca592d6..bcca393c5c 100644 --- a/BossMod/Autorotation/Utility/ClassBLUUtility.cs +++ b/BossMod/Autorotation/Utility/ClassBLUUtility.cs @@ -43,7 +43,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.Bristle), BLU.AID.Bristle, Player); diff --git a/BossMod/Autorotation/Utility/ClassBRDUtility.cs b/BossMod/Autorotation/Utility/ClassBRDUtility.cs index 0b0dacfc43..7bfbdcedd7 100644 --- a/BossMod/Autorotation/Utility/ClassBRDUtility.cs +++ b/BossMod/Autorotation/Utility/ClassBRDUtility.cs @@ -26,7 +26,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.WardensPaean), BRD.AID.WardensPaean, Player); diff --git a/BossMod/Autorotation/Utility/ClassDNCUtility.cs b/BossMod/Autorotation/Utility/ClassDNCUtility.cs index 0b1aa1ddaa..979a3293ae 100644 --- a/BossMod/Autorotation/Utility/ClassDNCUtility.cs +++ b/BossMod/Autorotation/Utility/ClassDNCUtility.cs @@ -26,7 +26,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.CuringWaltz), DNC.AID.CuringWaltz, Player); diff --git a/BossMod/Autorotation/Utility/ClassDRGUtility.cs b/BossMod/Autorotation/Utility/ClassDRGUtility.cs index 0d0f2bb20b..6727390402 100644 --- a/BossMod/Autorotation/Utility/ClassDRGUtility.cs +++ b/BossMod/Autorotation/Utility/ClassDRGUtility.cs @@ -21,7 +21,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); diff --git a/BossMod/Autorotation/Utility/ClassDRKUtility.cs b/BossMod/Autorotation/Utility/ClassDRKUtility.cs index e0ea183da3..d33c5140d5 100644 --- a/BossMod/Autorotation/Utility/ClassDRKUtility.cs +++ b/BossMod/Autorotation/Utility/ClassDRKUtility.cs @@ -50,7 +50,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, IDStanceApply, IDStanceRemove, (uint)DRK.SID.Grit, primaryTarget); //Execution of our shared abilities ExecuteSimple(strategy.Option(Track.DarkMind), DRK.AID.DarkMind, Player); //Execution of DarkMind diff --git a/BossMod/Autorotation/Utility/ClassGNBUtility.cs b/BossMod/Autorotation/Utility/ClassGNBUtility.cs index 1e381c02dd..0bacc018f1 100644 --- a/BossMod/Autorotation/Utility/ClassGNBUtility.cs +++ b/BossMod/Autorotation/Utility/ClassGNBUtility.cs @@ -44,7 +44,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Execution of Utility skills + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Execution of Utility skills { ExecuteShared(strategy, IDLimitBreak3, IDStanceApply, IDStanceRemove, (uint)GNB.SID.RoyalGuard, primaryTarget); ExecuteSimple(strategy.Option(Track.Camouflage), GNB.AID.Camouflage, Player); diff --git a/BossMod/Autorotation/Utility/ClassMCHUtility.cs b/BossMod/Autorotation/Utility/ClassMCHUtility.cs index c810c07ebd..3280cdb282 100644 --- a/BossMod/Autorotation/Utility/ClassMCHUtility.cs +++ b/BossMod/Autorotation/Utility/ClassMCHUtility.cs @@ -29,7 +29,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.Dismantle), MCH.AID.Dismantle, primaryTarget); diff --git a/BossMod/Autorotation/Utility/ClassMNKUtility.cs b/BossMod/Autorotation/Utility/ClassMNKUtility.cs index 4de9bd3e12..60a0bc041c 100644 --- a/BossMod/Autorotation/Utility/ClassMNKUtility.cs +++ b/BossMod/Autorotation/Utility/ClassMNKUtility.cs @@ -28,7 +28,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.Mantra), MNK.AID.Mantra, Player); diff --git a/BossMod/Autorotation/Utility/ClassNINUtility.cs b/BossMod/Autorotation/Utility/ClassNINUtility.cs index 7369dc67f0..75e916a25a 100644 --- a/BossMod/Autorotation/Utility/ClassNINUtility.cs +++ b/BossMod/Autorotation/Utility/ClassNINUtility.cs @@ -23,7 +23,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.ShadeShift), NIN.AID.ShadeShift, Player); diff --git a/BossMod/Autorotation/Utility/ClassPCTUtility.cs b/BossMod/Autorotation/Utility/ClassPCTUtility.cs index 39aa406f3e..0a12b307b8 100644 --- a/BossMod/Autorotation/Utility/ClassPCTUtility.cs +++ b/BossMod/Autorotation/Utility/ClassPCTUtility.cs @@ -16,7 +16,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.TemperaCoat), PCT.AID.TemperaCoat, Player); diff --git a/BossMod/Autorotation/Utility/ClassPLDUtility.cs b/BossMod/Autorotation/Utility/ClassPLDUtility.cs index 69fb16e3ce..265cbc826f 100644 --- a/BossMod/Autorotation/Utility/ClassPLDUtility.cs +++ b/BossMod/Autorotation/Utility/ClassPLDUtility.cs @@ -39,7 +39,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, IDStanceApply, IDStanceRemove, (uint)PLD.SID.IronWill, primaryTarget); ExecuteSimple(strategy.Option(Track.Cover), PLD.AID.Cover, ResolveTargetOverride(strategy.Option(Track.Cover).Value) ?? Player); //Cover execution diff --git a/BossMod/Autorotation/Utility/ClassRDMUtility.cs b/BossMod/Autorotation/Utility/ClassRDMUtility.cs index 1a68bc99a2..a7834fba99 100644 --- a/BossMod/Autorotation/Utility/ClassRDMUtility.cs +++ b/BossMod/Autorotation/Utility/ClassRDMUtility.cs @@ -15,7 +15,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.MagickBarrier), RDM.AID.MagickBarrier, Player); diff --git a/BossMod/Autorotation/Utility/ClassRPRUtility.cs b/BossMod/Autorotation/Utility/ClassRPRUtility.cs index 6f3d3da791..cd407e4153 100644 --- a/BossMod/Autorotation/Utility/ClassRPRUtility.cs +++ b/BossMod/Autorotation/Utility/ClassRPRUtility.cs @@ -16,7 +16,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.ArcaneCrest), RPR.AID.ArcaneCrest, Player); diff --git a/BossMod/Autorotation/Utility/ClassSAMUtility.cs b/BossMod/Autorotation/Utility/ClassSAMUtility.cs index 20f9f2def8..57ef31659f 100644 --- a/BossMod/Autorotation/Utility/ClassSAMUtility.cs +++ b/BossMod/Autorotation/Utility/ClassSAMUtility.cs @@ -21,7 +21,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); diff --git a/BossMod/Autorotation/Utility/ClassSCHUtility.cs b/BossMod/Autorotation/Utility/ClassSCHUtility.cs index 4439158c12..784df8b4dc 100644 --- a/BossMod/Autorotation/Utility/ClassSCHUtility.cs +++ b/BossMod/Autorotation/Utility/ClassSCHUtility.cs @@ -80,7 +80,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.WhisperingDawn), SCH.AID.WhisperingDawn, Player); diff --git a/BossMod/Autorotation/Utility/ClassSGEUtility.cs b/BossMod/Autorotation/Utility/ClassSGEUtility.cs index 1b626a48c2..232f638737 100644 --- a/BossMod/Autorotation/Utility/ClassSGEUtility.cs +++ b/BossMod/Autorotation/Utility/ClassSGEUtility.cs @@ -78,7 +78,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //How we're executing our skills listed below + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //How we're executing our skills listed below { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); ExecuteSimple(strategy.Option(Track.Eukrasia), SGE.AID.Eukrasia, Player); diff --git a/BossMod/Autorotation/Utility/ClassSMNUtility.cs b/BossMod/Autorotation/Utility/ClassSMNUtility.cs index 79ec52c675..36268688ce 100644 --- a/BossMod/Autorotation/Utility/ClassSMNUtility.cs +++ b/BossMod/Autorotation/Utility/ClassSMNUtility.cs @@ -22,7 +22,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); diff --git a/BossMod/Autorotation/Utility/ClassVPRUtility.cs b/BossMod/Autorotation/Utility/ClassVPRUtility.cs index 02ebbb5ff7..3ddcbf6865 100644 --- a/BossMod/Autorotation/Utility/ClassVPRUtility.cs +++ b/BossMod/Autorotation/Utility/ClassVPRUtility.cs @@ -24,7 +24,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, primaryTarget); diff --git a/BossMod/Autorotation/Utility/ClassWARUtility.cs b/BossMod/Autorotation/Utility/ClassWARUtility.cs index 971fb45c53..163cc2ef20 100644 --- a/BossMod/Autorotation/Utility/ClassWARUtility.cs +++ b/BossMod/Autorotation/Utility/ClassWARUtility.cs @@ -38,7 +38,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { ExecuteShared(strategy, IDLimitBreak3, IDStanceApply, IDStanceRemove, (uint)WAR.SID.Defiance, primaryTarget); ExecuteSimple(strategy.Option(Track.Thrill), WAR.AID.ThrillOfBattle, Player); diff --git a/BossMod/Autorotation/Utility/RolePvPUtility.cs b/BossMod/Autorotation/Utility/RolePvPUtility.cs index fd43ddfb2b..b658b4aae4 100644 --- a/BossMod/Autorotation/Utility/RolePvPUtility.cs +++ b/BossMod/Autorotation/Utility/RolePvPUtility.cs @@ -154,7 +154,7 @@ public float DebuffsLeft(Actor? target) } public bool HasAnyDebuff(Actor? target) => DebuffsLeft(target) > 0; - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { #region Variables hasSprint = HasEffect(SID.SprintPvP); diff --git a/BossMod/Autorotation/akechi/AkechiBLM.cs b/BossMod/Autorotation/akechi/AkechiBLM.cs index d677562dea..32cfeb03dc 100644 --- a/BossMod/Autorotation/akechi/AkechiBLM.cs +++ b/BossMod/Autorotation/akechi/AkechiBLM.cs @@ -451,7 +451,7 @@ float splashPriorityFunc(Actor actor) #endregion - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions { #region Variables var gauge = World.Client.GetGauge(); //Retrieve BLM gauge diff --git a/BossMod/Autorotation/akechi/AkechiDRG.cs b/BossMod/Autorotation/akechi/AkechiDRG.cs index b65d93a3d8..52989731d3 100644 --- a/BossMod/Autorotation/akechi/AkechiDRG.cs +++ b/BossMod/Autorotation/akechi/AkechiDRG.cs @@ -505,7 +505,7 @@ private int NumTargetsHitBySpear(Actor primary) //Count number of targets hit by #endregion - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { #region Variables diff --git a/BossMod/Autorotation/akechi/AkechiGNB.cs b/BossMod/Autorotation/akechi/AkechiGNB.cs index 060740d435..2dd8fd94bc 100644 --- a/BossMod/Autorotation/akechi/AkechiGNB.cs +++ b/BossMod/Autorotation/akechi/AkechiGNB.cs @@ -410,7 +410,7 @@ private AID BestContinuation //Determine the best Continuation to use public bool JustUsed(AID aid, float variance) => JustDid(aid) && DidWithin(variance); //Check if the last action used was the desired ability & was used within a certain timeframe #endregion - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions { #region Variables //Gauge diff --git a/BossMod/Autorotation/akechi/AkechiGNBPvP.cs b/BossMod/Autorotation/akechi/AkechiGNBPvP.cs index 90d793a961..9ac911dba1 100644 --- a/BossMod/Autorotation/akechi/AkechiGNBPvP.cs +++ b/BossMod/Autorotation/akechi/AkechiGNBPvP.cs @@ -181,7 +181,7 @@ public enum OGCDPriority public AID LimitBreak => HasEffect(SID.RelentlessRushPvP) ? AID.TerminalTriggerPvP : AID.RelentlessRushPvP; #endregion - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { #region Variables var gauge = World.Client.GetGauge(); diff --git a/BossMod/Autorotation/akechi/AkechiPLD.cs b/BossMod/Autorotation/akechi/AkechiPLD.cs index 82e006e840..7dbcd1b5cd 100644 --- a/BossMod/Autorotation/akechi/AkechiPLD.cs +++ b/BossMod/Autorotation/akechi/AkechiPLD.cs @@ -348,7 +348,7 @@ public AID BestBlade //public Actor? BestSplashTarget() #endregion - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions { #region Variables var gauge = World.Client.GetGauge(); //Retrieve Paladin gauge diff --git a/BossMod/Autorotation/akechi/AkechiSCH.cs b/BossMod/Autorotation/akechi/AkechiSCH.cs index c4672e8de1..ed50068c89 100644 --- a/BossMod/Autorotation/akechi/AkechiSCH.cs +++ b/BossMod/Autorotation/akechi/AkechiSCH.cs @@ -205,7 +205,7 @@ private AID BestAOE //Determine the best AOE to use : AID.ArtOfWar1; //Otherwise, default to Art of War #endregion - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) //Executes our actions { #region Variables var gauge = World.Client.GetGauge(); //Retrieve Scholar gauge diff --git a/BossMod/Autorotation/veyn/VeynBRD.cs b/BossMod/Autorotation/veyn/VeynBRD.cs index 682f29fdf0..163cc22c82 100644 --- a/BossMod/Autorotation/veyn/VeynBRD.cs +++ b/BossMod/Autorotation/veyn/VeynBRD.cs @@ -204,7 +204,7 @@ public enum Song { None, MagesBallad, ArmysPaeon, WanderersMinuet } 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 override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { var gauge = World.Client.GetGauge(); ActiveSong = (Song)((byte)gauge.SongFlags & 3); diff --git a/BossMod/Autorotation/xan/AI/Healer.cs b/BossMod/Autorotation/xan/AI/Healer.cs index 8085e1bb8f..4a4b7eca4b 100644 --- a/BossMod/Autorotation/xan/AI/Healer.cs +++ b/BossMod/Autorotation/xan/AI/Healer.cs @@ -58,7 +58,7 @@ private void HealSingle(Action healFun healFun(a, b); } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Player.MountId > 0) return; diff --git a/BossMod/Autorotation/xan/AI/Melee.cs b/BossMod/Autorotation/xan/AI/Melee.cs index bd89864cca..0c80981970 100644 --- a/BossMod/Autorotation/xan/AI/Melee.cs +++ b/BossMod/Autorotation/xan/AI/Melee.cs @@ -15,7 +15,7 @@ public static RotationModuleDefinition Definition() return def; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Player.Statuses.Any(x => x.ID is (uint)BossMod.NIN.SID.TenChiJin or (uint)BossMod.NIN.SID.Mudra)) return; diff --git a/BossMod/Autorotation/xan/AI/Ranged.cs b/BossMod/Autorotation/xan/AI/Ranged.cs index 03e480ddb8..f39d5f7fda 100644 --- a/BossMod/Autorotation/xan/AI/Ranged.cs +++ b/BossMod/Autorotation/xan/AI/Ranged.cs @@ -14,7 +14,7 @@ public static RotationModuleDefinition Definition() return def; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { // interrupt if (strategy.Enabled(Track.Interrupt) && NextChargeIn(ClassShared.AID.HeadGraze) == 0) diff --git a/BossMod/Autorotation/xan/AI/Tank.cs b/BossMod/Autorotation/xan/AI/Tank.cs index 708896f1b1..5eafe92cf2 100644 --- a/BossMod/Autorotation/xan/AI/Tank.cs +++ b/BossMod/Autorotation/xan/AI/Tank.cs @@ -78,7 +78,7 @@ public record struct TankActions(ActionID Ranged, ActionID Stance, uint StanceBu _ => default }; - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Player.MountId > 0) return; diff --git a/BossMod/Autorotation/xan/Basexan.cs b/BossMod/Autorotation/xan/Basexan.cs index 10c1cce5f8..024b86cf1e 100644 --- a/BossMod/Autorotation/xan/Basexan.cs +++ b/BossMod/Autorotation/xan/Basexan.cs @@ -336,7 +336,7 @@ protected void UpdatePositionals(Actor? target, ref (Positional pos, bool imm) p Manager.Hints.RecommendedPositional = (target, positional.pos, NextPositionalImminent, NextPositionalCorrect); } - public sealed override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public sealed override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { NextGCD = default; NextGCDPrio = 0; diff --git a/BossMod/Config/ConfigChangelog.cs b/BossMod/Config/ConfigChangelog.cs index baf59563f6..779cee2567 100644 --- a/BossMod/Config/ConfigChangelog.cs +++ b/BossMod/Config/ConfigChangelog.cs @@ -44,6 +44,18 @@ public override void Draw() } } +class AINotice2 : ChangelogNotice +{ + public override Version Since => new(0, 0, 0, 289); + + public override void Draw() + { + ImGui.TextUnformatted("`Automatically engage FATE mobs` AI configuration option has been removed."); + ImGui.TextUnformatted("The new way of getting this behaviour is by adding `Misc AI: Automatic farming of fates` module to your preset and configuring FATE-farming strategy track as wanted."); + ImGui.TextUnformatted("Note that this module "); + } +} + public class ConfigChangelogWindow : UIWindow { private readonly Version PreviousVersion; diff --git a/BossMod/Framework/IPCProvider.cs b/BossMod/Framework/IPCProvider.cs index 79e03e241a..9eed0474e5 100644 --- a/BossMod/Framework/IPCProvider.cs +++ b/BossMod/Framework/IPCProvider.cs @@ -63,7 +63,7 @@ public IPCProvider(RotationModuleManager autorotation, ActionManagerEx amex, Mov return true; }); - Register("Presets.AddTransientStrategy", (string presetName, string moduleTypeName, string trackName, string value) => + bool addTransientStrategy(string presetName, string moduleTypeName, string trackName, string value, StrategyTarget target = StrategyTarget.Automatic, int targetParam = 0) { var mt = Type.GetType(moduleTypeName); if (mt == null || !RotationModuleRegistry.Modules.TryGetValue(mt, out var md)) @@ -77,9 +77,11 @@ public IPCProvider(RotationModuleManager autorotation, ActionManagerEx amex, Mov var ms = autorotation.Database.Presets.FindPresetByName(presetName)?.Modules.Find(m => m.Type == mt); if (ms == null) return false; - ms.Settings.Add(new(default, iTrack, new() { Option = iOpt })); + ms.Settings.Add(new(default, iTrack, new() { Option = iOpt, Target = target, TargetParam = targetParam })); return true; - }); + } + Register("Presets.AddTransientStrategy", (string presetName, string moduleTypeName, string trackName, string value) => addTransientStrategy(presetName, moduleTypeName, trackName, value)); + Register("Presets.AddTransientStrategyTargetEnemyOID", (string presetName, string moduleTypeName, string trackName, string value, int oid) => addTransientStrategy(presetName, moduleTypeName, trackName, value, StrategyTarget.EnemyByOID, oid)); } public void Dispose() => _disposeActions?.Invoke(); @@ -112,6 +114,13 @@ private void Register(string name, Func(string name, Func func) + { + var p = Service.PluginInterface.GetIpcProvider("BossMod." + name); + p.RegisterFunc(func); + _disposeActions += p.UnregisterFunc; + } + //private void Register(string name, Action func) //{ // var p = Service.PluginInterface.GetIpcProvider("BossMod." + name); diff --git a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/AI/AIExperiment.cs b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/AI/AIExperiment.cs index 24f02e65d8..d9675b6d74 100644 --- a/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/AI/AIExperiment.cs +++ b/BossMod/Modules/Dawntrail/Savage/RM02SHoneyBLovely/AI/AIExperiment.cs @@ -36,7 +36,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Bossmods.ActiveModule is not RM02SHoneyBLovely module) return; diff --git a/BossMod/Modules/Dawntrail/Savage/RM04SWickedThunder/AI/AIExperiment.cs b/BossMod/Modules/Dawntrail/Savage/RM04SWickedThunder/AI/AIExperiment.cs index d17b860f4e..ac1bd215d4 100644 --- a/BossMod/Modules/Dawntrail/Savage/RM04SWickedThunder/AI/AIExperiment.cs +++ b/BossMod/Modules/Dawntrail/Savage/RM04SWickedThunder/AI/AIExperiment.cs @@ -27,7 +27,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Bossmods.ActiveModule is not RM04SWickedThunder module) return; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs index f340edad99..006d554e8f 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs @@ -26,7 +26,7 @@ public static RotationModuleDefinition Definition() private readonly FRUConfig _config = Service.Config.Get(); - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Bossmods.ActiveModule is FRU module && module.Raid.FindSlot(Player.InstanceID) is var playerSlot && playerSlot >= 0) { diff --git a/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs b/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs index f025f2000b..c8e479c169 100644 --- a/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs +++ b/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs @@ -19,7 +19,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { SetForcedMovement(CalculateDestination(strategy.Option(Track.Movement))); } diff --git a/BossMod/Modules/StrikingDummy.cs b/BossMod/Modules/StrikingDummy.cs index 62309b1c1e..5d11c60bf9 100644 --- a/BossMod/Modules/StrikingDummy.cs +++ b/BossMod/Modules/StrikingDummy.cs @@ -34,7 +34,7 @@ public static RotationModuleDefinition Definition() return res; } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (strategy.Option(Track.Test).As() == Strategy.Some && primaryTarget != null) { From 6bab20d957e12d70c40499fc80f662a330a535a8 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:37:02 -0500 Subject: [PATCH 20/33] build warnings --- BossMod/Modules/Endwalker/Quest/SagesFocus.cs | 2 -- .../Shadowbringers/Quest/ATearfulReunion.cs | 14 +++----------- .../Shadowbringers/Quest/NyelbertsLament.cs | 6 ++---- .../Shadowbringers/Quest/TheHardenedHeart.cs | 2 +- BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs | 2 +- 5 files changed, 7 insertions(+), 19 deletions(-) diff --git a/BossMod/Modules/Endwalker/Quest/SagesFocus.cs b/BossMod/Modules/Endwalker/Quest/SagesFocus.cs index 732e098c5a..72a5e90d80 100644 --- a/BossMod/Modules/Endwalker/Quest/SagesFocus.cs +++ b/BossMod/Modules/Endwalker/Quest/SagesFocus.cs @@ -4,14 +4,12 @@ public enum OID : uint { Boss = 0x3587, Helper = 0x233C, - _Gen_ChiBomb = 0x358D, // R1.000, x0 (spawn during fight) Mahaud = 0x3586, Loifa = 0x3588, } public enum AID : uint { - _AutoAttack_Attack = 872, // Boss->3589, no cast, single-target TripleThreat = 26535, // Boss->3589, 8.0s cast, single-target ChiBomb = 26536, // Boss->self, 5.0s cast, single-target Explosion = 26537, // 358D->self, 5.0s cast, range 6 circle diff --git a/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs b/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs index f1d2eabaa1..41bf920c9b 100644 --- a/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs +++ b/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs @@ -3,15 +3,7 @@ public enum OID : uint { Boss = 0x29C5, - _Gen_Phronesis = 0x29E7, // R0.500, x3 - _Gen_ = 0x2A1A, // R0.500, x0 (spawn during fight) - _Gen_1 = 0x2A1B, // R0.500, x0 (spawn during fight) - _Gen_2 = 0x2A1C, // R0.500, x0 (spawn during fight) - _Gen_3 = 0x2A19, // R0.500, x0 (spawn during fight) - _Gen_Hollow = 0x29C6, // R0.750-2.250, x0 (spawn during fight) - _Gen_4 = 0x2AC5, // R0.500, x0 (spawn during fight) - _Gen_5 = 0x2A1D, // R0.500, x0 (spawn during fight) - _Gen_LightningGlobe = 0x29C8, // R1.000, x0 (spawn during fight) + Hollow = 0x29C6, // R0.750-2.250, x0 (spawn during fight) } public enum AID : uint @@ -30,7 +22,7 @@ public enum AID : uint class SanctifiedBlizzardII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardII), new AOEShapeCircle(5)); class SanctifiedFireIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedFireIII), 6); class SanctifiedBlizzardIII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardIII), new AOEShapeCone(40.5f, 22.5f.Degrees())); -class Hollow(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID._Gen_Hollow)); +class Hollow(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.Hollow)); class HollowTether(BossModule module) : Components.Chains(module, 1, chainLength: 5); class SanctifiedFireIV(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SanctifiedFireIV1), 10); class SanctifiedFlare(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.SanctifiedFlare), 6, 1) @@ -48,7 +40,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme class LightningGlobe(BossModule module) : Components.GenericLineOfSightAOE(module, default, 100, false) { private readonly List Balls = []; - private IEnumerable<(WPos Center, float Radius)> Hollows => Module.Enemies(OID._Gen_Hollow).Select(h => (h.Position, h.HitboxRadius)); + private IEnumerable<(WPos Center, float Radius)> Hollows => Module.Enemies(OID.Hollow).Select(h => (h.Position, h.HitboxRadius)); public override void OnTethered(Actor source, ActorTetherInfo tether) { diff --git a/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs b/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs index 2a2292da1c..258a8bfde8 100644 --- a/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs +++ b/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs @@ -2,14 +2,12 @@ namespace BossMod.Shadowbringers.Quest.NyelbertsLament; -// TODO: add AI hint for the "enrage" + paladin safe zone - public enum OID : uint { Boss = 0x2977, Helper = 0x233C, BovianBull = 0x2976, - _Gen_LooseBoulder = 0x2978, // R2.400, x0 (spawn during fight) + LooseBoulder = 0x2978, // R2.400, x0 (spawn during fight) } public enum AID : uint @@ -56,7 +54,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) private void Refresh() { - var blockers = Module.Enemies(OID._Gen_LooseBoulder); + var blockers = Module.Enemies(OID.LooseBoulder); Modify(ActiveCaster?.CastInfo?.LocXZ, blockers.Select(b => (b.Position, b.HitboxRadius)), Module.CastFinishAt(ActiveCaster?.CastInfo)); } diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs b/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs index d828e9c33a..92b4469dfa 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs +++ b/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs @@ -68,7 +68,7 @@ protected override void Exec(Actor? primaryTarget) class TankbusterTether(BossModule module) : BossComponent(module) { private record class Tether(Actor Source, Actor Target, DateTime Activation); - private Tether? DwarfTether = null; + private Tether? DwarfTether; private bool Danger => DwarfTether?.Target.OID == 0x2917; diff --git a/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs b/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs index d895bae6f6..3a59b827e1 100644 --- a/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs +++ b/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs @@ -27,7 +27,7 @@ class WaterDrop(BossModule module) : Components.SpreadFromCastTargets(module, Ac class ExplosiveTataru(BossModule module) : BossComponent(module) { private readonly List Balls = []; - private Actor? Tataru = null; + private Actor? Tataru; public override void OnTethered(Actor source, ActorTetherInfo tether) { From b7a4f567458a0e9088570d5162c2253c839cfbd0 Mon Sep 17 00:00:00 2001 From: ace Date: Sat, 25 Jan 2025 15:02:51 -0800 Subject: [PATCH 21/33] opti --- BossMod/Autorotation/akechi/AkechiPLD.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BossMod/Autorotation/akechi/AkechiPLD.cs b/BossMod/Autorotation/akechi/AkechiPLD.cs index 7dbcd1b5cd..a781482859 100644 --- a/BossMod/Autorotation/akechi/AkechiPLD.cs +++ b/BossMod/Autorotation/akechi/AkechiPLD.cs @@ -325,6 +325,7 @@ public AID BestBlade public bool ShouldUseAOE; //Check if AOE rotation should be used public bool ShouldNormalHolyCircle; //Check if Holy Circle should be used public bool ShouldUseDMHolyCircle; //Check if Holy Circle should be used under Divine Might + public bool ShouldHoldDMandAC; //Check if Divine Might buff and Atonement combo should be held into Fight or Flight public AID NextGCD; //The next action to be executed during the global cooldown (for cartridge management) public bool canWeaveIn; //Can weave in oGCDs public bool canWeaveEarly; //Can early weave oGCDs @@ -405,6 +406,7 @@ public override void Execute(StrategyValues strategy, ref Actor? primaryTarget, ShouldUseAOE = TargetsHitByPlayerAOE() > 2; //Check if AOE rotation should be used ShouldNormalHolyCircle = !DivineMight.IsActive && TargetsHitByPlayerAOE() > 3; //Check if Holy Circle should be used (very niche) ShouldUseDMHolyCircle = DivineMight.IsActive && TargetsHitByPlayerAOE() > 2; //Check if Holy Circle should be used under Divine Might + ShouldHoldDMandAC = ComboLastMove is AID.RoyalAuthority ? FightOrFlight.CD < 5 : ComboLastMove is AID.FastBlade ? FightOrFlight.CD < 2.5 : ComboLastMove is AID.RiotBlade && FightOrFlight.CD < GCD; #region Strategy Options var AOE = strategy.Option(Track.AOE); //Retrieves AOE track @@ -805,7 +807,8 @@ public bool QueueAction(AID aid, Actor? target, float priority, float delay) Player.InCombat && //In combat target != null && //Target exists In3y(target) && //Target in range - Atonement.IsReady || Supplication.IsReady || Sepulchre.IsReady, //if any of the three are ready + !ShouldHoldDMandAC && + (Atonement.IsReady || Supplication.IsReady || Sepulchre.IsReady), //if any of the three are ready AtonementStrategy.ForceAtonement => Atonement.IsReady, //Force Atonement AtonementStrategy.ForceSupplication => Supplication.IsReady, //Force Supplication AtonementStrategy.ForceSepulchre => Sepulchre.IsReady, //Force Sepulchre @@ -830,6 +833,7 @@ public bool QueueAction(AID aid, Actor? target, float priority, float delay) target != null && //Target exists In25y(target) && //Target in range HolySpirit.IsReady && //can execute Holy Spirit + !ShouldHoldDMandAC && DivineMight.IsActive, //Divine Might is active _ => false }; From cc23baaf6830c70fa4e8b5bd0f97d030718810fc Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 12:44:53 +0000 Subject: [PATCH 22/33] More target filters --- BossMod/Autorotation/RotationModuleManager.cs | 47 ++++++++++++++-- BossMod/Autorotation/Strategy.cs | 27 ++++++++- BossMod/Autorotation/UIStrategyValue.cs | 56 ++++++++++++++----- .../Dawntrail/Ultimate/FRU/P2LightRampant.cs | 10 +++- 4 files changed, 118 insertions(+), 22 deletions(-) diff --git a/BossMod/Autorotation/RotationModuleManager.cs b/BossMod/Autorotation/RotationModuleManager.cs index dce34c33c1..2e329fb96f 100644 --- a/BossMod/Autorotation/RotationModuleManager.cs +++ b/BossMod/Autorotation/RotationModuleManager.cs @@ -116,8 +116,8 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) { StrategyTarget.Self => Player, StrategyTarget.PartyByAssignment => _prc.SlotsPerAssignment(WorldState.Party) is var spa && param < spa.Length ? WorldState.Party[spa[param]] : null, - StrategyTarget.PartyWithLowestHP => WorldState.Party.WithoutSlot().Exclude(param != 0 ? null : Player).MinBy(a => a.HPMP.CurHP), - StrategyTarget.EnemyWithHighestPriority => Player != null ? Hints.PriorityTargets.MinBy(e => (e.Actor.Position - Player.Position).LengthSq())?.Actor : null, + StrategyTarget.PartyWithLowestHP => FilteredPartyMembers((StrategyPartyFiltering)param).MinBy(a => a.HPMP.CurHP), + StrategyTarget.EnemyWithHighestPriority => Hints.PriorityTargets.MaxBy(RateEnemy((StrategyEnemySelection)param))?.Actor, StrategyTarget.EnemyByOID => Player != null && (uint)param is var oid && oid != 0 ? Hints.PotentialTargets.Where(e => e.Actor.OID == oid).MinBy(e => (e.Actor.Position - Player.Position).LengthSq())?.Actor : null, _ => null }; @@ -129,6 +129,47 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) _ => (ResolveTargetOverride(strategy, param)?.Position + off1 * off2.Degrees().ToDirection()) ?? Player?.Position ?? default, }; + public override string ToString() => string.Join(", ", _activeModules?.Select(m => m.Module.GetType().Name) ?? []); + + private IEnumerable FilteredPartyMembers(StrategyPartyFiltering filter) + { + var fullMask = new BitMask(~0ul); + var allowedMask = fullMask; + if (!filter.HasFlag(StrategyPartyFiltering.IncludeSelf)) + allowedMask.Clear(PlayerSlot); + if (filter.HasFlag(StrategyPartyFiltering.ExcludeNoPredictedDamage)) + { + var predictedDamage = Hints.PredictedDamage.Aggregate(default(BitMask), (s, p) => s | p.players); + allowedMask &= predictedDamage; + } + + if (allowedMask.None()) + return []; + var players = allowedMask != fullMask ? WorldState.Party.WithSlot().IncludedInMask(allowedMask).Actors() : WorldState.Party.WithoutSlot(); + if ((filter & (StrategyPartyFiltering.ExcludeTanks | StrategyPartyFiltering.ExcludeHealers | StrategyPartyFiltering.ExcludeMelee | StrategyPartyFiltering.ExcludeRanged)) != StrategyPartyFiltering.None) + { + players = players.Where(p => p.Role switch + { + Role.Tank => !filter.HasFlag(StrategyPartyFiltering.ExcludeTanks), + Role.Healer => !filter.HasFlag(StrategyPartyFiltering.ExcludeHealers), + Role.Melee => !filter.HasFlag(StrategyPartyFiltering.ExcludeMelee), + Role.Ranged => !filter.HasFlag(StrategyPartyFiltering.ExcludeRanged), + _ => true, + }); + } + return players; + } + + private Func RateEnemy(StrategyEnemySelection criterion) => criterion switch + { + StrategyEnemySelection.Closest => Player != null ? e => -(e.Actor.Position - Player.Position).LengthSq() : _ => 0, + StrategyEnemySelection.LowestCurHP => e => -e.Actor.HPMP.CurHP, + StrategyEnemySelection.HighestCurHP => e => e.Actor.HPMP.CurHP, + StrategyEnemySelection.LowestMaxHP => e => -e.Actor.HPMP.MaxHP, + StrategyEnemySelection.HighestMaxHP => e => e.Actor.HPMP.MaxHP, + _ => _ => 0 + }; + private Plan? CalculateExpectedPlan() { var player = Player; @@ -140,8 +181,6 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) return plans.SelectedIndex >= 0 ? plans.Plans[plans.SelectedIndex] : null; } - public override string ToString() => string.Join(", ", _activeModules?.Select(m => m.Module.GetType().Name) ?? []); - // TODO: consider not recreating modules that were active and continue to be active? private List RebuildActiveModules(List modules) where T : IRotationModuleData { diff --git a/BossMod/Autorotation/Strategy.cs b/BossMod/Autorotation/Strategy.cs index 06e9cb7242..368e4b5475 100644 --- a/BossMod/Autorotation/Strategy.cs +++ b/BossMod/Autorotation/Strategy.cs @@ -6,8 +6,8 @@ public enum StrategyTarget Automatic, // default 'smart' targeting, for hostile actions usually defaults to current primary target Self, PartyByAssignment, // parameter is assignment; won't work if assignments aren't set up properly for a party - PartyWithLowestHP, // parameter is whether self is allowed (1) or not (0) - EnemyWithHighestPriority, // selects closest if there are multiple + PartyWithLowestHP, // parameter is StrategyPartyFiltering, which filters subset of party members + EnemyWithHighestPriority, // parameter is StrategyEnemySelection, which determines selecton criteria if there are multiple matching enemies EnemyByOID, // parameter is oid; not really useful outside planner; selects closest if there are multiple PointAbsolute, // absolute x/y coordinates PointCenter, // offset from arena center @@ -15,6 +15,29 @@ public enum StrategyTarget Count } +// parameter for party member filtering +[Flags] +public enum StrategyPartyFiltering : int +{ + None = 0, + IncludeSelf = 1 << 0, + ExcludeTanks = 1 << 1, + ExcludeHealers = 1 << 2, + ExcludeMelee = 1 << 3, + ExcludeRanged = 1 << 4, + ExcludeNoPredictedDamage = 1 << 5, +} + +// parameter for prioritizing enemies +public enum StrategyEnemySelection : int +{ + Closest = 0, + LowestCurHP = 1, + HighestCurHP = 2, + LowestMaxHP = 3, + HighestMaxHP = 4, +} + // the tuning knobs of the rotation module are represented by strategy config rather than usual global config classes, since we they need to be changed dynamically by planner or manual input public record class StrategyConfig( Type OptionEnum, // type of the enum used for options diff --git a/BossMod/Autorotation/UIStrategyValue.cs b/BossMod/Autorotation/UIStrategyValue.cs index 6c47cea197..66078495fa 100644 --- a/BossMod/Autorotation/UIStrategyValue.cs +++ b/BossMod/Autorotation/UIStrategyValue.cs @@ -30,7 +30,8 @@ public static string PreviewTarget(ref StrategyValue value, BossModuleRegistry.I var targetDetails = value.Target switch { StrategyTarget.PartyByAssignment => ((PartyRolesConfig.Assignment)value.TargetParam).ToString(), - StrategyTarget.PartyWithLowestHP => $"{(value.TargetParam != 0 ? "include" : "exclude")} self", + StrategyTarget.PartyWithLowestHP => PreviewParam((StrategyPartyFiltering)value.TargetParam), + StrategyTarget.EnemyWithHighestPriority => $"{(StrategyEnemySelection)value.TargetParam}", StrategyTarget.EnemyByOID => $"{(moduleInfo?.ObjectIDType != null ? Enum.ToObject(moduleInfo.ObjectIDType, (uint)value.TargetParam).ToString() : "???")} (0x{value.TargetParam:X})", _ => "" }; @@ -157,23 +158,19 @@ public static bool DrawEditorTarget(ref StrategyValue value, ActionTargets suppo switch (value.Target) { case StrategyTarget.PartyByAssignment: - var assignment = (PartyRolesConfig.Assignment)value.TargetParam; - if (UICombo.Enum("Assignment", ref assignment)) - { - value.TargetParam = (int)assignment; - modified = true; - } + modified |= DrawEditorTargetParamCombo(ref value.TargetParam, "Assignment"); break; case StrategyTarget.PartyWithLowestHP: if (supportedTargets.HasFlag(ActionTargets.Self)) - { - var includeSelf = value.TargetParam != 0; - if (ImGui.Checkbox("Allow self", ref includeSelf)) - { - value.TargetParam = includeSelf ? 1 : 0; - modified = true; - } - } + modified |= DrawEditorTargetParamFlags(ref value.TargetParam, StrategyPartyFiltering.IncludeSelf, "Allow self", false); + modified |= DrawEditorTargetParamFlags(ref value.TargetParam, StrategyPartyFiltering.ExcludeTanks, "Allow tanks", true); + modified |= DrawEditorTargetParamFlags(ref value.TargetParam, StrategyPartyFiltering.ExcludeHealers, "Allow healers", true); + modified |= DrawEditorTargetParamFlags(ref value.TargetParam, StrategyPartyFiltering.ExcludeMelee, "Allow melee", true); + modified |= DrawEditorTargetParamFlags(ref value.TargetParam, StrategyPartyFiltering.ExcludeRanged, "Allow ranged", true); + modified |= DrawEditorTargetParamFlags(ref value.TargetParam, StrategyPartyFiltering.ExcludeNoPredictedDamage, "Only if more damage is expected", false); + break; + case StrategyTarget.EnemyWithHighestPriority: + modified |= DrawEditorTargetParamCombo(ref value.TargetParam, "Criterion"); break; case StrategyTarget.EnemyByOID: if (moduleInfo?.ObjectIDType != null) @@ -217,4 +214,33 @@ public static bool DrawEditorTarget(ref StrategyValue value, ActionTargets suppo StrategyTarget.PointAbsolute or StrategyTarget.PointCenter => false, _ => true }; + + private static string PreviewParam(StrategyPartyFiltering pf) + { + string excludeIfSet(StrategyPartyFiltering flag, string value) => pf.HasFlag(flag) ? $", exclude {value}" : ""; + return $"{(pf.HasFlag(StrategyPartyFiltering.IncludeSelf) ? "include" : "exclude")} self" + + excludeIfSet(StrategyPartyFiltering.ExcludeTanks, "tanks") + + excludeIfSet(StrategyPartyFiltering.ExcludeHealers, "healers") + + excludeIfSet(StrategyPartyFiltering.ExcludeMelee, "melee") + + excludeIfSet(StrategyPartyFiltering.ExcludeRanged, "ranged") + + excludeIfSet(StrategyPartyFiltering.ExcludeNoPredictedDamage, "players not expecting damage"); + } + + private static bool DrawEditorTargetParamCombo(ref int current, string text) where E : Enum + { + var value = (E)(object)current; + if (!UICombo.Enum(text, ref value)) + return false; + current = (int)(object)value; + return true; + } + + private static bool DrawEditorTargetParamFlags(ref int current, StrategyPartyFiltering flag, string text, bool inverted) + { + var isChecked = ((StrategyPartyFiltering)current).HasFlag(flag) != inverted; + if (!ImGui.Checkbox(text, ref isChecked)) + return false; + current ^= (int)flag; + return true; + } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs index 796b102d16..314aaf3797 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs @@ -33,7 +33,15 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) public readonly int[] BaitsPerPlayer = new int[PartyState.MaxPartySize]; public readonly WDir[] PrevBaitOffset = new WDir[PartyState.MaxPartySize]; - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // there are dedicated components for hints + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // note: movement hints are provided by dedicated components; this only marks targeted players as expecting to be damaged + BitMask predictedDamage = default; + foreach (var b in CurrentBaits) + predictedDamage.Set(Raid.FindSlot(b.Target.InstanceID)); + if (predictedDamage.Any()) + hints.PredictedDamage.Add((predictedDamage, CurrentBaits[0].Activation)); + } public override void OnEventCast(Actor caster, ActorCastEvent spell) { From cde4c700a6cfdf0d6f56d4e4106f5f929dcbb56c Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 13:08:58 +0000 Subject: [PATCH 23/33] Removed weird dependency from actor to aihints. --- BossMod/Autorotation/RotationModule.cs | 6 ++---- BossMod/Autorotation/xan/Basexan.cs | 3 ++- BossMod/Data/Actor.cs | 5 +---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/BossMod/Autorotation/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index 2ce41a5522..c08e4e3f5a 100644 --- a/BossMod/Autorotation/RotationModule.cs +++ b/BossMod/Autorotation/RotationModule.cs @@ -1,6 +1,4 @@ -using static BossMod.AIHints; - -namespace BossMod.Autorotation; +namespace BossMod.Autorotation; public enum RotationModuleQuality { @@ -121,7 +119,7 @@ public bool TraitUnlocked(uint id) return status != null ? (StatusDuration(status.Value.ExpireAt), status.Value.Extra & 0xFF) : (0, 0); } protected (float Left, int Stacks) StatusDetails(Actor? actor, SID sid, ulong sourceID, float pendingDuration = 1000) where SID : Enum => StatusDetails(actor, (uint)(object)sid, sourceID, pendingDuration); - protected (float Left, int Stacks) StatusDetails(Enemy? enemy, SID sid, ulong sourceID, float pendingDuration = 1000) where SID : Enum => StatusDetails(enemy?.Actor, (uint)(object)sid, sourceID, pendingDuration); + protected (float Left, int Stacks) StatusDetails(AIHints.Enemy? enemy, SID sid, ulong sourceID, float pendingDuration = 1000) where SID : Enum => StatusDetails(enemy?.Actor, (uint)(object)sid, sourceID, pendingDuration); protected (float Left, int Stacks) SelfStatusDetails(uint sid, float pendingDuration = 1000) => StatusDetails(Player, sid, Player.InstanceID, pendingDuration); protected (float Left, int Stacks) SelfStatusDetails(SID sid, float pendingDuration = 1000) where SID : Enum => StatusDetails(Player, sid, Player.InstanceID, pendingDuration); diff --git a/BossMod/Autorotation/xan/Basexan.cs b/BossMod/Autorotation/xan/Basexan.cs index 9a144dd89f..fafd2a7bc9 100644 --- a/BossMod/Autorotation/xan/Basexan.cs +++ b/BossMod/Autorotation/xan/Basexan.cs @@ -369,7 +369,7 @@ private void EstimateCastTime() { MaxCastTime = Hints.MaxCastTimeEstimate; - if (Player.PendingKnockbacks.Count > 0) + if (Player.PendingKnockbacks > 0) { MaxCastTime = 0f; return; @@ -522,4 +522,5 @@ public static RotationModuleDefinition.ConfigRef DefineSimple public static OffensiveStrategy Simple(this StrategyValues strategy, Index track) where Index : Enum => strategy.Option(track).As(); public static bool BuffsOk(this StrategyValues strategy) => strategy.Option(SharedTrack.Buffs).As() != OffensiveStrategy.Delay; public static bool AOEOk(this StrategyValues strategy) => strategy.AOE() is AOEStrategy.AOE or AOEStrategy.ForceAOE; + public static float DistanceToHitbox(this Actor actor, Enemy? other) => actor.DistanceToHitbox(other?.Actor); } diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index 755de448c5..648710bdd0 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -1,6 +1,4 @@ -using static BossMod.AIHints; - -namespace BossMod; +namespace BossMod; // objkind << 8 + objsubkind public enum ActorType : ushort @@ -168,7 +166,6 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public Angle AngleTo(Actor other) => Angle.FromDirection(other.Position - Position); public float DistanceToHitbox(Actor? other) => other == null ? float.MaxValue : (other.Position - Position).Length() - other.HitboxRadius - HitboxRadius; - public float DistanceToHitbox(Enemy? other) => DistanceToHitbox(other?.Actor); public override string ToString() => $"{OID:X} '{Name}' <{InstanceID:X}>"; } From c7748d5e74b50049750a17ac5e9fad0b29f86fe1 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 14:30:14 +0000 Subject: [PATCH 24/33] Stylistic changes. --- BossMod/ActionQueue/ActionDefinition.cs | 28 ++----- BossMod/Data/DeepDungeonState.cs | 96 ++++++++++------------ BossMod/Framework/Utils.cs | 9 +++ BossMod/Framework/WorldStateGameSync.cs | 102 +++++++++--------------- BossMod/Replay/ReplayParserLog.cs | 42 ++++------ 5 files changed, 117 insertions(+), 160 deletions(-) diff --git a/BossMod/ActionQueue/ActionDefinition.cs b/BossMod/ActionQueue/ActionDefinition.cs index 5da6a08a03..a730dfae45 100644 --- a/BossMod/ActionQueue/ActionDefinition.cs +++ b/BossMod/ActionQueue/ActionDefinition.cs @@ -219,30 +219,13 @@ private ActionDefinitions() RegisterPotion(IDPotionInt); RegisterPotion(IDPotionMnd); - // bozja actions + // special content actions - bozja, deep dungeons, etc for (var i = BozjaHolsterID.None + 1; i < BozjaHolsterID.Count; ++i) RegisterBozja(i); - - // pomanders for (var i = PomanderID.Safety; i < PomanderID.Count; ++i) - { - var pid = new ActionID(ActionType.Pomander, (uint)i); - _definitions[pid] = new(pid) - { - InstantAnimLock = 2.1f, - AllowedTargets = ActionTargets.Self - }; - } - + RegisterDeepDungeon(new(ActionType.Pomander, (uint)i)); for (var i = 1u; i <= 3; i++) - { - var mid = new ActionID(ActionType.Magicite, i); - _definitions[mid] = new(mid) - { - InstantAnimLock = 2.1f, - AllowedTargets = ActionTargets.Self - }; - } + RegisterDeepDungeon(new(ActionType.Magicite, i)); } public void Dispose() @@ -442,6 +425,11 @@ private void RegisterBozja(BozjaHolsterID id) } } + private void RegisterDeepDungeon(ActionID id) + { + _definitions[id] = new(id) { AllowedTargets = ActionTargets.Self, InstantAnimLock = 2.1f }; + } + // hardcoded mechanic implementations public void RegisterChargeIncreaseTrait(ActionID aid, uint traitId) { diff --git a/BossMod/Data/DeepDungeonState.cs b/BossMod/Data/DeepDungeonState.cs index 6dbc9fb2d4..622c3ee9f2 100644 --- a/BossMod/Data/DeepDungeonState.cs +++ b/BossMod/Data/DeepDungeonState.cs @@ -1,17 +1,9 @@ -using static FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon; +using RoomFlags = FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon.RoomFlags; namespace BossMod; public sealed class DeepDungeonState { - public DungeonProgress Progress; - public byte DungeonId; - public RoomFlags[] MapData = new RoomFlags[25]; - public PartyMember[] Party = new PartyMember[4]; - public Item[] Items = new Item[16]; - public Chest[] Chests = new Chest[16]; - public byte[] Magicite = new byte[3]; - public enum DungeonType : byte { None = 0, @@ -20,56 +12,54 @@ public enum DungeonType : byte EO = 3 } - public DungeonType Type => (DungeonType)DungeonId; - - public record struct DungeonProgress(byte Floor, byte Tileset, byte WeaponLevel, byte ArmorLevel, byte SyncedGearLevel, byte HoardCount, byte ReturnProgress, byte PassageProgress) - { - public readonly bool IsBossFloor => Floor % 10 == 0; - } - public record struct PartyMember(ulong EntityId, byte Room); - public record struct Item(byte Count, byte Flags) + public readonly record struct DungeonProgress(byte Floor, byte Tileset, byte WeaponLevel, byte ArmorLevel, byte SyncedGearLevel, byte HoardCount, byte ReturnProgress, byte PassageProgress); + public readonly record struct PartyMember(ulong EntityId, byte Room); + public readonly record struct PomanderState(byte Count, byte Flags) { public readonly bool Usable => (Flags & (1 << 0)) != 0; public readonly bool Active => (Flags & (1 << 1)) != 0; } - public record struct Chest(byte Type, byte Room); + public readonly record struct Chest(byte Type, byte Room); - public Item GetItem(PomanderID pid) => GetSlotForPomander(pid) is var s && s >= 0 ? Items[s] : default; + public const int NumRooms = 25; + public const int NumPartyMembers = 4; + public const int NumPomanderSlots = 16; + public const int NumChests = 16; + public const int NumMagicites = 3; - public int GetSlotForPomander(PomanderID pid) => Service.LuminaRow(DungeonId)!.Value.PomanderSlot.ToList().FindIndex(p => p.RowId == (uint)pid); - public PomanderID GetPomanderForSlot(int slot) - { - var slots = Service.LuminaRow(DungeonId)!.Value.PomanderSlot; - return slot >= 0 && slot < slots.Count ? (PomanderID)slots[slot].RowId : PomanderID.None; - } + public DungeonType DungeonId; + public DungeonProgress Progress; + public readonly RoomFlags[] Rooms = new RoomFlags[NumRooms]; + public readonly PartyMember[] Party = new PartyMember[NumPartyMembers]; + public readonly PomanderState[] Pomanders = new PomanderState[NumPomanderSlots]; + public readonly Chest[] Chests = new Chest[NumChests]; + public readonly byte[] Magicite = new byte[NumMagicites]; public bool ReturnActive => Progress.ReturnProgress >= 11; public bool PassageActive => Progress.PassageProgress >= 11; public byte Floor => Progress.Floor; + public bool IsBossFloor => Progress.Floor % 10 == 0; + + public Lumina.Excel.Sheets.DeepDungeon GetDungeonDefinition() => Service.LuminaRow((uint)DungeonId)!.Value; + public int GetPomanderSlot(PomanderID pid) => GetDungeonDefinition().PomanderSlot.FindIndex(p => p.RowId == (uint)pid); + public PomanderState GetPomanderState(PomanderID pid) => GetPomanderSlot(pid) is var s && s >= 0 ? Pomanders[s] : default; + public PomanderID GetPomanderID(int slot) => GetDungeonDefinition().PomanderSlot is var slots && slot >= 0 && slot < slots.Count ? (PomanderID)slots[slot].RowId : PomanderID.None; public IEnumerable CompareToInitial() { - if (Progress != default || DungeonId != 0) + if (DungeonId != DungeonType.None) + { yield return new OpProgressChange(DungeonId, Progress); - - if (MapData.Any(m => m > 0)) - yield return new OpMapDataChange(MapData); - - if (Party.Any(p => p != default)) + yield return new OpMapDataChange(Rooms); yield return new OpPartyStateChange(Party); - - if (Items.Any(i => i != default)) - yield return new OpItemsChange(Items); - - if (Chests.Any(c => c != default)) + yield return new OpPomandersChange(Pomanders); yield return new OpChestsChange(Chests); - - if (Magicite.Any(c => c > 0)) yield return new OpMagiciteChange(Magicite); + } } public Event ProgressChanged = new(); - public sealed record class OpProgressChange(byte DungeonId, DungeonProgress Value) : WorldState.Operation + public sealed record class OpProgressChange(DungeonType DungeonId, DungeonProgress Value) : WorldState.Operation { protected override void Exec(WorldState ws) { @@ -80,7 +70,7 @@ protected override void Exec(WorldState ws) public override void Write(ReplayRecorder.Output output) { output.EmitFourCC("DDPG"u8) - .Emit(DungeonId) + .Emit((byte)DungeonId) .Emit(Value.Floor) .Emit(Value.Tileset) .Emit(Value.WeaponLevel) @@ -93,18 +83,20 @@ public override void Write(ReplayRecorder.Output output) } public Event MapDataChanged = new(); - public sealed record class OpMapDataChange(RoomFlags[] Value) : WorldState.Operation + public sealed record class OpMapDataChange(RoomFlags[] Rooms) : WorldState.Operation { - public readonly RoomFlags[] Value = Value; + public readonly RoomFlags[] Rooms = Rooms; protected override void Exec(WorldState ws) { - ws.DeepDungeon.MapData = Value; + Array.Copy(Rooms, ws.DeepDungeon.Rooms, NumRooms); ws.DeepDungeon.MapDataChanged.Fire(this); } public override void Write(ReplayRecorder.Output output) { - output.EmitFourCC("DDMP"u8).Emit(Array.ConvertAll(Value, r => (byte)r)); + output.EmitFourCC("DDMP"u8); + foreach (var r in Rooms) + output.Emit((byte)r, "X2"); } } @@ -115,7 +107,7 @@ public sealed record class OpPartyStateChange(PartyMember[] Value) : WorldState. protected override void Exec(WorldState ws) { - ws.DeepDungeon.Party = Value; + Array.Copy(Value, ws.DeepDungeon.Party, NumPartyMembers); ws.DeepDungeon.PartyStateChanged.Fire(this); } public override void Write(ReplayRecorder.Output output) @@ -126,15 +118,15 @@ public override void Write(ReplayRecorder.Output output) } } - public Event ItemsChanged = new(); - public sealed record class OpItemsChange(Item[] Value) : WorldState.Operation + public Event PomandersChanged = new(); + public sealed record class OpPomandersChange(PomanderState[] Value) : WorldState.Operation { - public readonly Item[] Value = Value; + public readonly PomanderState[] Value = Value; protected override void Exec(WorldState ws) { - ws.DeepDungeon.Items = Value; - ws.DeepDungeon.ItemsChanged.Fire(this); + Array.Copy(Value, ws.DeepDungeon.Pomanders, NumPomanderSlots); + ws.DeepDungeon.PomandersChanged.Fire(this); } public override void Write(ReplayRecorder.Output output) { @@ -151,7 +143,7 @@ public sealed record class OpChestsChange(Chest[] Value) : WorldState.Operation protected override void Exec(WorldState ws) { - ws.DeepDungeon.Chests = Value; + Array.Copy(Value, ws.DeepDungeon.Chests, NumChests); ws.DeepDungeon.ChestsChanged.Fire(this); } public override void Write(ReplayRecorder.Output output) @@ -169,7 +161,7 @@ public sealed record class OpMagiciteChange(byte[] Value) : WorldState.Operation protected override void Exec(WorldState ws) { - ws.DeepDungeon.Magicite = Value; + Array.Copy(Value, ws.DeepDungeon.Magicite, NumMagicites); ws.DeepDungeon.MagiciteChanged.Fire(this); } public override void Write(ReplayRecorder.Output output) diff --git a/BossMod/Framework/Utils.cs b/BossMod/Framework/Utils.cs index 940dfea2a7..c8dbf26a7b 100644 --- a/BossMod/Framework/Utils.cs +++ b/BossMod/Framework/Utils.cs @@ -50,6 +50,15 @@ public static string ObjectKindString(IGameObject obj) public static unsafe ulong SceneObjectFlags(FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object* o) => ReadField(o, 0x38); + // lumina extensions + public static int FindIndex(this Lumina.Excel.Collection collection, Func predicate) where T : struct + { + for (int i = 0; i < collection.Count; ++i) + if (predicate(collection[i])) + return i; + return -1; + } + // backport from .net 6, except that it doesn't throw on empty enumerable... public static TSource? MinBy(this IEnumerable source, Func keySelector) where TKey : IComparable { diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs index 46b52d3333..f9f8974567 100644 --- a/BossMod/Framework/WorldStateGameSync.cs +++ b/BossMod/Framework/WorldStateGameSync.cs @@ -666,83 +666,59 @@ private unsafe void UpdateClient() } private unsafe void UpdateDeepDungeon() - { - var ddold = _ws.DeepDungeon; - var ddnew = GetDeepDungeonState(); - - if (ddold.DungeonId != ddnew.DungeonId || ddold.Progress != ddnew.Progress) - _ws.Execute(new DeepDungeonState.OpProgressChange(ddnew.DungeonId, ddnew.Progress)); - if (!MemoryExtensions.SequenceEqual(ddold.MapData, ddnew.MapData)) - _ws.Execute(new DeepDungeonState.OpMapDataChange(ddnew.MapData)); - if (!MemoryExtensions.SequenceEqual(ddold.Party, ddnew.Party)) - _ws.Execute(new DeepDungeonState.OpPartyStateChange(ddnew.Party)); - if (!MemoryExtensions.SequenceEqual(ddold.Items, ddnew.Items)) - _ws.Execute(new DeepDungeonState.OpItemsChange(ddnew.Items)); - if (!MemoryExtensions.SequenceEqual(ddold.Chests, ddnew.Chests)) - _ws.Execute(new DeepDungeonState.OpChestsChange(ddnew.Chests)); - if (!MemoryExtensions.SequenceEqual(ddold.Magicite, ddnew.Magicite)) - _ws.Execute(new DeepDungeonState.OpMagiciteChange(ddnew.Magicite)); - } - - private unsafe DeepDungeonState GetDeepDungeonState() { var dd = EventFramework.Instance()->GetInstanceContentDeepDungeon(); - if (dd == null) - return new(); - - var progress = new DeepDungeonState.DungeonProgress + if (dd != null) { - Floor = dd->Floor, - WeaponLevel = dd->WeaponLevel, - ArmorLevel = dd->ArmorLevel, + var currentId = (DeepDungeonState.DungeonType)dd->DeepDungeonId; + var fullUpdate = currentId != _ws.DeepDungeon.DungeonId; - SyncedGearLevel = dd->SyncedGearLevel, - HoardCount = dd->HoardCount, + var progress = new DeepDungeonState.DungeonProgress(dd->Floor, dd->ActiveLayoutIndex, dd->WeaponLevel, dd->ArmorLevel, dd->SyncedGearLevel, dd->HoardCount, dd->ReturnProgress, dd->PassageProgress); + if (fullUpdate || progress != _ws.DeepDungeon.Progress) + _ws.Execute(new DeepDungeonState.OpProgressChange(currentId, progress)); - ReturnProgress = dd->ReturnProgress, - PassageProgress = dd->PassageProgress, + if (fullUpdate || !MemoryExtensions.SequenceEqual(_ws.DeepDungeon.Rooms.AsSpan(), dd->MapData)) + _ws.Execute(new DeepDungeonState.OpMapDataChange(dd->MapData.ToArray())); - Tileset = dd->ActiveLayoutIndex - }; - - var state = new DeepDungeonState - { - Progress = progress, - Magicite = dd->Magicite.ToArray(), - DungeonId = dd->DeepDungeonId - }; + Span party = stackalloc DeepDungeonState.PartyMember[DeepDungeonState.NumPartyMembers]; + for (var i = 0; i < DeepDungeonState.NumPartyMembers; ++i) + { + ref var p = ref dd->Party[i]; + party[i] = new(SanitizedObjectID(p.EntityId), SanitizeDeepDungeonRoom(p.RoomIndex)); + } + if (fullUpdate || !MemoryExtensions.SequenceEqual(_ws.DeepDungeon.Party.AsSpan(), party)) + _ws.Execute(new DeepDungeonState.OpPartyStateChange(party.ToArray())); - dd->MapData.CopyTo(state.MapData); + Span pomanders = stackalloc DeepDungeonState.PomanderState[DeepDungeonState.NumPomanderSlots]; + for (var i = 0; i < DeepDungeonState.NumPomanderSlots; ++i) + { + ref var item = ref dd->Items[i]; + pomanders[i] = new(item.Count, item.Flags); + } + if (fullUpdate || !MemoryExtensions.SequenceEqual(_ws.DeepDungeon.Pomanders.AsSpan(), pomanders)) + _ws.Execute(new DeepDungeonState.OpPomandersChange(pomanders.ToArray())); - var ddParty = dd->Party; - for (var i = 0; i < 4; i++) - { - ref var pinfo = ref state.Party[i]; - pinfo.EntityId = (uint)SanitizedObjectID(ddParty[i].EntityId); - pinfo.Room = SanitizeRoom(ddParty[i].RoomIndex); - } + Span chests = stackalloc DeepDungeonState.Chest[DeepDungeonState.NumChests]; + for (var i = 0; i < DeepDungeonState.NumChests; ++i) + { + ref var c = ref dd->Chests[i]; + chests[i] = new(c.ChestType, SanitizeDeepDungeonRoom(c.RoomIndex)); + } + if (fullUpdate || !MemoryExtensions.SequenceEqual(_ws.DeepDungeon.Chests.AsSpan(), chests)) + _ws.Execute(new DeepDungeonState.OpChestsChange(chests.ToArray())); - var ddItem = dd->Items; - for (var i = 0; i < ddItem.Length; i++) - { - ref var pitem = ref state.Items[i]; - pitem.Count = ddItem[i].Count; - pitem.Flags = ddItem[i].Flags; + if (fullUpdate || !MemoryExtensions.SequenceEqual(_ws.DeepDungeon.Magicite.AsSpan(), dd->Magicite)) + _ws.Execute(new DeepDungeonState.OpMagiciteChange(dd->Magicite.ToArray())); } - - var ddChest = dd->Chests; - for (var i = 0; i < ddChest.Length; i++) + else if (_ws.DeepDungeon.DungeonId != DeepDungeonState.DungeonType.None) { - ref var pchest = ref state.Chests[i]; - pchest.Type = ddChest[i].ChestType; - pchest.Room = SanitizeRoom(ddChest[i].RoomIndex); + // exiting deep dungeon, clean up all state + _ws.Execute(new DeepDungeonState.OpProgressChange(DeepDungeonState.DungeonType.None, default)); } - - return state; + // else: we were and still are outside deep dungeon, nothing to do } - private byte SanitizeRoom(sbyte room) => room < 0 ? (byte)0 : (byte)room; - + private byte SanitizeDeepDungeonRoom(sbyte room) => room < 0 ? (byte)0 : (byte)room; private ulong SanitizedObjectID(ulong raw) => raw != InvalidEntityId ? raw : 0; private void DispatchActorEvents(ulong instanceID) diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index a8b63ca187..bcf4baa76b 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -1,5 +1,4 @@ -using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; -using System.Globalization; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Threading; @@ -355,7 +354,7 @@ private ReplayParserLog(Input input, ReplayBuilder builder) [new("DDPG"u8)] = ParseDeepDungeonProgress, [new("DDMP"u8)] = ParseDeepDungeonMap, [new("DDPT"u8)] = ParseDeepDungeonParty, - [new("DDIT"u8)] = ParseDeepDungeonItems, + [new("DDIT"u8)] = ParseDeepDungeonPomanders, [new("DDCT"u8)] = ParseDeepDungeonChests, [new("DDMG"u8)] = ParseDeepDungeonMagicite, [new("IPCI"u8)] = ParseNetworkIDScramble, @@ -705,42 +704,35 @@ private ClientState.OpClassJobLevelsChange ParseClientClassJobLevels() private ClientState.OpActivePetChange ParseClientActivePet() => new(new(_input.ReadULong(true), _input.ReadByte(false), _input.ReadByte(false))); private ClientState.OpFocusTargetChange ParseClientFocusTarget() => new(_input.ReadULong(true)); - private DeepDungeonState.OpProgressChange ParseDeepDungeonProgress() => new(_input.ReadByte(false), new DeepDungeonState.DungeonProgress(_input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false))); - private DeepDungeonState.OpMapDataChange ParseDeepDungeonMap() => new(Array.ConvertAll(_input.ReadBytes(), b => (InstanceContentDeepDungeon.RoomFlags)b)); + private DeepDungeonState.OpProgressChange ParseDeepDungeonProgress() => new((DeepDungeonState.DungeonType)_input.ReadByte(false), new(_input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false))); + private DeepDungeonState.OpMapDataChange ParseDeepDungeonMap() + { + var rooms = new FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon.RoomFlags[DeepDungeonState.NumRooms]; + for (var i = 0; i < rooms.Length; ++i) + rooms[i] = (FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon.RoomFlags)_input.ReadByte(true); + return new(rooms); + } private DeepDungeonState.OpPartyStateChange ParseDeepDungeonParty() { - var pt = new DeepDungeonState.PartyMember[4]; + var pt = new DeepDungeonState.PartyMember[DeepDungeonState.NumPartyMembers]; for (var i = 0; i < pt.Length; i++) - { - ref var p = ref pt[i]; - p.EntityId = _input.ReadActorID(); - p.Room = _input.ReadByte(false); - } + pt[i] = new(_input.ReadActorID(), _input.ReadByte(false)); return new(pt); } - private DeepDungeonState.OpItemsChange ParseDeepDungeonItems() + private DeepDungeonState.OpPomandersChange ParseDeepDungeonPomanders() { - var it = new DeepDungeonState.Item[16]; + var it = new DeepDungeonState.PomanderState[DeepDungeonState.NumPomanderSlots]; for (var i = 0; i < it.Length; i++) - { - ref var item = ref it[i]; - item.Count = _input.ReadByte(false); - item.Flags = _input.ReadByte(true); - } + it[i] = new(_input.ReadByte(false), _input.ReadByte(true)); return new(it); } private DeepDungeonState.OpChestsChange ParseDeepDungeonChests() { - var ct = new DeepDungeonState.Chest[16]; + var ct = new DeepDungeonState.Chest[DeepDungeonState.NumChests]; for (var i = 0; i < ct.Length; i++) - { - ref var chest = ref ct[i]; - chest.Type = _input.ReadByte(false); - chest.Room = _input.ReadByte(false); - } + ct[i] = new(_input.ReadByte(false), _input.ReadByte(false)); return new(ct); } - private DeepDungeonState.OpMagiciteChange ParseDeepDungeonMagicite() => new(_input.ReadBytes()); private NetworkState.OpIDScramble ParseNetworkIDScramble() => new(_input.ReadUInt(false)); From da7bd214a0d28353771d3fae780f5f9563866cbb Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 14:40:24 +0000 Subject: [PATCH 25/33] Moved rotation module yet again. --- BossMod/Components/RotationModule.cs | 9 --------- .../Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs | 2 +- .../Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs | 2 +- .../Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs | 2 +- BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs | 2 +- BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs | 2 +- .../Quest/SleepNowInSapphire/P1GuidanceSystem.cs | 2 +- BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs | 2 +- BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs | 2 +- .../Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs | 2 +- .../Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs | 2 +- BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs | 2 +- BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs | 2 +- BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs | 2 +- 14 files changed, 13 insertions(+), 22 deletions(-) delete mode 100644 BossMod/Components/RotationModule.cs diff --git a/BossMod/Components/RotationModule.cs b/BossMod/Components/RotationModule.cs deleted file mode 100644 index 9f8084d5eb..0000000000 --- a/BossMod/Components/RotationModule.cs +++ /dev/null @@ -1,9 +0,0 @@ -using BossMod.QuestBattle; - -namespace BossMod.Components; - -public abstract class RotationModule(BossModule module) : BossComponent(module) where R : UnmanagedRotation -{ - private readonly R _rotation = New.Constructor()(module.WorldState); - public sealed override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) => _rotation.Execute(actor, hints); -} diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs index bdb2da0d87..b36a861858 100644 --- a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs @@ -13,7 +13,7 @@ enum OID : uint Boss = 0x3376 } -class AlisaieAI(BossModule module) : Components.RotationModule(module); +class AlisaieAI(BossModule module) : QuestBattle.RotationModule(module); class AntiPersonnelMissile(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.AntiPersonnelMissile), 6); class MRVMissile(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MRVMissile), 12, maxCasts: 6); diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs index a0768d5224..ebc0e28f31 100644 --- a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs @@ -59,7 +59,7 @@ protected override void Exec(Actor? primaryTarget) } class Fetters(BossModule module) : Components.Adds(module, (uint)OID.Fetters); -class AutoUri(BossModule module) : Components.RotationModule(module); +class AutoUri(BossModule module) : RotationModule(module); class GunmetalSoul(BossModule module) : Components.GenericAOEs(module) { public override IEnumerable ActiveAOEs(int slot, Actor actor) => Module.Enemies(0x1EB1D5).Where(e => e.EventState != 7).Select(e => new AOEInstance(new AOEShapeDonut(4, 100), e.Position)); diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs index 21aa217e5f..f6bdd8e466 100644 --- a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs @@ -71,7 +71,7 @@ protected override void Exec(Actor? primaryTarget) } } -class AutoGraha(BossModule module) : Components.RotationModule(module); +class AutoGraha(BossModule module) : RotationModule(module); class DirectionalParry(BossModule module) : Components.DirectionalParry(module, 0x3201) { public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) diff --git a/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs b/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs index d77256e6f8..6e464d9242 100644 --- a/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs +++ b/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs @@ -54,7 +54,7 @@ class HotPursuit(BossModule module) : Components.LocationTargetedAOEs(module, Ac class CoiledLevin(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CoiledLevin1), new AOEShapeCircle(6)); class LightningVoidzone(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.LightningVoidzone).Where(x => x.EventState != 7)); -class ThancredAI(BossModule module) : Components.RotationModule(module); +class ThancredAI(BossModule module) : RotationModule(module); class AutoThancred(WorldState ws) : UnmanagedRotation(ws, 3) { diff --git a/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs b/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs index 258a8bfde8..f8bbffa5d0 100644 --- a/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs +++ b/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs @@ -94,7 +94,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) } } -class NyelbertAI(BossModule module) : Components.RotationModule(module); +class NyelbertAI(BossModule module) : QuestBattle.RotationModule(module); class BovianStates : StateMachineBuilder { diff --git a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs index 81930eb7aa..9395f1677e 100644 --- a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs +++ b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs @@ -15,7 +15,7 @@ public enum AID : uint class AerialBombardment(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AerialBombardment), 12); -class GWarrior(BossModule module) : Components.RotationModule(module); +class GWarrior(BossModule module) : QuestBattle.RotationModule(module); class GuidanceSystemStates : StateMachineBuilder { diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs b/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs index 92b4469dfa..9c23a90128 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs +++ b/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs @@ -103,7 +103,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class BrandenAI(BossModule module) : Components.RotationModule(module); +class BrandenAI(BossModule module) : RotationModule(module); class RustingClaw(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RustingClaw), new AOEShapeCone(10.3f, 45.Degrees())); diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs b/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs index 5ab0e350f1..4aa94e49a9 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs +++ b/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs @@ -50,7 +50,7 @@ protected override void Exec(Actor? primaryTarget) } } -class RendaRaeAI(BossModule module) : Components.RotationModule(module); +class RendaRaeAI(BossModule module) : RotationModule(module); class RonkanAura(BossModule module) : BossComponent(module) { diff --git a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs index a230df22b4..d6806f5362 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs +++ b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs @@ -67,7 +67,7 @@ protected override void Exec(Actor? primaryTarget) } } -class AutoLamitt(BossModule module) : Components.RotationModule(module); +class AutoLamitt(BossModule module) : RotationModule(module); class YxtliltonStates : StateMachineBuilder { diff --git a/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs b/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs index cd20c4d00d..b8d1e24348 100644 --- a/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs +++ b/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs @@ -99,7 +99,7 @@ protected override void Exec(Actor? primaryTarget) } } -class AutoEstinien(BossModule module) : Components.RotationModule(module); +class AutoEstinien(BossModule module) : RotationModule(module); class ArchUltimaStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs index 78509c493d..52ef8c972b 100644 --- a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs +++ b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs @@ -37,7 +37,7 @@ protected override void Exec(Actor? primaryTarget) } } -class HienAI(BossModule module) : Components.RotationModule(module); +class HienAI(BossModule module) : RotationModule(module); public class ZenosP1States : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs b/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs index 294ed5aa69..9936b94e45 100644 --- a/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs +++ b/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs @@ -8,7 +8,7 @@ public enum OID : uint Helper = 0x233C, } -class AlphiAI(BossModule module) : Components.RotationModule(module); +class AlphiAI(BossModule module) : QuestBattle.RotationModule(module); class LB(BossModule module) : BossComponent(module) { diff --git a/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs b/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs index 800ecc9340..b3da138e51 100644 --- a/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs +++ b/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs @@ -101,7 +101,7 @@ protected override void Exec(Actor? primaryTarget) } } -class YshtolaAI(BossModule module) : Components.RotationModule(module); +class YshtolaAI(BossModule module) : RotationModule(module); class P1Hints(BossModule module) : BossComponent(module) { From f51da496255520344f2b63b0fdaeb058134e4706 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 14:57:20 +0000 Subject: [PATCH 26/33] Replay version bump, so that I can write converters for DD later. --- BossMod/Replay/ReplayRecorder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BossMod/Replay/ReplayRecorder.cs b/BossMod/Replay/ReplayRecorder.cs index 002cf9ed76..cef4662b2d 100644 --- a/BossMod/Replay/ReplayRecorder.cs +++ b/BossMod/Replay/ReplayRecorder.cs @@ -176,7 +176,7 @@ public override void EndEntry() { } private readonly Output _logger; private readonly EventSubscription _subscription; - public const int Version = 22; + public const int Version = 23; public ReplayRecorder(WorldState ws, ReplayLogFormat format, bool logInitialState, DirectoryInfo targetDirectory, string logPrefix) { From 239dcad4e961c3e1ff3487d2b4d94a572a41d494 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 16:38:17 +0000 Subject: [PATCH 27/33] DD replay conversion. --- BossMod/Replay/ReplayParserLog.cs | 12 ++++++++++-- BossMod/Replay/Visualization/EventList.cs | 7 +++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index bcf4baa76b..b4fff9141e 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -708,8 +708,16 @@ private ClientState.OpClassJobLevelsChange ParseClientClassJobLevels() private DeepDungeonState.OpMapDataChange ParseDeepDungeonMap() { var rooms = new FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon.RoomFlags[DeepDungeonState.NumRooms]; - for (var i = 0; i < rooms.Length; ++i) - rooms[i] = (FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon.RoomFlags)_input.ReadByte(true); + if (_version < 23) + { + var raw = _input.ReadBytes(); + Array.Copy(raw, rooms, raw.Length); + } + else + { + for (var i = 0; i < rooms.Length; ++i) + rooms[i] = (FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon.RoomFlags)_input.ReadByte(true); + } return new(rooms); } private DeepDungeonState.OpPartyStateChange ParseDeepDungeonParty() diff --git a/BossMod/Replay/Visualization/EventList.cs b/BossMod/Replay/Visualization/EventList.cs index 968c6bd5d6..9d7a4e87e5 100644 --- a/BossMod/Replay/Visualization/EventList.cs +++ b/BossMod/Replay/Visualization/EventList.cs @@ -124,9 +124,12 @@ private void DrawContents(Replay.Encounter? filter, BossModuleRegistry.Info? mod foreach (var n in _tree.Node("EnvControls", !envControls.Any())) { - foreach (var n2 in _tree.Node("All")) + if (envControls.Any()) { - _tree.LeafNodes(envControls, ec => $"{tp(ec.Timestamp)}: {ec.Index:X2} = {ec.State:X8}"); + foreach (var n2 in _tree.Node("All")) + { + _tree.LeafNodes(envControls, ec => $"{tp(ec.Timestamp)}: {ec.Index:X2} = {ec.State:X8}"); + } } foreach (var index in _tree.Nodes(new SortedSet(envControls.Select(ec => ec.Index)), index => new($"Index {index:X2}"))) { From 9b53c04141d382d890d74b09854969ad0afbee63 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:05:16 -0500 Subject: [PATCH 28/33] fix steps of faith class name --- BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs b/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs index 98729f1aad..ec19d7d21a 100644 --- a/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs +++ b/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs @@ -249,7 +249,7 @@ public VishapStates(BossModule module) : base(module) } [ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70127, NameID = 3330)] -public class TheStepsOfFaith(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 245), ScrollingBounds.Bounds) +public class Vishap(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 245), ScrollingBounds.Bounds) { // vishap doesn't start targetable protected override bool CheckPull() => PrimaryActor.InCombat; From 05bbfc2c82a77b07ef12bc60152f92a5e9b00223 Mon Sep 17 00:00:00 2001 From: xanunderscore <149614526+xanunderscore@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:05:44 -0500 Subject: [PATCH 29/33] fix MNK in default preset --- BossMod/DefaultRotationPresets.json | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/BossMod/DefaultRotationPresets.json b/BossMod/DefaultRotationPresets.json index b51145be60..29c8777a46 100644 --- a/BossMod/DefaultRotationPresets.json +++ b/BossMod/DefaultRotationPresets.json @@ -44,11 +44,29 @@ ], "BossMod.Autorotation.xan.MNK": [ { - "Track": "Buffs", + "Track": "RoF", "Option": "Auto" }, { - "Track": "Buffs", + "Track": "BH", + "Option": "Auto" + }, + { + "Track": "RoW", + "Option": "Auto" + }, + { + "Track": "RoF", + "Option": "Delay", + "Mod": "Shift, Ctrl" + }, + { + "Track": "BH", + "Option": "Delay", + "Mod": "Shift, Ctrl" + }, + { + "Track": "RoW", "Option": "Delay", "Mod": "Shift, Ctrl" }, From f4376dd258892935a9be7b7e9099b3cd0a72691f Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 18:26:09 +0000 Subject: [PATCH 30/33] JSON conversion utility. --- BossMod/BossModule/BossModuleRegistry.cs | 2 +- BossMod/BossModule/ZoneModuleRegistry.cs | 4 +- BossMod/Config/ConfigConverter.cs | 302 +++++++++++++++++++ BossMod/Config/ConfigRoot.cs | 354 +---------------------- BossMod/Util/JsonExtensions.cs | 16 + BossMod/Util/VersionedJSONSchema.cs | 60 ++++ TODO | 8 +- 7 files changed, 390 insertions(+), 356 deletions(-) create mode 100644 BossMod/Config/ConfigConverter.cs create mode 100644 BossMod/Util/JsonExtensions.cs create mode 100644 BossMod/Util/VersionedJSONSchema.cs diff --git a/BossMod/BossModule/BossModuleRegistry.cs b/BossMod/BossModule/BossModuleRegistry.cs index 0313d2224e..a9db519a98 100644 --- a/BossMod/BossModule/BossModuleRegistry.cs +++ b/BossMod/BossModule/BossModuleRegistry.cs @@ -176,7 +176,7 @@ static BossModuleRegistry() continue; _modulesByType[t] = info; if (!_modulesByOID.TryAdd(info.PrimaryActorOID, info)) - Service.Log($"Two boss modules have same primary actor OID: {t.Name} and {_modulesByOID[info.PrimaryActorOID].ModuleType.Name}"); + Service.Log($"[ModuleRegistry] Two boss modules have same primary actor OID: {t.FullName} and {_modulesByOID[info.PrimaryActorOID].ModuleType.FullName}"); } } diff --git a/BossMod/BossModule/ZoneModuleRegistry.cs b/BossMod/BossModule/ZoneModuleRegistry.cs index 6e0cbee503..021185cd98 100644 --- a/BossMod/BossModule/ZoneModuleRegistry.cs +++ b/BossMod/BossModule/ZoneModuleRegistry.cs @@ -24,12 +24,12 @@ static ZoneModuleRegistry() var attr = t.GetCustomAttribute(); if (attr == null) { - Service.Log($"Zone module {t} has no ZoneModuleInfo attribute, skipping"); + Service.Log($"[ZoneModuleRegistry] Zone module {t} has no ZoneModuleInfo attribute, skipping"); continue; } if (_modulesByCFC.TryGetValue(attr.CFCID, out var existingModule)) { - Service.Log($"Two zone modules have same CFCID: {t.Name} and {existingModule.ModuleType.Name}"); + Service.Log($"[ZoneModuleRegistry] Two zone modules have same CFCID: {t.FullName} and {existingModule.ModuleType.FullName}"); continue; } _modulesByCFC[attr.CFCID] = new Info(t, attr, New.ConstructorDerived(t)); diff --git a/BossMod/Config/ConfigConverter.cs b/BossMod/Config/ConfigConverter.cs new file mode 100644 index 0000000000..d239bf4cf7 --- /dev/null +++ b/BossMod/Config/ConfigConverter.cs @@ -0,0 +1,302 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace BossMod; + +public static class ConfigConverter +{ + public static VersionedJSONSchema Schema = BuildSchema(); + + private static VersionedJSONSchema BuildSchema() + { + var res = new VersionedJSONSchema(); + res.Converters.Add((j, _, _) => j); // v1: moved BossModuleConfig children to special encounter config node; use type names as keys - do nothing, next converter takes care of it + res.Converters.Add((j, v, _) => // v2: flat structure (config root contains all nodes) + { + JsonObject newPayload = []; + ConvertV1GatherChildren(newPayload, j, v == 0); + return newPayload; + }); + res.Converters.Add((j, _, _) => // v3: modified namespaces for old modules + { + j.TryRenameNode("BossMod.Endwalker.P1S.P1SConfig", "BossMod.Endwalker.Savage.P1SErichthonios.P1SConfig"); + j.TryRenameNode("BossMod.Endwalker.P4S2.P4S2Config", "BossMod.Endwalker.Savage.P4S2Hesperos.P4S2Config"); + return j; + }); + res.Converters.Add((j, _, _) => // v4: cooldown plans moved to encounter configs + { + if (j["BossMod.CooldownPlanManager"]?["Plans"] is JsonObject plans) + { + foreach (var (k, planData) in plans) + { + var oid = uint.Parse(k); + var info = BossModuleRegistry.FindByOID(oid); + var config = info?.PlanLevel > 0 ? info.ConfigType : null; + if (config?.FullName == null) + continue; + + if (j[config.FullName] is not JsonObject node) + j[config.FullName] = node = []; + node["CooldownPlans"] = planData; + } + } + j.Remove("BossMod.CooldownPlanManager"); + return j; + }); + res.Converters.Add((j, _, _) => // v5: bloodwhetting -> raw intuition in cd planner, to support low-level content + { + foreach (var (k, config) in j) + if (config?["CooldownPlans"]?["WAR"]?["Available"] is JsonArray plans) + foreach (var plan in plans) + if (plan!["PlanAbilities"] is JsonObject planAbilities) + planAbilities.TryRenameNode(ActionID.MakeSpell(WAR.AID.Bloodwhetting).Raw.ToString(), ActionID.MakeSpell(WAR.AID.RawIntuition).Raw.ToString()); + return j; + }); + res.Converters.Add((j, _, _) => // v6: new cooldown planner + { + foreach (var (k, config) in j) + { + if (config?["CooldownPlans"] is not JsonObject plans) + continue; + bool isTEA = k == typeof(Shadowbringers.Ultimate.TEA.TEAConfig).FullName; + foreach (var (cls, planList) in plans) + { + if (planList?["Available"] is not JsonArray avail) + continue; + var c = Enum.Parse(cls); + foreach (var plan in avail) + { + if (plan?["PlanAbilities"] is not JsonObject abilities) + continue; + + var actions = new JsonArray(); + foreach (var (aidRaw, aidData) in abilities) + { + if (aidData is not JsonArray aidList) + continue; + + var aid = new ActionID(uint.Parse(aidRaw)); + // hack revert, out of config modules existing before v6 only TEA could use raw intuition instead of BW + if (!isTEA && aid.ID == (uint)WAR.AID.RawIntuition) + aid = ActionID.MakeSpell(WAR.AID.Bloodwhetting); + + foreach (var abilUse in aidList) + { + abilUse!["ID"] = Utils.StringToIdentifier(aid.Name()); + abilUse["StateID"] = $"0x{abilUse["StateID"]?.GetValue():X8}"; + actions.Add(abilUse); + } + } + var jplan = (JsonObject)plan!; + jplan.Remove("PlanAbilities"); + jplan["Actions"] = actions; + } + } + } + return j; + }); + res.Converters.Add((j, _, _) => // v7: action manager refactor + { + var amConfig = j["BossMod.ActionManagerConfig"] = new JsonObject(); + var autorotConfig = j["BossMod.AutorotationConfig"]; + amConfig["RemoveAnimationLockDelay"] = autorotConfig?["RemoveAnimationLockDelay"] ?? false; + amConfig["PreventMovingWhileCasting"] = autorotConfig?["PreventMovingWhileCasting"] ?? false; + amConfig["RestoreRotation"] = autorotConfig?["RestoreRotation"] ?? false; + amConfig["GTMode"] = autorotConfig?["GTMode"] ?? "Manual"; + return j; + }); + res.Converters.Add((j, _, _) => j); // v8: remove accidentally serializable Modified field + res.Converters.Add((j, _, _) => // v9: and again the same thing... + { + foreach (var (_, config) in j) + if (config is JsonObject jconfig) + jconfig.Remove("Modified"); + return j; + }); + res.Converters.Add((j, _, f) => // v10: autorotation v2: moved configs around and importantly moved cdplans outside + { + j.TryRenameNode("BossMod.ActionManagerConfig", "BossMod.ActionTweaksConfig"); + j.TryRenameNode("BossMod.AutorotationConfig", "BossMod.Autorotation.AutorotationConfig"); + ConvertV9Plans(j, f.Directory!); + return j; + }); + return res; + } + + private static void ConvertV1GatherChildren(JsonObject result, JsonObject json, bool isV0) + { + if (json["__children__"] is not JsonObject children) + return; + foreach ((var childTypeName, var jChild) in children) + { + if (jChild is not JsonObject jChildObj) + continue; + + string realTypeName = isV0 ? (jChildObj["__type__"]?.ToString() ?? childTypeName) : childTypeName; + ConvertV1GatherChildren(result, jChildObj, isV0); + result.Add(realTypeName, jChild); + } + json.Remove("__children__"); + } + + private static void ConvertV9Plans(JsonObject payload, DirectoryInfo dir) + { + var dbRoot = new DirectoryInfo(dir.FullName + "/BossMod/autorot/plans"); + if (!dbRoot.Exists) + dbRoot.Create(); + using var manifestStream = new FileStream(dbRoot + ".manifest.json", FileMode.Create, FileAccess.Write, FileShare.Read); + using var manifest = Serialization.WriteJson(manifestStream); + manifest.WriteStartObject(); + manifest.WriteNumber("version", 0); + manifest.WriteStartObject("payload"); + foreach (var (ct, cfg) in payload.AsObject()) + { + if (!cfg!.AsObject().TryRemoveNode("CooldownPlans", out var cdplans)) + continue; + var t = ct[..^6]; + var type = Type.GetType(t); + manifest.WriteStartObject(t); + foreach (var (cls, plans) in cdplans!.AsObject()) + { + manifest.WriteStartObject(cls); + if (plans!.AsObject().TryGetPropertyValue("SelectedIndex", out var jsel)) + manifest.WriteNumber("SelectedIndex", jsel!.GetValue()); + manifest.WriteStartArray("Plans"); + foreach (var plan in plans["Available"]!.AsArray()) + { + var guid = Guid.NewGuid().ToString(); + manifest.WriteStringValue(guid); + + var oplan = plan!.AsObject(); + var utilityTracks = ConvertV9ActionsToUtilityTracks(oplan); + var rotationTracks = ConvertV9StrategiesToRotationTracks(oplan); + if (rotationTracks.Remove("Special", out var lb)) + utilityTracks["LB"] = lb; + + using var planStream = new FileStream($"{dbRoot}/{guid}.json", FileMode.Create, FileAccess.Write, FileShare.Read); + using var jplan = Serialization.WriteJson(planStream); + jplan.WriteStartObject(); + jplan.WriteNumber("version", 0); + jplan.WriteStartObject("payload"); + jplan.WriteString("Name", plan!["Name"]!.GetValue()); + jplan.WriteString("Encounter", t); + jplan.WriteString("Class", cls); + jplan.WriteNumber("Level", type != null ? BossModuleRegistry.FindByType(type)?.PlanLevel ?? 0 : 0); + jplan.WriteStartArray("PhaseDurations"); + foreach (var d in plan["Timings"]!["PhaseDurations"]!.AsArray()) + jplan.WriteNumberValue(d!.GetValue()); + jplan.WriteEndArray(); + jplan.WriteStartObject("Modules"); + ConvertV9WriteTrack(jplan, $"BossMod.Autorotation.Class{cls}Utility", utilityTracks); + ConvertV9WriteTrack(jplan, $"BossMod.Autorotation.Legacy.Legacy{cls}", rotationTracks); + jplan.WriteEndObject(); + if (oplan.TryGetPropertyValue("Targets", out var jtargets)) + { + jplan.WriteStartArray("Targeting"); + foreach (var target in jtargets!.AsArray()) + { + var jt = target!.AsObject(); + if (jt.TryRemoveNode("OID", out var oid)) + { + jt["Target"] = "EnemyByOID"; + jt["TargetParam"] = int.Parse(oid!.GetValue()[2..], System.Globalization.NumberStyles.HexNumber); + } + jt.WriteTo(jplan); + } + jplan.WriteEndArray(); + } + jplan.WriteEndObject(); + jplan.WriteEndObject(); + } + manifest.WriteEndArray(); + manifest.WriteEndObject(); + } + manifest.WriteEndObject(); + } + manifest.WriteEndObject(); + manifest.WriteEndObject(); + } + + private static Dictionary> ConvertV9ActionsToUtilityTracks(JsonObject plan) + { + Dictionary> tracks = []; + if (!plan.TryGetPropertyValue("Actions", out var actions)) + return tracks; + foreach (var action in actions!.AsArray()) + { + var aobj = action!.AsObject(); + aobj["Option"] = "Use"; + if (aobj.TryRemoveNode("LowPriority", out var jprio)) + aobj.Add("PriorityOverride", jprio!.GetValue() ? ActionQueue.Priority.Low : ActionQueue.Priority.High); + if (aobj.TryRemoveNode("Target", out var jtarget)) + { + switch (jtarget!["Type"]!.GetValue()!) + { + case "Self": + aobj["Target"] = "Self"; + break; + case "EnemyByOID": + aobj["Target"] = "EnemyByOID"; + aobj["TargetParam"] = jtarget["OID"]!.GetValue(); + break; + case "LowestHPPartyMember": + aobj["Target"] = "PartyWithLowestHP"; + aobj["TargetParam"] = jtarget["AllowSelf"]!.GetValue() ? 1 : 0; + break; + } + } + if (aobj.TryRemoveNode("ID", out var jid)) + { + var id = jid!.GetValue(); + switch (id) + { + case "Bloodwhetting": + id = "BW"; + aobj["Option"] = "BW"; + break; + case "RawIntuition": + id = "BW"; + aobj["Option"] = "RI"; + break; + case "NascentFlash": + id = "BW"; + aobj["Option"] = "NF"; + break; + } + tracks.GetOrAdd(id).Add(aobj); + } + } + return tracks; + } + + private static Dictionary> ConvertV9StrategiesToRotationTracks(JsonObject plan) + { + Dictionary> tracks = []; + if (!plan.TryGetPropertyValue("Strategies", out var strategies)) + return tracks; + foreach (var (track, values) in strategies!.AsObject()) + { + var t = tracks[track] = [.. values!.AsArray().Select(n => n!.AsObject())]; + foreach (var v in t) + if (v.TryRemoveNode("Value", out var jv)) + v["Option"] = jv; + } + return tracks; + } + + private static void ConvertV9WriteTrack(Utf8JsonWriter writer, string module, Dictionary> tracks) + { + writer.WriteStartObject(module); + foreach (var (tn, td) in tracks) + { + writer.WriteStartArray(tn); + foreach (var d in td) + { + d.WriteTo(writer); + } + writer.WriteEndArray(); + } + writer.WriteEndObject(); + } +} diff --git a/BossMod/Config/ConfigRoot.cs b/BossMod/Config/ConfigRoot.cs index c295ed2895..0cb48ea454 100644 --- a/BossMod/Config/ConfigRoot.cs +++ b/BossMod/Config/ConfigRoot.cs @@ -1,14 +1,10 @@ using System.IO; using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; namespace BossMod; public class ConfigRoot { - private const int _version = 10; - public Event Modified = new(); public Version AssemblyVersion = new(); // we use this to show newly added config options private readonly Dictionary _nodes = []; @@ -38,9 +34,10 @@ public void LoadFromFile(FileInfo file) { try { - using var json = ReadConvertFile(file); + var data = ConfigConverter.Schema.Load(file); + using var json = data.document; var ser = Serialization.BuildSerializationOptions(); - foreach (var jconfig in json.RootElement.GetProperty("Payload").EnumerateObject()) + foreach (var jconfig in data.payload.EnumerateObject()) { var type = Type.GetType(jconfig.Name); var node = type != null ? _nodes.GetValueOrDefault(type) : null; @@ -58,7 +55,7 @@ public void SaveToFile(FileInfo file) { try { - WriteFile(file, jwriter => + ConfigConverter.Schema.Save(file, jwriter => { jwriter.WriteStartObject(); var ser = Serialization.BuildSerializationOptions(); @@ -179,347 +176,4 @@ public List ConsoleCommand(ReadOnlySpan cmd, bool save = true) : t == typeof(int) ? int.Parse(str) : t.IsAssignableTo(typeof(Enum)) ? Enum.Parse(t, str) : null; - - private JsonDocument ReadConvertFile(FileInfo file) - { - var json = Serialization.ReadJson(file.FullName); - var version = json.RootElement.TryGetProperty("Version", out var jver) ? jver.GetInt32() : 0; - if (version > _version) - throw new ArgumentException($"Config file version {version} is newer than supported {_version}"); - if (version == _version) - return json; - - var converted = ConvertConfig(JsonObject.Create(json.RootElement.GetProperty("Payload"))!, version, file.Directory!); - - var original = new FileInfo(file.FullName); - var backup = new FileInfo(file.FullName + $".v{version}"); - if (!backup.Exists) - file.MoveTo(backup.FullName); - WriteFile(original, jwriter => converted.WriteTo(jwriter)); - json.Dispose(); - - return Serialization.ReadJson(original.FullName); - } - - private void WriteFile(FileInfo file, Action writePayload) - { - using var fstream = new FileStream(file.FullName, FileMode.Create, FileAccess.Write, FileShare.Read); - using var jwriter = Serialization.WriteJson(fstream); - jwriter.WriteStartObject(); - jwriter.WriteNumber("Version", _version); - jwriter.WritePropertyName("Payload"); - writePayload(jwriter); - jwriter.WriteEndObject(); - } - - private static JsonObject ConvertConfig(JsonObject payload, int version, DirectoryInfo dir) - { - // v1: moved BossModuleConfig children to special encounter config node; use type names as keys - // v2: flat structure (config root contains all nodes) - if (version < 2) - { - JsonObject newPayload = []; - ConvertV1GatherChildren(newPayload, payload, version == 0); - payload = newPayload; - } - // v3: modified namespaces for old modules - if (version < 3) - { - if (TryRemoveNode(payload, "BossMod.Endwalker.P1S.P1SConfig", out var p1s)) - payload.Add("BossMod.Endwalker.Savage.P1SErichthonios.P1SConfig", p1s); - if (TryRemoveNode(payload, "BossMod.Endwalker.P4S2.P4S2Config", out var p4s2)) - payload.Add("BossMod.Endwalker.Savage.P4S2Hesperos.P4S2Config", p4s2); - } - // v4: cooldown plans moved to encounter configs - if (version < 4) - { - if (payload["BossMod.CooldownPlanManager"]?["Plans"] is JsonObject plans) - { - foreach (var (k, planData) in plans) - { - var oid = uint.Parse(k); - var info = BossModuleRegistry.FindByOID(oid); - var config = info?.PlanLevel > 0 ? info.ConfigType : null; - if (config?.FullName == null) - continue; - - if (payload[config.FullName] is not JsonObject node) - payload[config.FullName] = node = []; - node["CooldownPlans"] = planData; - } - } - payload.Remove("BossMod.CooldownPlanManager"); - } - // v5: bloodwhetting -> raw intuition in cd planner, to support low-level content - if (version < 5) - { - foreach (var (k, config) in payload) - { - if (config?["CooldownPlans"]?["WAR"]?["Available"] is JsonArray plans) - { - foreach (var plan in plans) - { - if (plan!["PlanAbilities"] is JsonObject planAbilities) - { - if (TryRemoveNode(planAbilities, ActionID.MakeSpell(WAR.AID.Bloodwhetting).Raw.ToString(), out var bw)) - planAbilities.Add(ActionID.MakeSpell(WAR.AID.RawIntuition).Raw.ToString(), bw); - } - } - } - } - } - // v6: new cooldown planner - if (version < 6) - { - foreach (var (k, config) in payload) - { - if (config?["CooldownPlans"] is not JsonObject plans) - continue; - bool isTEA = k == typeof(Shadowbringers.Ultimate.TEA.TEAConfig).FullName; - foreach (var (cls, planList) in plans) - { - if (planList?["Available"] is not JsonArray avail) - continue; - var c = Enum.Parse(cls); - foreach (var plan in avail) - { - if (plan?["PlanAbilities"] is not JsonObject abilities) - continue; - - var actions = new JsonArray(); - foreach (var (aidRaw, aidData) in abilities) - { - if (aidData is not JsonArray aidList) - continue; - - var aid = new ActionID(uint.Parse(aidRaw)); - // hack revert, out of config modules existing before v6 only TEA could use raw intuition instead of BW - if (!isTEA && aid.ID == (uint)WAR.AID.RawIntuition) - aid = ActionID.MakeSpell(WAR.AID.Bloodwhetting); - - foreach (var abilUse in aidList) - { - abilUse!["ID"] = Utils.StringToIdentifier(aid.Name()); - abilUse["StateID"] = $"0x{abilUse["StateID"]?.GetValue():X8}"; - actions.Add(abilUse); - } - } - var jplan = (JsonObject)plan!; - jplan.Remove("PlanAbilities"); - jplan["Actions"] = actions; - } - } - } - } - // v7: action manager refactor - if (version < 7) - { - var amConfig = payload["BossMod.ActionManagerConfig"] = new JsonObject(); - var autorotConfig = payload["BossMod.AutorotationConfig"]; - amConfig["RemoveAnimationLockDelay"] = autorotConfig?["RemoveAnimationLockDelay"] ?? false; - amConfig["PreventMovingWhileCasting"] = autorotConfig?["PreventMovingWhileCasting"] ?? false; - amConfig["RestoreRotation"] = autorotConfig?["RestoreRotation"] ?? false; - amConfig["GTMode"] = autorotConfig?["GTMode"] ?? "Manual"; - } - // v8: remove accidentally serializable Modified field - // v9: and again the same thing... - if (version < 9) - { - foreach (var (_, config) in payload) - { - if (config is JsonObject jconfig) - { - jconfig.Remove("Modified"); - } - } - } - // v10: autorotation v2: moved configs around and importantly moved cdplans outside - if (version < 10) - { - if (TryRemoveNode(payload, "BossMod.ActionManagerConfig", out var tweaks)) - payload.Add("BossMod.ActionTweaksConfig", tweaks); - if (TryRemoveNode(payload, "BossMod.AutorotationConfig", out var autorot)) - payload.Add("BossMod.Autorotation.AutorotationConfig", autorot); - ConvertV9Plans(payload, dir); - } - return payload; - } - - private static void ConvertV1GatherChildren(JsonObject result, JsonObject json, bool isV0) - { - if (json["__children__"] is not JsonObject children) - return; - foreach ((var childTypeName, var jChild) in children) - { - if (jChild is not JsonObject jChildObj) - continue; - - string realTypeName = isV0 ? (jChildObj["__type__"]?.ToString() ?? childTypeName) : childTypeName; - ConvertV1GatherChildren(result, jChildObj, isV0); - result.Add(realTypeName, jChild); - } - json.Remove("__children__"); - } - - private static void ConvertV9Plans(JsonObject payload, DirectoryInfo dir) - { - var dbRoot = new DirectoryInfo(dir.FullName + "/BossMod/autorot/plans"); - if (!dbRoot.Exists) - dbRoot.Create(); - using var manifestStream = new FileStream(dbRoot + ".manifest.json", FileMode.Create, FileAccess.Write, FileShare.Read); - using var manifest = Serialization.WriteJson(manifestStream); - manifest.WriteStartObject(); - manifest.WriteNumber("version", 0); - manifest.WriteStartObject("payload"); - foreach (var (ct, cfg) in payload.AsObject()) - { - if (!TryRemoveNode(cfg!.AsObject(), "CooldownPlans", out var cdplans)) - continue; - var t = ct[..^6]; - var type = Type.GetType(t); - manifest.WriteStartObject(t); - foreach (var (cls, plans) in cdplans!.AsObject()) - { - manifest.WriteStartObject(cls); - if (plans!.AsObject().TryGetPropertyValue("SelectedIndex", out var jsel)) - manifest.WriteNumber("SelectedIndex", jsel!.GetValue()); - manifest.WriteStartArray("Plans"); - foreach (var plan in plans["Available"]!.AsArray()) - { - var guid = Guid.NewGuid().ToString(); - manifest.WriteStringValue(guid); - - var oplan = plan!.AsObject(); - var utilityTracks = ConvertV9ActionsToUtilityTracks(oplan); - var rotationTracks = ConvertV9StrategiesToRotationTracks(oplan); - if (rotationTracks.Remove("Special", out var lb)) - utilityTracks["LB"] = lb; - - using var planStream = new FileStream($"{dbRoot}/{guid}.json", FileMode.Create, FileAccess.Write, FileShare.Read); - using var jplan = Serialization.WriteJson(planStream); - jplan.WriteStartObject(); - jplan.WriteNumber("version", 0); - jplan.WriteStartObject("payload"); - jplan.WriteString("Name", plan!["Name"]!.GetValue()); - jplan.WriteString("Encounter", t); - jplan.WriteString("Class", cls); - jplan.WriteNumber("Level", type != null ? BossModuleRegistry.FindByType(type)?.PlanLevel ?? 0 : 0); - jplan.WriteStartArray("PhaseDurations"); - foreach (var d in plan["Timings"]!["PhaseDurations"]!.AsArray()) - jplan.WriteNumberValue(d!.GetValue()); - jplan.WriteEndArray(); - jplan.WriteStartObject("Modules"); - ConvertV9WriteTrack(jplan, $"BossMod.Autorotation.Class{cls}Utility", utilityTracks); - ConvertV9WriteTrack(jplan, $"BossMod.Autorotation.Legacy.Legacy{cls}", rotationTracks); - jplan.WriteEndObject(); - if (oplan.TryGetPropertyValue("Targets", out var jtargets)) - { - jplan.WriteStartArray("Targeting"); - foreach (var target in jtargets!.AsArray()) - { - var jt = target!.AsObject(); - if (TryRemoveNode(jt, "OID", out var oid)) - { - jt["Target"] = "EnemyByOID"; - jt["TargetParam"] = int.Parse(oid!.GetValue()[2..], System.Globalization.NumberStyles.HexNumber); - } - jt.WriteTo(jplan); - } - jplan.WriteEndArray(); - } - jplan.WriteEndObject(); - jplan.WriteEndObject(); - } - manifest.WriteEndArray(); - manifest.WriteEndObject(); - } - manifest.WriteEndObject(); - } - manifest.WriteEndObject(); - manifest.WriteEndObject(); - } - - private static Dictionary> ConvertV9ActionsToUtilityTracks(JsonObject plan) - { - Dictionary> tracks = []; - if (!plan.TryGetPropertyValue("Actions", out var actions)) - return tracks; - foreach (var action in actions!.AsArray()) - { - var aobj = action!.AsObject(); - aobj["Option"] = "Use"; - if (TryRemoveNode(aobj, "LowPriority", out var jprio)) - aobj.Add("PriorityOverride", jprio!.GetValue() ? ActionQueue.Priority.Low : ActionQueue.Priority.High); - if (TryRemoveNode(aobj, "Target", out var jtarget)) - { - switch (jtarget!["Type"]!.GetValue()!) - { - case "Self": - aobj["Target"] = "Self"; - break; - case "EnemyByOID": - aobj["Target"] = "EnemyByOID"; - aobj["TargetParam"] = jtarget["OID"]!.GetValue(); - break; - case "LowestHPPartyMember": - aobj["Target"] = "PartyWithLowestHP"; - aobj["TargetParam"] = jtarget["AllowSelf"]!.GetValue() ? 1 : 0; - break; - } - } - if (TryRemoveNode(aobj, "ID", out var jid)) - { - var id = jid!.GetValue(); - switch (id) - { - case "Bloodwhetting": - id = "BW"; - aobj["Option"] = "BW"; - break; - case "RawIntuition": - id = "BW"; - aobj["Option"] = "RI"; - break; - case "NascentFlash": - id = "BW"; - aobj["Option"] = "NF"; - break; - } - tracks.GetOrAdd(id).Add(aobj); - } - } - return tracks; - } - - private static Dictionary> ConvertV9StrategiesToRotationTracks(JsonObject plan) - { - Dictionary> tracks = []; - if (!plan.TryGetPropertyValue("Strategies", out var strategies)) - return tracks; - foreach (var (track, values) in strategies!.AsObject()) - { - var t = tracks[track] = [.. values!.AsArray().Select(n => n!.AsObject())]; - foreach (var v in t) - if (TryRemoveNode(v, "Value", out var jv)) - v["Option"] = jv; - } - return tracks; - } - - private static void ConvertV9WriteTrack(Utf8JsonWriter writer, string module, Dictionary> tracks) - { - writer.WriteStartObject(module); - foreach (var (tn, td) in tracks) - { - writer.WriteStartArray(tn); - foreach (var d in td) - { - d.WriteTo(writer); - } - writer.WriteEndArray(); - } - writer.WriteEndObject(); - } - - private static bool TryRemoveNode(JsonObject parent, string key, out JsonNode? node) => parent.TryGetPropertyValue(key, out node) && parent.Remove(key); } diff --git a/BossMod/Util/JsonExtensions.cs b/BossMod/Util/JsonExtensions.cs new file mode 100644 index 0000000000..7b0ffc48c4 --- /dev/null +++ b/BossMod/Util/JsonExtensions.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Nodes; + +namespace BossMod; + +public static class JsonExtensions +{ + public static bool TryRemoveNode(this JsonObject parent, string key, out JsonNode? node) => parent.TryGetPropertyValue(key, out node) && parent.Remove(key); + + public static bool TryRenameNode(this JsonObject parent, string oldKey, string newKey) + { + if (!TryRemoveNode(parent, oldKey, out JsonNode? node)) + return false; + parent.Add(newKey, node); + return true; + } +} diff --git a/BossMod/Util/VersionedJSONSchema.cs b/BossMod/Util/VersionedJSONSchema.cs new file mode 100644 index 0000000000..7ee59f7a79 --- /dev/null +++ b/BossMod/Util/VersionedJSONSchema.cs @@ -0,0 +1,60 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace BossMod; + +// utility for loading versioned json configuration files, executing conversion if needed +public sealed class VersionedJSONSchema +{ + public delegate JsonObject ConvertDelegate(JsonObject input, int startingVersion, FileInfo path); + + public readonly int MinSupportedVersion; + public readonly List Converters = []; + + public int CurrentVersion => MinSupportedVersion + Converters.Count; + + public (JsonDocument document, JsonElement payload) Load(FileInfo file) + { + var json = Serialization.ReadJson(file.FullName); + var version = json.RootElement.TryGetProperty("version", out var jver) || json.RootElement.TryGetProperty("Version", out jver) ? jver.GetInt32() : 0; + if (version < MinSupportedVersion) + throw new ArgumentException($"Config file {file.FullName} version {version} is older than supported {MinSupportedVersion}"); + if (version > CurrentVersion) + throw new ArgumentException($"Config file {file.FullName} version {version} is newer than supported {CurrentVersion}"); + if (!json.RootElement.TryGetProperty("payload", out var jpayload) && !json.RootElement.TryGetProperty("Payload", out jpayload)) + throw new ArgumentException($"Config file {file.FullName} does not contain a payload"); + + // fast path: if file is of correct version, we're done + if (version == CurrentVersion) + return (json, jpayload); + + // execute the conversion + var converted = JsonObject.Create(jpayload) ?? throw new ArgumentException($"Failed to upgrade {file.FullName} from {version} to {CurrentVersion}"); + for (int i = version - MinSupportedVersion; i < Converters.Count; ++i) + converted = Converters[i](converted, version, file); + + // backup the old version and write out new one + var original = new FileInfo(file.FullName); + var backup = new FileInfo(file.FullName + $".v{version}"); + if (!backup.Exists) + file.MoveTo(backup.FullName); + Save(original, jwriter => converted.WriteTo(jwriter)); + json.Dispose(); + + // and now read again... + json = Serialization.ReadJson(original.FullName); + return (json, json.RootElement.GetProperty("payload")); + } + + public void Save(FileInfo file, Action writePayload) + { + using var fstream = new FileStream(file.FullName, FileMode.Create, FileAccess.Write, FileShare.Read); + using var jwriter = Serialization.WriteJson(fstream); + jwriter.WriteStartObject(); + jwriter.WriteNumber("version", CurrentVersion); + jwriter.WritePropertyName("payload"); + writePayload(jwriter); + jwriter.WriteEndObject(); + } +} diff --git a/TODO b/TODO index 6a1ebbf242..da1154ada5 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,5 @@ immediate plans +- get rid of legacyxxx - ai refactoring -- high-level ai modules --- ordered before standard rotation modules @@ -13,14 +14,13 @@ immediate plans --- responsible for setting forced-movement (using pathfinding or other strategies) and max-cast-hint (based on leeway) --- tracks like destination (pathfind / explicit abs orient / explicit target orient) and adjustment (direct / maxmelee greed / uptime-downtime based on gcd / force-finish-cast / ...) -- framework (actionqueue getbest) should skip spells that won't finish in time +-- order should be a module-specified enum and framework should enforce ordering constraints - review enemy prios usage - should framework do anything about any prios? like taunt at -4... - freeze - gaze avoidance + forced movement fail -- get rid of legacyxxx - ex3 p2 ice bridges - on ex1 the cleave is still telegraphed a bit too wide - for p2 thordan cleavebuster the telegraph on the minimap is narrower than the actual hitbox -- alt style for player indicator on arena - ishape general: @@ -53,8 +53,10 @@ general: - refactor ipc/dtr - questbattles - autoautos: remove target-setting shenanigans in ual, instead deal with disabling autos in hook -- pathfinding to actual cell entry instead of cell center? - pathfinding can cut corners by entering aoe (los check returns safe) - is that good?.. +- alt style for player indicator on arena +- MAO for pomanders holsters etc +- ManualActionQueueTweak.Push should not special case gcds?.. boss modules: - timers From 289900ba377105e5cfa2fc61af98276dbb165d44 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 18:47:32 +0000 Subject: [PATCH 31/33] Kill dupe modules. --- .../Heavensward/Quest/AtTheEndOfOurHope.cs | 18 --- .../Modules/Heavensward/Quest/Heliodrome.cs | 110 ------------------ .../Quest/VowsOfVitrueDeedsOfCruelty.cs | 94 --------------- 3 files changed, 222 deletions(-) delete mode 100644 BossMod/Modules/Heavensward/Quest/AtTheEndOfOurHope.cs delete mode 100644 BossMod/Modules/Heavensward/Quest/Heliodrome.cs delete mode 100644 BossMod/Modules/Shadowbringers/Quest/VowsOfVitrueDeedsOfCruelty.cs diff --git a/BossMod/Modules/Heavensward/Quest/AtTheEndOfOurHope.cs b/BossMod/Modules/Heavensward/Quest/AtTheEndOfOurHope.cs deleted file mode 100644 index 6b47c42ce4..0000000000 --- a/BossMod/Modules/Heavensward/Quest/AtTheEndOfOurHope.cs +++ /dev/null @@ -1,18 +0,0 @@ -using BossMod.QuestBattle; - -namespace BossMod.Heavensward.Quest; - -[ZoneModuleInfo(BossModuleInfo.Maturity.WIP, 416)] -public class AtTheEndOfOurHope(WorldState ws) : QuestBattle.QuestBattle(ws) -{ - public override List DefineObjectives(WorldState ws) => [ - new QuestObjective(ws).WithConnections( - // doorway - new Vector3(455.42f, 164.31f, -542.78f), - // basement - new Vector3(456.10f, 157.41f, -554.90f) - ) - .WithInteract(0x1E9B5A) - .PauseForCombat(false) - ]; -} diff --git a/BossMod/Modules/Heavensward/Quest/Heliodrome.cs b/BossMod/Modules/Heavensward/Quest/Heliodrome.cs deleted file mode 100644 index 95aede9114..0000000000 --- a/BossMod/Modules/Heavensward/Quest/Heliodrome.cs +++ /dev/null @@ -1,110 +0,0 @@ -namespace BossMod.Heavensward.Quest.Heliodrome; - -public enum OID : uint -{ - Boss = 0x195E, - Helper = 0x233C, - GrynewahtP2 = 0x195F, // R0.500, x0 (spawn during fight) - ImperialColossus = 0x1966, // R3.000, x0 (spawn during fight) -} - -public enum AID : uint -{ - AugmentedUprising = 7608, // Boss->self, 3.0s cast, range 8+R 120-degree cone - AugmentedSuffering = 7607, // Boss->self, 3.5s cast, range 6+R circle - Heartstopper = 866, // _Gen_ImperialEques->self, 2.5s cast, range 3+R width 3 rect - Overpower = 720, // _Gen_ImperialLaquearius->self, 2.1s cast, range 6+R 90-degree cone - GrandSword = 7615, // _Gen_ImperialColossus->self, 3.0s cast, range 18+R 120-degree cone - MagitekRay = 7617, // _Gen_ImperialColossus->location, 3.0s cast, range 6 circle - GrandStrike = 7616, // _Gen_ImperialColossus->self, 2.5s cast, range 45+R width 4 rect - ShrapnelShell = 7614, // GrynewahtP2->location, 2.5s cast, range 6 circle - MagitekMissiles = 7612, // GrynewahtP2->location, 5.0s cast, range 15 circle - -} - -class MagitekMissiles(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekMissiles), 15); -class ShrapnelShell(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ShrapnelShell), 6); -class Firebomb(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(0x1E86DF).Where(e => e.EventState != 7)); - -class Uprising(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); -class Suffering(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), new AOEShapeCircle(6.5f)); -class Heartstopper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Heartstopper), new AOEShapeRect(3.5f, 1.5f)); -class Overpower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6, 45.Degrees())); -class GrandSword(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GrandSword), new AOEShapeCone(21, 60.Degrees())); -class MagitekRay(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRay), 6); -class GrandStrike(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GrandStrike), new AOEShapeRect(48, 2)); - -class Adds(BossModule module) : Components.AddsMulti(module, [0x1960, 0x1961, 0x1962, 0x1963, 0x1964, 0x1965, 0x1966]) -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - foreach (var e in hints.PotentialTargets) - e.Priority = (OID)e.Actor.OID == OID.ImperialColossus ? 5 : e.Actor.TargetID == actor.InstanceID ? 1 : 0; - } -} - -class Bounds(BossModule module) : BossComponent(module) -{ - public override void OnEventDirectorUpdate(uint updateID, uint param1, uint param2, uint param3, uint param4) - { - if (updateID == 0x10000002) - Arena.Bounds = new ArenaBoundsCircle(20); - } -} - -class ReaperAI(BossModule module) : BossComponent(module) -{ - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (actor.MountId == 103 && WorldState.Actors.Find(actor.TargetID) is var target && target != null) - { - var aid = (OID)target.OID == OID.ImperialColossus ? Roleplay.AID.DiffractiveMagitekCannon : Roleplay.AID.MagitekCannon; - hints.ActionsToExecute.Push(ActionID.MakeSpell(aid), target, ActionQueue.Priority.High, targetPos: target.PosRot.XYZ()); - } - } -} - -class GrynewahtStates : StateMachineBuilder -{ - public GrynewahtStates(BossModule module) : base(module) - { - State build(uint id) => SimpleState(id, 10000, "Enrage") - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter(); - - SimplePhase(1, id => build(id).ActivateOnEnter(), "P1") - .Raw.Update = () => Module.Enemies(OID.GrynewahtP2).Any(); - DeathPhase(0x100, id => build(id).ActivateOnEnter().OnEnter(() => - { - Module.Arena.Bounds = new ArenaBoundsCircle(20); - })); - } -} - -[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 222, NameID = 5576)] -public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), HexBounds) -{ - public static readonly ArenaBoundsCustom HexBounds = BuildHexBounds(); - - private static ArenaBoundsCustom BuildHexBounds() - { - var hexSideLen = 20 / MathF.Sqrt(3); - - // slight adjustment to account for player hitbox radius, otherwise dodges can get very sketchy - hexSideLen -= 1.5f; - - List verts = [new(hexSideLen, 0), hexSideLen * 30.Degrees().ToDirection(), -hexSideLen * 150.Degrees().ToDirection(), new(-hexSideLen, 0), hexSideLen * -30.Degrees().ToDirection(), hexSideLen * 150.Degrees().ToDirection()]; - return new(hexSideLen, new(verts)); - } - - protected override bool CheckPull() => true; -} diff --git a/BossMod/Modules/Shadowbringers/Quest/VowsOfVitrueDeedsOfCruelty.cs b/BossMod/Modules/Shadowbringers/Quest/VowsOfVitrueDeedsOfCruelty.cs deleted file mode 100644 index 40d701e867..0000000000 --- a/BossMod/Modules/Shadowbringers/Quest/VowsOfVitrueDeedsOfCruelty.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace BossMod.Shadowbringers.Quest.VowsOfVitrueDeedsOfCruelty; - -public enum OID : uint -{ - Boss = 0x2C85, // R6.000, x1 - TerminusEstVisual = 0x2C98, // R1.000, x3 - BossHelper = 0x233C, // R0.500, x15, 523 type - SigniferPraetorianus = 0x2C9A, // R0.500, x0 (spawn during fight), the adds on the catwalk that just rain down Fire II - LembusPraetorianus = 0x2C99, // R2.400, x0 (spawn during fight), two large magitek ships - MagitekBit = 0x2C9C, // R0.600, x0 (spawn during fight) -} - -public enum AID : uint -{ - LoadData = 18786, // Boss->self, 3.0s cast, single-target - AutoAttack = 870, // Boss/LembusPraetorianus->player, no cast, single-target - MagitekRayRightArm = 18783, // Boss->self, 3.2s cast, range 45+R width 8 rect - MagitekRayLeftArm = 18784, // Boss->self, 3.2s cast, range 45+R width 8 rect - SystemError = 18785, // Boss->self, 1.0s cast, single-target - AngrySalamander = 18787, // Boss->self, 3.0s cast, range 40+R width 6 rect - FireII = 18959, // SigniferPraetorianus->location, 3.0s cast, range 5 circle - TerminusEstBossCast = 18788, // Boss->self, 3.0s cast, single-target - TerminusEstLocationHelper = 18889, // BossHelper->self, 4.0s cast, range 3 circle - TerminusEstVisual = 18789, // TerminusEstVisual->self, 1.0s cast, range 40+R width 4 rect - HorridRoar = 18779, // 2CC5->location, 2.0s cast, range 6 circle, this is your own attack. It spawns an aoe at the location of any enemy it initally hits - GarleanFire = 4007, // LembusPraetorianus->location, 3.0s cast, range 5 circle - MagitekBit = 18790, // Boss->self, no cast, single-target - MetalCutterCast = 18793, // Boss->self, 6.0s cast, single-target - MetalCutter = 18794, // BossHelper->self, 6.0s cast, range 30+R 20-degree cone - AtomicRayCast = 18795, // Boss->self, 6.0s cast, single-target - AtomicRay = 18796, // BossHelper->location, 6.0s cast, range 10 circle - MagitekRayBit = 18791, // MagitekBit->self, 6.0s cast, range 50+R width 2 rect - SelfDetonate = 18792, // MagitekBit->self, 7.0s cast, range 40+R circle, enrage if bits are not killed before cast -} - -class MagitekRayRightArm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRayRightArm), new AOEShapeRect(45, 4)); -class MagitekRayLeftArm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRayLeftArm), new AOEShapeRect(45, 4)); -class AngrySalamander(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AngrySalamander), new AOEShapeRect(40, 3)); -class TerminusEstRects(BossModule module) : Components.GenericAOEs(module) -{ - private readonly List _aoes = []; - private static readonly AOEShapeRect _shape = new(40, 2); - public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; - - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID == AID.TerminusEstLocationHelper) - { - _aoes.AddRange( - [ - new(_shape, caster.Position, spell.Rotation, Module.CastFinishAt(spell)), - new(_shape, caster.Position, spell.Rotation - 90.Degrees(), Module.CastFinishAt(spell)), - new(_shape, caster.Position, spell.Rotation + 90.Degrees(), Module.CastFinishAt(spell)) - ]); - } - } - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID.TerminusEstVisual) - { - _aoes.Clear(); - ++NumCasts; - } - } -} -class TerminusEstCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TerminusEstLocationHelper), new AOEShapeCircle(3)); -class FireII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FireII), 5); -class GarleanFire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.GarleanFire), 5); -class MetalCutter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MetalCutter), new AOEShapeCone(30, 10.Degrees())); -class MagitekRayBits(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRayBit), new AOEShapeRect(50, 1)); -class AtomicRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AtomicRay), new AOEShapeCircle(10)); -class SelfDetonate(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SelfDetonate), "Enrage if bits are not killed before cast"); -class VowsOfVirtueDeedsOfCrueltyStates : StateMachineBuilder -{ - public VowsOfVirtueDeedsOfCrueltyStates(BossModule module) : base(module) - { - TrivialPhase() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter(); - } -} - -[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "croizat", GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69218, NameID = 9189)] -public class VowsOfVirtueDeedsOfCruelty(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(20)); From 5b59d13942e34c2ee9896edd9686be4740adbc01 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 18:54:01 +0000 Subject: [PATCH 32/33] New category for chaotic raids. --- BossMod/BossModule/BossModuleInfo.cs | 1 + BossMod/Config/ModuleViewer.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/BossMod/BossModule/BossModuleInfo.cs b/BossMod/BossModule/BossModuleInfo.cs index b5da87f79d..515baae890 100644 --- a/BossMod/BossModule/BossModuleInfo.cs +++ b/BossMod/BossModule/BossModuleInfo.cs @@ -38,6 +38,7 @@ public enum Category Ultimate, Unreal, Alliance, + Chaotic, Foray, Criterion, DeepDungeon, diff --git a/BossMod/Config/ModuleViewer.cs b/BossMod/Config/ModuleViewer.cs index 1906f647cf..d42cbbeb24 100644 --- a/BossMod/Config/ModuleViewer.cs +++ b/BossMod/Config/ModuleViewer.cs @@ -51,6 +51,7 @@ public ModuleViewer(PlanDatabase? planDB, WorldState ws) Customize(BossModuleInfo.Category.Dungeon, contentType.GetRow(2)); Customize(BossModuleInfo.Category.Trial, contentType.GetRow(4)); Customize(BossModuleInfo.Category.Raid, contentType.GetRow(5)); + Customize(BossModuleInfo.Category.Chaotic, contentType.GetRow(37)); Customize(BossModuleInfo.Category.PVP, contentType.GetRow(6)); Customize(BossModuleInfo.Category.Quest, contentType.GetRow(7)); Customize(BossModuleInfo.Category.FATE, contentType.GetRow(8)); From c9262ee2e0c4602812baebed3585c18837c5b134 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 26 Jan 2025 21:36:16 +0000 Subject: [PATCH 33/33] Preparation for preset/plan converters. --- BossMod/Autorotation/PlanDatabase.cs | 15 +-- BossMod/Autorotation/PlanPresetConverter.cs | 102 +++++++++++++++++++ BossMod/Autorotation/PresetDatabase.cs | 15 +-- BossMod/Autorotation/Standard/StandardWAR.cs | 2 +- BossMod/Config/ConfigConverter.cs | 20 ++-- BossMod/Util/VersionedJSONSchema.cs | 9 +- 6 files changed, 128 insertions(+), 35 deletions(-) create mode 100644 BossMod/Autorotation/PlanPresetConverter.cs diff --git a/BossMod/Autorotation/PlanDatabase.cs b/BossMod/Autorotation/PlanDatabase.cs index 4833ce9178..7e27b1ad72 100644 --- a/BossMod/Autorotation/PlanDatabase.cs +++ b/BossMod/Autorotation/PlanDatabase.cs @@ -33,10 +33,9 @@ public PlanDatabase(string rootPath) { try { - using var json = Serialization.ReadJson(f.FullName); - var version = json.RootElement.GetProperty("version").GetInt32(); - var payload = json.RootElement.GetProperty("payload"); - var plan = payload.Deserialize(serOptions); + var data = PlanPresetConverter.PlanSchema.Load(f); + using var json = data.document; + var plan = data.payload.Deserialize(serOptions); if (plan != null) { plan.Guid = f.Name[..^5]; @@ -204,13 +203,7 @@ private void SavePlan(Plan plan) var filename = $"{_planStore.FullName}/{plan.Guid}.json"; try { - using var fstream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read); - using var jwriter = Serialization.WriteJson(fstream); - jwriter.WriteStartObject(); - jwriter.WriteNumber("version", 0); - jwriter.WritePropertyName("payload"); - JsonSerializer.Serialize(jwriter, plan, Serialization.BuildSerializationOptions()); - jwriter.WriteEndObject(); + PlanPresetConverter.PlanSchema.Save(new(filename), jwriter => JsonSerializer.Serialize(jwriter, plan, Serialization.BuildSerializationOptions())); Service.Log($"Plan saved successfully to '{filename}'"); } catch (Exception ex) diff --git a/BossMod/Autorotation/PlanPresetConverter.cs b/BossMod/Autorotation/PlanPresetConverter.cs new file mode 100644 index 0000000000..729e120b02 --- /dev/null +++ b/BossMod/Autorotation/PlanPresetConverter.cs @@ -0,0 +1,102 @@ +using System.Text.Json.Nodes; + +namespace BossMod.Autorotation; + +// TODO: waiting for .net9 to complete implementation, since it adds proper API for renaming keys, it's a pita to maintain order otherwise +public static class PlanPresetConverter +{ + // note: we always apply renames _after_ changes - this allows converting old modules to new ones without affecting plans/presets that already use new ones + private record class TrackChanges(Dictionary OptionRenames); + private record class ModuleChanges(Dictionary TrackChanges, Dictionary TrackRenames); + private record class ModuleConverter(Dictionary ModuleChanges, Dictionary ModuleRenames); + + public static VersionedJSONSchema PlanSchema = BuildSchema(true); + public static VersionedJSONSchema PresetSchema = BuildSchema(false); + + private static VersionedJSONSchema BuildSchema(bool plan) + { + var res = new VersionedJSONSchema(); + //AddModuleConverter(res, plan, BuildModuleConverterV1()); // v1: StandardWAR -> VeynVAR rename + return res; + } + + //private static void AddModuleConverter(VersionedJSONSchema schema, bool plan, ModuleConverter cvt) + //{ + // schema.Converters.Add((j, _, _) => + // { + // if (plan) + // { + // var modules = j!["Modules"]!.AsObject(); + // foreach (var (moduleName, moduleData) in modules) + // { + // if (cvt.ModuleChanges.TryGetValue(moduleName, out var moduleChanges)) + // { + // var tracks = moduleData!.AsObject(); + // foreach (var (trackName, trackData) in tracks) + // { + // if (moduleChanges.TrackChanges.TryGetValue(trackName, out var trackChanges)) + // { + // foreach (var entry in trackData!.AsArray()) + // { + // var optionName = entry!["Option"]!.GetValue(); + // if (trackChanges.OptionRenames.TryGetValue(optionName, out var optionNewName)) + // entry["Option"] = optionNewName; + // } + // } + // } + // ApplyRenames(tracks, moduleChanges.TrackRenames); + // } + // } + // ApplyRenames(modules, cvt.ModuleRenames); + // } + // else + // { + // foreach (var preset in j.AsArray()) + // { + // var modules = preset!["Modules"]!.AsObject(); + // foreach (var (moduleName, moduleData) in modules) + // { + // if (cvt.ModuleChanges.TryGetValue(moduleName, out var moduleChanges)) + // { + // foreach (var entry in moduleData!.AsArray()) + // { + // var trackName = entry!["Track"]!.GetValue(); + // if (moduleChanges.TrackChanges.TryGetValue(trackName, out var trackChanges)) + // { + // var optionName = entry!["Option"]!.GetValue(); + // if (trackChanges.OptionRenames.TryGetValue(optionName, out var optionNewName)) + // entry["Option"] = optionNewName; + // } + + // if (moduleChanges.TrackRenames.TryGetValue(trackName, out var trackNewName)) + // entry["Track"] = trackNewName; + // } + // } + // } + // ApplyRenames(modules, cvt.ModuleRenames); + // } + // } + // return j; + // }); + //} + + //private static void ApplyRenames(JsonObject j, Dictionary renames) + //{ + // if (renames.Count == 0) + // return; + + // for (int i = 0; i < j.Count; ++i) + // { + // // TODO: implement... + // } + //} + + //private static ModuleConverter BuildModuleConverterV1() + //{ + // Dictionary moduleRenames = new() + // { + // ["BossMod.Autorotation.StandardWAR"] = "BossMod.Autorotation.VeynWAR", + // }; + // return new([], moduleRenames); + //} +} diff --git a/BossMod/Autorotation/PresetDatabase.cs b/BossMod/Autorotation/PresetDatabase.cs index 38669c42f9..75b5b54574 100644 --- a/BossMod/Autorotation/PresetDatabase.cs +++ b/BossMod/Autorotation/PresetDatabase.cs @@ -27,10 +27,9 @@ private List LoadPresetsFromFile(FileInfo file) { try { - using var json = Serialization.ReadJson(file.FullName); - var version = json.RootElement.GetProperty("version").GetInt32(); - var payload = json.RootElement.GetProperty("payload"); - return payload.Deserialize>(Serialization.BuildSerializationOptions()) ?? []; + var data = PlanPresetConverter.PresetSchema.Load(file); + using var json = data.document; + return data.payload.Deserialize>(Serialization.BuildSerializationOptions()) ?? []; } catch (Exception ex) { @@ -62,13 +61,7 @@ public void Save() { try { - using var fstream = new FileStream(_dbPath.FullName, FileMode.Create, FileAccess.Write, FileShare.Read); - using var jwriter = Serialization.WriteJson(fstream); - jwriter.WriteStartObject(); - jwriter.WriteNumber("version", 0); - jwriter.WritePropertyName("payload"); - JsonSerializer.Serialize(jwriter, UserPresets, Serialization.BuildSerializationOptions()); - jwriter.WriteEndObject(); + PlanPresetConverter.PresetSchema.Save(_dbPath, jwriter => JsonSerializer.Serialize(jwriter, UserPresets, Serialization.BuildSerializationOptions())); Service.Log($"Database saved successfully to '{_dbPath.FullName}'"); } catch (Exception ex) diff --git a/BossMod/Autorotation/Standard/StandardWAR.cs b/BossMod/Autorotation/Standard/StandardWAR.cs index ccac8d9b50..d5090a30b4 100644 --- a/BossMod/Autorotation/Standard/StandardWAR.cs +++ b/BossMod/Autorotation/Standard/StandardWAR.cs @@ -17,7 +17,7 @@ public enum BozjaStrategy { None, WithIR, BloodRage } public static RotationModuleDefinition Definition() { - var res = new RotationModuleDefinition("Standard WAR", "Standard rotation module", "Standard rotation (veyn)", "veyn", RotationModuleQuality.Good, BitMask.Build((int)Class.WAR, (int)Class.MRD), 100); + var res = new RotationModuleDefinition("Veyn WAR", "Standard rotation module", "Standard rotation (veyn)", "veyn", RotationModuleQuality.Good, BitMask.Build((int)Class.WAR, (int)Class.MRD), 100); res.Define(Track.AOE).As("AOE", uiPriority: 90) .AddOption(AOEStrategy.SingleTarget, "ST", "Use single-target rotation") diff --git a/BossMod/Config/ConfigConverter.cs b/BossMod/Config/ConfigConverter.cs index d239bf4cf7..9503ea4516 100644 --- a/BossMod/Config/ConfigConverter.cs +++ b/BossMod/Config/ConfigConverter.cs @@ -15,13 +15,13 @@ private static VersionedJSONSchema BuildSchema() res.Converters.Add((j, v, _) => // v2: flat structure (config root contains all nodes) { JsonObject newPayload = []; - ConvertV1GatherChildren(newPayload, j, v == 0); + ConvertV1GatherChildren(newPayload, j.AsObject(), v == 0); return newPayload; }); res.Converters.Add((j, _, _) => // v3: modified namespaces for old modules { - j.TryRenameNode("BossMod.Endwalker.P1S.P1SConfig", "BossMod.Endwalker.Savage.P1SErichthonios.P1SConfig"); - j.TryRenameNode("BossMod.Endwalker.P4S2.P4S2Config", "BossMod.Endwalker.Savage.P4S2Hesperos.P4S2Config"); + j.AsObject().TryRenameNode("BossMod.Endwalker.P1S.P1SConfig", "BossMod.Endwalker.Savage.P1SErichthonios.P1SConfig"); + j.AsObject().TryRenameNode("BossMod.Endwalker.P4S2.P4S2Config", "BossMod.Endwalker.Savage.P4S2Hesperos.P4S2Config"); return j; }); res.Converters.Add((j, _, _) => // v4: cooldown plans moved to encounter configs @@ -41,12 +41,12 @@ private static VersionedJSONSchema BuildSchema() node["CooldownPlans"] = planData; } } - j.Remove("BossMod.CooldownPlanManager"); + j.AsObject().Remove("BossMod.CooldownPlanManager"); return j; }); res.Converters.Add((j, _, _) => // v5: bloodwhetting -> raw intuition in cd planner, to support low-level content { - foreach (var (k, config) in j) + foreach (var (k, config) in j.AsObject()) if (config?["CooldownPlans"]?["WAR"]?["Available"] is JsonArray plans) foreach (var plan in plans) if (plan!["PlanAbilities"] is JsonObject planAbilities) @@ -55,7 +55,7 @@ private static VersionedJSONSchema BuildSchema() }); res.Converters.Add((j, _, _) => // v6: new cooldown planner { - foreach (var (k, config) in j) + foreach (var (k, config) in j.AsObject()) { if (config?["CooldownPlans"] is not JsonObject plans) continue; @@ -109,16 +109,16 @@ private static VersionedJSONSchema BuildSchema() res.Converters.Add((j, _, _) => j); // v8: remove accidentally serializable Modified field res.Converters.Add((j, _, _) => // v9: and again the same thing... { - foreach (var (_, config) in j) + foreach (var (_, config) in j.AsObject()) if (config is JsonObject jconfig) jconfig.Remove("Modified"); return j; }); res.Converters.Add((j, _, f) => // v10: autorotation v2: moved configs around and importantly moved cdplans outside { - j.TryRenameNode("BossMod.ActionManagerConfig", "BossMod.ActionTweaksConfig"); - j.TryRenameNode("BossMod.AutorotationConfig", "BossMod.Autorotation.AutorotationConfig"); - ConvertV9Plans(j, f.Directory!); + j.AsObject().TryRenameNode("BossMod.ActionManagerConfig", "BossMod.ActionTweaksConfig"); + j.AsObject().TryRenameNode("BossMod.AutorotationConfig", "BossMod.Autorotation.AutorotationConfig"); + ConvertV9Plans(j.AsObject(), f.Directory!); return j; }); return res; diff --git a/BossMod/Util/VersionedJSONSchema.cs b/BossMod/Util/VersionedJSONSchema.cs index 7ee59f7a79..e00145cec3 100644 --- a/BossMod/Util/VersionedJSONSchema.cs +++ b/BossMod/Util/VersionedJSONSchema.cs @@ -7,7 +7,7 @@ namespace BossMod; // utility for loading versioned json configuration files, executing conversion if needed public sealed class VersionedJSONSchema { - public delegate JsonObject ConvertDelegate(JsonObject input, int startingVersion, FileInfo path); + public delegate JsonNode ConvertDelegate(JsonNode input, int startingVersion, FileInfo path); public readonly int MinSupportedVersion; public readonly List Converters = []; @@ -30,7 +30,12 @@ public sealed class VersionedJSONSchema return (json, jpayload); // execute the conversion - var converted = JsonObject.Create(jpayload) ?? throw new ArgumentException($"Failed to upgrade {file.FullName} from {version} to {CurrentVersion}"); + JsonNode converted = jpayload.ValueKind switch + { + JsonValueKind.Object => JsonObject.Create(jpayload)!, + JsonValueKind.Array => JsonArray.Create(jpayload)!, + _ => throw new ArgumentException($"Config file {file.FullName} has unsupported payload type {jpayload.ValueKind}") + }; for (int i = version - MinSupportedVersion; i < Converters.Count; ++i) converted = Converters[i](converted, version, file);