From 072607182dd0f840e69ba29fe49d88ea6e07ea7e Mon Sep 17 00:00:00 2001 From: AceAkechi123 Date: Sun, 19 Jan 2025 05:23:51 -0800 Subject: [PATCH 01/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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); From d71e673081aedf66360bc43022e82e157fa7ede0 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Mon, 27 Jan 2025 13:32:48 +0100 Subject: [PATCH 34/35] fixed comments --- .../Dungeon/D02WorqorZormor/D023Gurfurlur.cs | 2 +- .../TheHiddenCanalsOfUznair/Airavata.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs index 22e663f234..783a2fa5b6 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs @@ -174,7 +174,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) public override void OnCastFinished(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID is AID.Allfire1 or AID.Allfire2 or AID.Allfire3) + if (AOEs.Count != 0 && (AID)spell.Action.ID is AID.Allfire1 or AID.Allfire2 or AID.Allfire3) AOEs.RemoveAt(0); } diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs index 29863cc883..2cd6a45c1b 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs @@ -12,11 +12,11 @@ public enum OID : uint GoldenApa = 0x1FEC, // R3.12 GoldenDhara = 0x1FEB, // R1.95 - CanalQueen = 0x1FD1, // R0.840, x0 (spawn during fight) - CanalEgg = 0x1FCE, // R0.840, x0 (spawn during fight) - CanalTomato = 0x1FD0, // R0.840, x0 (spawn during fight) - CanalGarlic = 0x1FCF, // R0.840, x0 (spawn during fight) - CanalOnion = 0x1FCD, // R0.840, x0 (spawn during fight) + CanalQueen = 0x1FD1, // R0.84, icon 5, needs to be killed in order from 1 to 5 for maximum rewards + CanalEgg = 0x1FCE, // R0.84, icon 2, needs to be killed in order from 1 to 5 for maximum rewards + CanalTomato = 0x1FD0, // R0.84, icon 4, needs to be killed in order from 1 to 5 for maximum rewards + CanalGarlic = 0x1FCF, // R0.84, icon 3, needs to be killed in order from 1 to 5 for maximum rewards + CanalOnion = 0x1FCD, // R0.84, icon 1, needs to be killed in order from 1 to 5 for maximum rewards NamazuStickywhisker = 0x2063, // R0.54 Abharamu = 0x2064, // R3.42 @@ -50,9 +50,9 @@ public enum AID : uint RaucousScritch = 8598, // Abharamu->self, 2.5s cast, range 5+R 120-degree cone Hurl = 5352, // Abharamu->location, 3.0s cast, range 6 circle - PungentPirouette = 6450, // 1FCF->self, 3.5s cast, range 6+R circle + PungentPirouette = 6450, // CanalGarlic->self, 3.5s cast, range 6+R circle PluckAndPrune = 6449, // CanalEgg->self, 3.5s cast, range 6+R circle - HeirloomScream = 6451, // 1FD0->self, 3.5s cast, range 6+R circle + HeirloomScream = 6451, // CanalTomato->self, 3.5s cast, range 6+R circle Pollen = 6452, // CanalQueen->self, 3.5s cast, range 6+R circle TearyTwirl = 6448, // CanalOnion->self, 3.5s cast, range 6+R circle From 7e1ecfe22f21011b86d897185738228563c44832 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:15:49 +0100 Subject: [PATCH 35/35] merge fixes --- BossMod/Autorotation/PlanPresetConverter.cs | 4 +- BossMod/Autorotation/RotationModuleManager.cs | 2 +- BossMod/BossModule/AOEShapes.cs | 23 +- BossMod/BossModule/ArenaBounds.cs | 87 +++-- BossMod/BossModule/BossModule.cs | 3 +- BossMod/BossModule/MiniArena.cs | 24 +- BossMod/BossModule/Shapes.cs | 174 +++++---- BossMod/Components/Cleave.cs | 4 +- BossMod/Components/Tethers.cs | 122 ++++++- BossMod/Config/ColorConfig.cs | 2 +- BossMod/Config/ConfigConverter.cs | 2 +- BossMod/Config/ConfigRoot.cs | 3 +- BossMod/Config/ModuleViewer.cs | 1 - BossMod/Data/Actor.cs | 6 +- BossMod/Data/DeepDungeonState.cs | 28 +- BossMod/Data/WorldState.cs | 7 +- BossMod/Framework/Plugin.cs | 4 +- BossMod/Framework/WorldStateGameSync.cs | 60 ++-- .../Pictomancer/MindOverManor.cs | 2 +- .../FlightOfTheGriffin.cs | 2 +- .../JanquetilaquesPortrait.cs | 2 +- .../SomewhereOnlySheKnows/TheWingedSteed.cs | 2 +- .../Viper/FangsOfTheViper.cs | 2 +- .../Viper/VengeanceOfTheViper.cs | 2 +- .../Quest/{RoleQuests => Role}/AHunterTrue.cs | 2 +- .../AnAntidoteForAnarchy.cs | 2 +- .../{RoleQuests => Role}/DreamsOfANewDay.cs | 2 +- .../HeroesAndPretenders.cs | 4 +- .../TheMightiestShield.cs | 2 +- .../Raid/M01NBIackCat/PredaceousPounce.cs | 6 +- .../Dawntrail/Unreal/Un1Byakko/Un1Byakko.cs | 2 +- .../Alliance/A13Azeyma/WildfireWard.cs | 12 +- .../Alliance/A31Thaliak/Tetraktys.cs | 9 +- .../Quest/Job/Reaper/TheKillingArt.cs | 109 ++++++ .../AncelAndMahaud.cs | 95 +++++ .../LifeEphemeralPathEternal/Guildivain.cs | 125 +++++++ .../Endwalker/Quest/Job/Sage/SagesFocus.cs | 83 +++++ .../LifeEphemeralPathEternal/AncelRockfist.cs | 59 --- .../Quest/LifeEphemeralPathEternal/Enums.cs | 74 ---- .../LifeEphemeralPathEternal/Guildivain.cs | 91 ----- .../AsTheHeavensBurn/P1TerminusIdolizer.cs | 2 +- .../AsTheHeavensBurn/P2TerminusLacerator.cs | 2 +- .../AsTheHeavensBurn/P3TerminusVanquisher.cs | 4 +- BossMod/Modules/Endwalker/Quest/SagesFocus.cs | 65 ---- .../Modules/Endwalker/Quest/TheKillingArt.cs | 87 ----- .../Modules/Endwalker/Ultimate/DSW2/DSW2.cs | 14 +- .../Endwalker/Unreal/Un4Zurvan/Un4Zurvan.cs | 8 +- .../Dungeon/D04TheVault/D042SerGrinnaux.cs | 2 +- .../D053TheEverlivingBibliotaph.cs | 2 +- .../D060Trash1.cs | 8 +- .../Dungeon/D13SohrKhai/D0130CloudGardener.cs | 2 +- .../Extreme/Ext3Thordan/Ex3Thordan.cs | 2 +- .../Heavensward/Quest/DivineIntervention.cs | 63 ---- .../Quest/{ => Job/Dragoon}/DragoonsFate.cs | 36 +- .../Quest/{ => MSQ}/ASpectacleForTheAges.cs | 6 +- .../{ => MSQ}/CloseEncountersOfTheVIthKind.cs | 13 +- .../Quest/MSQ/DivineIntervention.cs | 70 ++++ .../Quest/{ => MSQ}/FlyFreeMyPretty.cs | 53 +-- .../Heavensward/Quest/MSQ/Heliodrome.cs | 125 ------- .../Heavensward/Quest/MSQ/OneLifeOneWorld.cs | 6 +- .../Heavensward/Quest/TheFateOfStars.cs | 50 --- .../Quest/TheWarringTriad/ABloodyReunion.cs | 59 +++ .../Extreme/Ex2Garuda/Ex2Garuda.cs | 6 +- .../Quest/{ => MSQ}/OperationArchon.cs | 31 +- .../RealmReborn/Quest/MSQ/TheStepsOfFaith.cs | 340 ++++++++++++++++++ .../Quest/{ => MSQ}/TheUltimateWeapon.cs | 77 ++-- .../RealmReborn/Quest/TheStepsOfFaith.cs | 263 -------------- .../Raid/T01Caduceus/T01Caduceus.cs | 2 +- .../Raid/T04Gauntlet/T04Gauntlet.cs | 2 +- .../D08AkadaemiaAnyder/D082MorbolMarquis.cs | 19 +- .../Quest/{ => Job/Dancer}/GambolingForGil.cs | 21 +- .../{ => Job/Dancer}/SaveTheLastDanceForMe.cs | 34 +- .../Quest/Job/Gunbreaker/SteelAgainstSteel.cs | 99 +++++ .../Quest/{ => MSQ}/AFeastOfLies.cs | 61 ++-- .../{FullSteamAhead.cs => MSQ/ComingClean.cs} | 51 +-- .../{ => MSQ}/DeathUntoDawn/P1TelotekGamma.cs | 6 +- .../{ => MSQ}/DeathUntoDawn/P2LunarOdin.cs | 45 +-- .../{ => MSQ}/DeathUntoDawn/P3LunarRavana.cs | 17 +- .../{ => MSQ}/DeathUntoDawn/P4LunarIfrit.cs | 16 +- .../Quest/{ => MSQ}/FadedMemories/Ardbert.cs | 53 ++- .../{ => MSQ}/FadedMemories/FadedMemories.cs | 4 +- .../FadedMemories/FlameGeneralAldynn.cs | 9 +- .../{ => MSQ}/FadedMemories/KingThordan.cs | 12 +- .../Quest/{ => MSQ}/FadedMemories/Nidhogg.cs | 10 +- .../Quest/{ => MSQ}/FadedMemories/Zenos.cs | 9 +- .../Quest/MSQ/TheGreatShipVylbrand.cs | 207 +++++++++++ .../Quest/{ => MSQ}/TheOracleOfLight.cs | 14 +- .../Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs | 39 +- .../Quest/{ => Role}/ATearfulReunion.cs | 21 +- .../Quest/{ => Role}/CourageBornOfFear.cs | 27 +- .../Quest/{ => Role}/NyelbertsLament.cs | 21 +- .../Quest/{ => Role}/TheHardenedHeart.cs | 14 +- .../Quest/{ => Role}/TheHuntersLegacy.cs | 23 +- .../TheLostAndTheFound/Sophrosyne.cs | 4 +- .../TheLostAndTheFound/Yxtlilton.cs | 24 +- .../Quest/{ => Role}/TheSoulOfTemperance.cs | 16 +- .../Quest/{ => Role}/ToHaveLovedAndLost.cs | 34 +- .../SleepNowInSapphire/P2SapphireWeapon.cs | 86 ----- .../Shadowbringers/Quest/SteelAgainstSteel.cs | 128 ------- .../Quest/TheGreatShipVylbrand.cs | 116 ------ .../SleepNowInSapphire/P1GuidanceSystem.cs | 6 +- .../SleepNowInSapphire/P2SapphireWeapon.cs | 70 ++++ .../Quest/VowsOfVirtueDeedsOfCruelty.cs | 149 -------- .../SecretKeeper.cs | 2 +- .../Shadowbringers/Ultimate/TEA/TEA.cs | 4 +- .../BaldesionsArsenal/BA1Owain/IvoryPalm.cs | 2 +- .../TheOrphansAndTheBrokenBlade.cs | 12 +- .../Quest/{ => Job/Dragoon}/DragonSound.cs | 6 +- .../Quest/{ => Job/Monk}/ThePowerToProtect.cs | 14 +- .../{ => Job/Paladin}/RaisingTheSword.cs | 14 +- .../Quest/{ => Job/Samurai}/BloodOnTheDeck.cs | 23 +- .../{ => Job/Samurai}/TheBattleOnBekko.cs | 45 +-- .../{ => Job/Samurai}/TheFaceOfTrueEvil.cs | 36 +- .../{ => Job/Scholar}/OurUnsungHeroes.cs | 21 +- .../{ => Job/Summoner}/AnArtForTheLiving.cs | 38 +- .../{ => MSQ}/ARequiemForHeroes/Enums.cs | 13 +- .../Quest/{ => MSQ}/ARequiemForHeroes/P1.cs | 2 +- .../Quest/{ => MSQ}/ARequiemForHeroes/P2.cs | 37 +- .../{ => MSQ}/BestServedWithColdSteel.cs | 29 +- .../Quest/{ => MSQ}/EmissaryOfTheDawn.cs | 10 +- .../Quest/{ => MSQ}/HisForgottenHome.cs | 24 +- .../Quest/{ => MSQ}/HopeOnTheWaves.cs | 23 +- .../Stormblood/Quest/{ => MSQ}/Naadam.cs | 36 +- .../Quest/{ => MSQ}/ReturnOfTheBull.cs | 18 +- .../Quest/{ => MSQ}/RhalgrsBeacon.cs | 10 +- .../Quest/{ => MSQ}/TheMeasureOfHisReach.cs | 20 +- .../Stormblood/Quest/{ => MSQ}/TheResonant.cs | 13 +- .../{ => MSQ}/TheTimeBetweenTheSeconds.cs | 20 +- .../Quest/{ => MSQ}/TheWillOfTheMoon.cs | 46 +-- .../{ => TheFourLords}/TortoiseInTime.cs | 22 +- .../TheHiddenCanalsOfUznair/Airavata.cs | 2 +- .../TheLostCanalsOfUznair/CanalIcebeast.cs | 2 +- .../AltarAiravata.cs | 2 +- .../TheShiftingAltarsOfUznair/AltarArachne.cs | 2 +- .../TheShiftingAltarsOfUznair/AltarBeast.cs | 2 +- .../TheShiftingAltarsOfUznair/AltarChimera.cs | 2 +- .../AltarDiresaur.cs | 2 +- .../AltarDullahan.cs | 2 +- .../TheShiftingAltarsOfUznair/AltarKelpie.cs | 2 +- .../TheShiftingAltarsOfUznair/AltarSkatene.cs | 2 +- .../TheShiftingAltarsOfUznair/AltarTotem.cs | 2 +- .../TheShiftingAltarsOfUznair/TheOlderOne.cs | 2 +- .../TheShiftingAltarsOfUznair/TheWinged.cs | 2 +- .../Modules/Stormblood/Ultimate/UCOB/UCOB.cs | 4 +- .../Ultimate/UWU/P4ViscousAetheroplasm.cs | 2 +- .../Modules/Stormblood/Ultimate/UWU/UWU.cs | 8 +- BossMod/Util/Color.cs | 1 + BossMod/Util/CurveApprox.cs | 12 - 148 files changed, 2402 insertions(+), 2301 deletions(-) rename BossMod/Modules/Dawntrail/Quest/{JobQuests => Job}/Pictomancer/MindOverManor.cs (98%) rename BossMod/Modules/Dawntrail/Quest/{JobQuests => Job}/Pictomancer/SomewhereOnlySheKnows/FlightOfTheGriffin.cs (98%) rename BossMod/Modules/Dawntrail/Quest/{JobQuests => Job}/Pictomancer/SomewhereOnlySheKnows/JanquetilaquesPortrait.cs (98%) rename BossMod/Modules/Dawntrail/Quest/{JobQuests => Job}/Pictomancer/SomewhereOnlySheKnows/TheWingedSteed.cs (97%) rename BossMod/Modules/Dawntrail/Quest/{JobQuests => Job}/Viper/FangsOfTheViper.cs (98%) rename BossMod/Modules/Dawntrail/Quest/{JobQuests => Job}/Viper/VengeanceOfTheViper.cs (99%) rename BossMod/Modules/Dawntrail/Quest/{RoleQuests => Role}/AHunterTrue.cs (99%) rename BossMod/Modules/Dawntrail/Quest/{RoleQuests => Role}/AnAntidoteForAnarchy.cs (99%) rename BossMod/Modules/Dawntrail/Quest/{RoleQuests => Role}/DreamsOfANewDay.cs (99%) rename BossMod/Modules/Dawntrail/Quest/{RoleQuests => Role}/HeroesAndPretenders.cs (97%) rename BossMod/Modules/Dawntrail/Quest/{RoleQuests => Role}/TheMightiestShield.cs (99%) create mode 100644 BossMod/Modules/Endwalker/Quest/Job/Reaper/TheKillingArt.cs create mode 100644 BossMod/Modules/Endwalker/Quest/Job/Sage/LifeEphemeralPathEternal/AncelAndMahaud.cs create mode 100644 BossMod/Modules/Endwalker/Quest/Job/Sage/LifeEphemeralPathEternal/Guildivain.cs create mode 100644 BossMod/Modules/Endwalker/Quest/Job/Sage/SagesFocus.cs delete mode 100644 BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs delete mode 100644 BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs delete mode 100644 BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs delete mode 100644 BossMod/Modules/Endwalker/Quest/SagesFocus.cs delete mode 100644 BossMod/Modules/Endwalker/Quest/TheKillingArt.cs delete mode 100644 BossMod/Modules/Heavensward/Quest/DivineIntervention.cs rename BossMod/Modules/Heavensward/Quest/{ => Job/Dragoon}/DragoonsFate.cs (61%) rename BossMod/Modules/Heavensward/Quest/{ => MSQ}/ASpectacleForTheAges.cs (75%) rename BossMod/Modules/Heavensward/Quest/{ => MSQ}/CloseEncountersOfTheVIthKind.cs (79%) create mode 100644 BossMod/Modules/Heavensward/Quest/MSQ/DivineIntervention.cs rename BossMod/Modules/Heavensward/Quest/{ => MSQ}/FlyFreeMyPretty.cs (67%) delete mode 100644 BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs delete mode 100644 BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs create mode 100644 BossMod/Modules/Heavensward/Quest/TheWarringTriad/ABloodyReunion.cs rename BossMod/Modules/RealmReborn/Quest/{ => MSQ}/OperationArchon.cs (54%) create mode 100644 BossMod/Modules/RealmReborn/Quest/MSQ/TheStepsOfFaith.cs rename BossMod/Modules/RealmReborn/Quest/{ => MSQ}/TheUltimateWeapon.cs (58%) delete mode 100644 BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs rename BossMod/Modules/Shadowbringers/Quest/{ => Job/Dancer}/GambolingForGil.cs (75%) rename BossMod/Modules/Shadowbringers/Quest/{ => Job/Dancer}/SaveTheLastDanceForMe.cs (65%) create mode 100644 BossMod/Modules/Shadowbringers/Quest/Job/Gunbreaker/SteelAgainstSteel.cs rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/AFeastOfLies.cs (56%) rename BossMod/Modules/Shadowbringers/Quest/{FullSteamAhead.cs => MSQ/ComingClean.cs} (63%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/DeathUntoDawn/P1TelotekGamma.cs (80%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/DeathUntoDawn/P2LunarOdin.cs (63%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/DeathUntoDawn/P3LunarRavana.cs (85%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/DeathUntoDawn/P4LunarIfrit.cs (64%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/FadedMemories/Ardbert.cs (50%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/FadedMemories/FadedMemories.cs (96%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/FadedMemories/FlameGeneralAldynn.cs (62%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/FadedMemories/KingThordan.cs (72%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/FadedMemories/Nidhogg.cs (50%) rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/FadedMemories/Zenos.cs (61%) create mode 100644 BossMod/Modules/Shadowbringers/Quest/MSQ/TheGreatShipVylbrand.cs rename BossMod/Modules/Shadowbringers/Quest/{ => MSQ}/TheOracleOfLight.cs (67%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/ATearfulReunion.cs (76%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/CourageBornOfFear.cs (73%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/NyelbertsLament.cs (83%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/TheHardenedHeart.cs (90%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/TheHuntersLegacy.cs (71%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/TheLostAndTheFound/Sophrosyne.cs (83%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/TheLostAndTheFound/Yxtlilton.cs (79%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/TheSoulOfTemperance.cs (75%) rename BossMod/Modules/Shadowbringers/Quest/{ => Role}/ToHaveLovedAndLost.cs (65%) delete mode 100644 BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs delete mode 100644 BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs delete mode 100644 BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs rename BossMod/Modules/Shadowbringers/Quest/{ => TheSorrowOfWerlyt}/SleepNowInSapphire/P1GuidanceSystem.cs (79%) create mode 100644 BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P2SapphireWeapon.cs delete mode 100644 BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs rename BossMod/Modules/Stormblood/Quest/{ => Job/DarkKnight}/TheOrphansAndTheBrokenBlade.cs (77%) rename BossMod/Modules/Stormblood/Quest/{ => Job/Dragoon}/DragonSound.cs (86%) rename BossMod/Modules/Stormblood/Quest/{ => Job/Monk}/ThePowerToProtect.cs (79%) rename BossMod/Modules/Stormblood/Quest/{ => Job/Paladin}/RaisingTheSword.cs (80%) rename BossMod/Modules/Stormblood/Quest/{ => Job/Samurai}/BloodOnTheDeck.cs (52%) rename BossMod/Modules/Stormblood/Quest/{ => Job/Samurai}/TheBattleOnBekko.cs (53%) rename BossMod/Modules/Stormblood/Quest/{ => Job/Samurai}/TheFaceOfTrueEvil.cs (59%) rename BossMod/Modules/Stormblood/Quest/{ => Job/Scholar}/OurUnsungHeroes.cs (68%) rename BossMod/Modules/Stormblood/Quest/{ => Job/Summoner}/AnArtForTheLiving.cs (69%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/ARequiemForHeroes/Enums.cs (77%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/ARequiemForHeroes/P1.cs (96%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/ARequiemForHeroes/P2.cs (58%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/BestServedWithColdSteel.cs (80%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/EmissaryOfTheDawn.cs (80%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/HisForgottenHome.cs (75%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/HopeOnTheWaves.cs (66%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/Naadam.cs (73%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/ReturnOfTheBull.cs (84%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/RhalgrsBeacon.cs (90%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/TheMeasureOfHisReach.cs (58%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/TheResonant.cs (83%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/TheTimeBetweenTheSeconds.cs (76%) rename BossMod/Modules/Stormblood/Quest/{ => MSQ}/TheWillOfTheMoon.cs (72%) rename BossMod/Modules/Stormblood/Quest/{ => TheFourLords}/TortoiseInTime.cs (78%) diff --git a/BossMod/Autorotation/PlanPresetConverter.cs b/BossMod/Autorotation/PlanPresetConverter.cs index 729e120b02..121ba710ed 100644 --- a/BossMod/Autorotation/PlanPresetConverter.cs +++ b/BossMod/Autorotation/PlanPresetConverter.cs @@ -10,8 +10,8 @@ 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); + public static readonly VersionedJSONSchema PlanSchema = BuildSchema(true); + public static readonly VersionedJSONSchema PresetSchema = BuildSchema(false); private static VersionedJSONSchema BuildSchema(bool plan) { diff --git a/BossMod/Autorotation/RotationModuleManager.cs b/BossMod/Autorotation/RotationModuleManager.cs index 190ec3867e..db404af416 100644 --- a/BossMod/Autorotation/RotationModuleManager.cs +++ b/BossMod/Autorotation/RotationModuleManager.cs @@ -132,7 +132,7 @@ 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) ?? []); + public override string ToString() => string.Join(", ", ActiveModules?.Select(m => m.Module.GetType().Name) ?? []); private IEnumerable FilteredPartyMembers(StrategyPartyFiltering filter) { diff --git a/BossMod/BossModule/AOEShapes.cs b/BossMod/BossModule/AOEShapes.cs index 6a8ee345d9..0a9b7235ba 100644 --- a/BossMod/BossModule/AOEShapes.cs +++ b/BossMod/BossModule/AOEShapes.cs @@ -247,7 +247,8 @@ public RelSimplifiedComplexPolygon GetCombinedPolygon(WPos origin) if (Shapes2 != null) { Polygon = clipper.Simplify(shapes1); - for (var i = 0; i < Shapes2.Count; ++i) + var count = Shapes2.Count; + for (var i = 0; i < count; ++i) { var shape = Shapes2[i]; var singleShapeOperand = CreateOperandFromShape(shape, origin); @@ -312,27 +313,31 @@ public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint col public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = 0) { var combinedPolygon = Polygon ?? GetCombinedPolygon(origin); - for (var i = 0; i < combinedPolygon.Parts.Count; ++i) + var count = combinedPolygon.Parts.Count; + for (var i = 0; i < count; ++i) { var part = combinedPolygon.Parts[i]; - var exteriorEdges = part.ExteriorEdges.ToList(); - for (var j = 0; j < exteriorEdges.Count; ++j) + var exteriorEdges = part.ExteriorEdges; + var exteriorCount = exteriorEdges.Count; + for (var j = 0; j < exteriorCount; ++j) { var (start, end) = exteriorEdges[j]; arena.PathLineTo(origin + start); - if (j != exteriorEdges.Count - 1) + if (j != exteriorCount - 1) arena.PathLineTo(origin + end); } MiniArena.PathStroke(true, color); - foreach (var holeIndex in part.Holes) + var lenHoles = part.Holes.Length; + for (var k = 0; k < lenHoles; ++k) { - var interiorEdges = part.InteriorEdges(holeIndex).ToList(); - for (var j = 0; j < interiorEdges.Count; ++j) + var interiorEdges = part.InteriorEdges(part.Holes[k]); + var interiorCount = interiorEdges.Count; + for (var j = 0; j < interiorCount; ++j) { var (start, end) = interiorEdges[j]; arena.PathLineTo(origin + start); - if (j != interiorEdges.Count - 1) + if (j != interiorCount - 1) arena.PathLineTo(origin + end); } MiniArena.PathStroke(true, color); diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index de0be6bb3a..a155f50340 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -58,9 +58,11 @@ public List ClipAndTriangulateCone(WDir centerOffset, float innerRa (true, false) => CurveApprox.DonutSector(innerRadius, outerRadius, centerDirection - halfAngle, centerDirection + halfAngle, MaxApproxError), (true, true) => CurveApprox.Donut(innerRadius, outerRadius, MaxApproxError), }; - for (var i = 0; i < points.Length; ++i) + var len = points.Length; + var offset = centerOffset; + for (var i = 0; i < len; ++i) { - points[i] += centerOffset; + points[i] += offset; } return ClipAndTriangulate(points); } @@ -68,9 +70,11 @@ public List ClipAndTriangulateCone(WDir centerOffset, float innerRa public List ClipAndTriangulateCircle(WDir centerOffset, float radius) { var points = CurveApprox.Circle(radius, MaxApproxError); - for (var i = 0; i < points.Length; ++i) + var len = points.Length; + var offset = centerOffset; + for (var i = 0; i < len; ++i) { - points[i] += centerOffset; + points[i] += offset; } return ClipAndTriangulate(points); } @@ -78,9 +82,11 @@ public List ClipAndTriangulateCircle(WDir centerOffset, float radiu public List ClipAndTriangulateCapsule(WDir centerOffset, WDir direction, float radius, float length) { var points = CurveApprox.Capsule(direction, length, radius, MaxApproxError); - for (var i = 0; i < points.Length; ++i) + var len = points.Length; + var offset = centerOffset; + for (var i = 0; i < len; ++i) { - points[i] += centerOffset; + points[i] += offset; } return ClipAndTriangulate(points); } @@ -90,9 +96,11 @@ public List ClipAndTriangulateDonut(WDir centerOffset, float innerR if (innerRadius < outerRadius && innerRadius >= 0) { var points = CurveApprox.Donut(innerRadius, outerRadius, MaxApproxError); - for (var i = 0; i < points.Length; ++i) + var len = points.Length; + var offset = centerOffset; + for (var i = 0; i < len; ++i) { - points[i] += centerOffset; + points[i] += offset; } return ClipAndTriangulate(points); } @@ -144,13 +152,18 @@ public sealed record class ArenaBoundsCircle(float Radius, float MapResolution = protected override PolygonClipper.Operand BuildClipPoly() => new((ReadOnlySpan)CurveApprox.Circle(Radius, MaxApproxError)); public override void PathfindMap(Pathfinding.Map map, WPos center) => map.Init(_cachedMap ??= BuildMap(), center); - public override bool Contains(WDir offset) => offset.LengthSq() <= Radius * Radius; + public override bool Contains(WDir offset) + { + var radius = Radius; + return offset.LengthSq() <= radius * radius; + } public override float IntersectRay(WDir originOffset, WDir dir) => Intersect.RayCircle(originOffset, dir, Radius); public override WDir ClampToBounds(WDir offset) { - if (offset.LengthSq() > Radius * Radius) - offset *= Radius / offset.Length(); + var radius = Radius; + if (offset.LengthSq() > radius * radius) + offset *= radius / offset.Length(); return offset; } @@ -177,8 +190,11 @@ private static float CalculateScaleFactor(Angle Rotation) public override void PathfindMap(Pathfinding.Map map, WPos center) => map.Init(_cachedMap ??= BuildMap(), center); private Pathfinding.Map BuildMap() { - var map = new Pathfinding.Map(MapResolution, default, HalfWidth, HalfHeight, Rotation); - map.BlockPixelsInside2(ShapeDistance.InvertedRect(default, Rotation, HalfHeight, HalfHeight, HalfWidth), -1); + var halfWidth = HalfWidth; + var halfHeight = HalfHeight; + var rotation = Rotation; + var map = new Pathfinding.Map(MapResolution, default, halfWidth, halfHeight, rotation); + map.BlockPixelsInside2(ShapeDistance.InvertedRect(default, rotation, halfHeight, halfHeight, halfWidth), -1); return map; } @@ -187,13 +203,16 @@ private Pathfinding.Map BuildMap() public override WDir ClampToBounds(WDir offset) { - var offsetX = offset.Dot(Orientation.OrthoL()); - var offsetY = offset.Dot(Orientation); - if (Math.Abs(offsetX) > HalfWidth) - offsetX = Math.Sign(offsetX) * HalfWidth; - if (Math.Abs(offsetY) > HalfHeight) - offsetY = Math.Sign(offsetY) * HalfHeight; - return Orientation.OrthoL() * offsetX + Orientation * offsetY; + var orientation = Orientation; + var halfWidth = HalfWidth; + var halfHeight = HalfHeight; + var offsetX = offset.Dot(orientation.OrthoL()); + var offsetY = offset.Dot(orientation); + if (Math.Abs(offsetX) > halfWidth) + offsetX = Math.Sign(offsetX) * halfWidth; + if (Math.Abs(offsetY) > halfHeight) + offsetY = Math.Sign(offsetY) * halfHeight; + return orientation.OrthoL() * offsetX + orientation * offsetY; } } @@ -214,11 +233,15 @@ public ArenaBoundsCustom(float Radius, RelSimplifiedComplexPolygon Poly, float M poly = Poly; var edgeList = new List<(WDir, WDir)>(); - for (var i = 0; i < Poly.Parts.Count; ++i) + var count = Poly.Parts.Count; + for (var i = 0; i < count; ++i) { var part = Poly.Parts[i]; edgeList.AddRange(part.ExteriorEdges); - for (var j = 0; j < part.Holes.Length; ++j) + var len = part.Holes.Length; + if (len == 0) + continue; + for (var j = 0; j < len; ++j) { edgeList.AddRange(part.InteriorEdges(j)); } @@ -253,7 +276,8 @@ public override WDir ClampToBounds(WDir offset) } var minDistance = float.MaxValue; var nearestPoint = offset; - for (var i = 0; i < edges.Length; ++i) + var len = edges.Length; + for (var i = 0; i < len; ++i) { ref var edge = ref edges[i]; var edge1 = edge.Item1; @@ -279,21 +303,24 @@ private Pathfinding.Map BuildMap() if (HalfHeight == default) // calculate bounding box if not already done by ArenaBoundsComplex to reduce amount of point in polygon tests { float minX = float.MaxValue, maxX = float.MinValue, minZ = float.MaxValue, maxZ = float.MinValue; - - for (var i = 0; i < polygon.Parts.Count; ++i) + var count = polygon.Parts.Count; + for (var i = 0; i < count; ++i) { var part = polygon.Parts[i]; - for (var j = 0; j < part.Exterior.Length; ++j) + var len = part.Exterior.Length; + for (var j = 0; j < len; ++j) { var vertex = part.Exterior[j]; + var vertexX = vertex.X; + var vertexZ = vertex.Z; if (vertex.X < minX) - minX = vertex.X; + minX = vertexX; if (vertex.X > maxX) - maxX = vertex.X; + maxX = vertexX; if (vertex.Z < minZ) - minZ = vertex.Z; + minZ = vertexZ; if (vertex.Z > maxZ) - maxZ = vertex.Z; + maxZ = vertexZ; } } HalfWidth = (maxX - minX) * Half; diff --git a/BossMod/BossModule/BossModule.cs b/BossMod/BossModule/BossModule.cs index 2dad51035d..198c3e2753 100644 --- a/BossMod/BossModule/BossModule.cs +++ b/BossMod/BossModule/BossModule.cs @@ -36,7 +36,8 @@ public List Enemies(uint oid) public List Enemies(ReadOnlySpan enemies) { List relevantenemies = []; - for (var i = 0; i < enemies.Length; ++i) + var len = enemies.Length; + for (var i = 0; i < len; ++i) { var enemy = enemies[i]; var entry = RelevantEnemies.GetValueOrDefault(enemy); diff --git a/BossMod/BossModule/MiniArena.cs b/BossMod/BossModule/MiniArena.cs index 23358889ae..b9dcfa155d 100644 --- a/BossMod/BossModule/MiniArena.cs +++ b/BossMod/BossModule/MiniArena.cs @@ -281,7 +281,8 @@ public void Zone(List triangulation, uint color = 0) var drawlist = ImGui.GetWindowDrawList(); var restoreFlags = drawlist.Flags; drawlist.Flags &= ~ImDrawListFlags.AntiAliasedFill; - for (var i = 0; i < triangulation.Count; ++i) + var count = triangulation.Count; + for (var i = 0; i < count; ++i) { var tri = triangulation[i]; drawlist.AddTriangleFilled(ScreenCenter + WorldOffsetToScreenOffset(tri.A), ScreenCenter + WorldOffsetToScreenOffset(tri.B), ScreenCenter + WorldOffsetToScreenOffset(tri.C), color != 0 ? color : Colors.AOE); @@ -349,13 +350,13 @@ public void TextWorld(WPos center, string text, uint color, float fontSize = 17) public void Border(uint color) { var dl = ImGui.GetWindowDrawList(); - - for (var i = 0; i < _bounds.ShapeSimplified.Parts.Count; ++i) + var count = _bounds.ShapeSimplified.Parts.Count; + for (var i = 0; i < count; ++i) { var part = _bounds.ShapeSimplified.Parts[i]; Vector2? lastPoint = null; - - for (var j = 0; j < part.Exterior.Length; ++j) + var exteriorLen = part.Exterior.Length; + for (var j = 0; j < exteriorLen; ++j) { var offset = part.Exterior[j]; var currentPoint = ScreenCenter + WorldOffsetToScreenOffset(offset); @@ -366,12 +367,14 @@ public void Border(uint color) dl.PathStroke(color, ImDrawFlags.Closed, 2); - foreach (var holeIndex in part.Holes) + var lenHoles = part.Holes.Length; + for (var l = 0; l < lenHoles; ++l) { lastPoint = null; - var holeInteriorPoints = part.Interior(holeIndex); - for (var k = 0; k < holeInteriorPoints.Length; ++k) + var holeInteriorPoints = part.Interior(part.Holes[l]); + var interiorLen = holeInteriorPoints.Length; + for (var k = 0; k < interiorLen; ++k) { var offset = holeInteriorPoints[k]; var currentPoint = ScreenCenter + WorldOffsetToScreenOffset(offset); @@ -467,7 +470,10 @@ public void Actors(IEnumerable actors, uint color = 0, bool allowDeadAndU public void Actors(List actors, uint color = 0, bool allowDeadAndUntargetable = false) { - for (var i = 0; i < actors.Count; ++i) + var count = actors.Count; + if (count == 0) + return; + for (var i = 0; i < count; ++i) { Actor(actors[i], color == 0 ? Colors.Enemy : color, allowDeadAndUntargetable); } diff --git a/BossMod/BossModule/Shapes.cs b/BossMod/BossModule/Shapes.cs index d8192cd673..ebd40cf634 100644 --- a/BossMod/BossModule/Shapes.cs +++ b/BossMod/BossModule/Shapes.cs @@ -194,39 +194,6 @@ public override List Contour(WPos center) public override string ToString() => $"Cross:{Center.X},{Center.Z},{Length},{HalfWidth},{Rotation}"; } -// Equilateral triangle defined by center, sidelength and rotation -public sealed record class TriangleE(WPos Center, float SideLength, Angle Rotation = default) : Shape -{ - private static readonly float heightFactor = MathF.Sqrt(3) * Half; - - public override List Contour(WPos center) - { - if (Points == null) - { - var height = SideLength * heightFactor; - var halfSideLength = SideLength * Half; - var halfHeight = height * Half; - var (sin, cos) = ((float, float))Math.SinCos(Rotation.Rad); - var halfSideCos = halfSideLength * cos; - var halfSideSin = halfSideLength * sin; - var halfHeightSin = halfHeight * sin; - var halfHeightCos = halfHeight * cos; - Points = - [ - new WDir(halfSideCos - halfHeightSin, halfSideSin + halfHeightCos), - new WDir(-halfSideCos - halfHeightSin, -halfSideSin + halfHeightCos), - new WDir(halfHeightSin, -halfHeight * cos) - ]; - } - var offset = Center - center; - var result = new List(3); - for (var i = 0; i < 3; ++i) - result.Add(Points[i] + offset); - return result; - } - public override string ToString() => $"TriangleE:{Center.X},{Center.Z},{SideLength},{Rotation}"; -} - // for polygons with edge count number of lines of symmetry, eg. pentagons, hexagons and octagons public sealed record class Polygon(WPos Center, float Radius, int Edges, Angle Rotation = default) : Shape { @@ -236,13 +203,13 @@ public override List Contour(WPos center) { var angleIncrement = Angle.DoublePI / Edges; var initialRotation = Rotation.Rad; - var vertices = new List(Edges); + var vertices = new WDir[Edges]; for (var i = 0; i < Edges; ++i) { var (sin, cos) = ((float, float))Math.SinCos(i * angleIncrement + initialRotation); - vertices.Add(new(Center.X + Radius * sin, Center.Z + Radius * cos)); + vertices[i] = new(Center.X + Radius * sin, Center.Z + Radius * cos); } - Points = [.. vertices]; + Points = vertices; } var len = Points.Length; var result = new List(len); @@ -262,10 +229,10 @@ public override List Contour(WPos center) { var points = CurveApprox.CircleSector(Center, Radius, StartAngle, EndAngle, MaxApproxError); var length = points.Length; - var vertices = new List(length); + var vertices = new WDir[length]; for (var i = 0; i < length; ++i) - vertices.Add(points[i] - new WPos()); - Points = [.. vertices]; + vertices[i] = points[i] - new WPos(); + Points = vertices; } var len = Points.Length; var result = new List(len); @@ -310,14 +277,17 @@ public override List Contour(WPos center) { var angleIncrement = 2 * HalfAngle.Rad / Edges; var startAngle = CenterDir.Rad - HalfAngle.Rad; - var vertices = new List(Edges + 1); + var vertices = new WDir[Edges + 2]; + var centerX = Center.X; + var CenterZ = Center.Z; + var radius = Radius; for (var i = 0; i < Edges + 1; ++i) { var (sin, cos) = ((float, float))Math.SinCos(startAngle + i * angleIncrement); - vertices.Add(new(Center.X + Radius * sin, Center.Z + Radius * cos)); + vertices[i] = new(centerX + radius * sin, CenterZ + radius * cos); } - vertices.Add(Center - new WPos()); - Points = [.. vertices]; + vertices[Edges + 1] = Center - new WPos(); + Points = vertices; } var len = Points.Length; var result = new List(len); @@ -338,18 +308,20 @@ public override List Contour(WPos center) { var angleIncrement = 2 * HalfAngle.Rad / Edges; var startAngle = CenterDir.Rad - HalfAngle.Rad; - var vertices = new List(2 * (Edges + 1)); - for (var i = 0; i < Edges + 1; ++i) + var n = Edges + 1; + var vertices = new WDir[2 * n]; + var centerX = Center.X; + var CenterZ = Center.Z; + var innerRadius = InnerRadius; + var outerRadius = OuterRadius; + for (var i = 0; i < n; ++i) { var (sin, cos) = ((float, float))Math.SinCos(startAngle + i * angleIncrement); - vertices.Add(new(Center.X + OuterRadius * sin, Center.Z + OuterRadius * cos)); + vertices[i] = new(centerX + outerRadius * sin, CenterZ + outerRadius * cos); + vertices[2 * n - 1 - i] = new WDir(centerX + innerRadius * sin, CenterZ + innerRadius * cos); } - for (var i = Edges; i >= 0; --i) - { - var (sin, cos) = ((float, float))Math.SinCos(startAngle + i * angleIncrement); - vertices.Add(new(Center.X + InnerRadius * sin, Center.Z + InnerRadius * cos)); - } - Points = [.. vertices]; + + Points = vertices; } var len = Points.Length; var result = new List(len); @@ -369,27 +341,105 @@ public override List Contour(WPos center) if (Points == null) { var angleIncrement = Angle.DoublePI / Edges; - var vertices = new List(2 * (Edges + 1)); - - for (var i = 0; i <= Edges; ++i) + var n = Edges + 1; + var vertices = new WDir[2 * n]; + var centerX = Center.X; + var CenterZ = Center.Z; + var innerRadius = InnerRadius; + var outerRadius = OuterRadius; + for (var i = 0; i < n; ++i) { var (sin, cos) = ((float, float))Math.SinCos(i * angleIncrement); - vertices.Add(new(Center.X + OuterRadius * sin, Center.Z + OuterRadius * cos)); + vertices[i] = new(centerX + outerRadius * sin, CenterZ + outerRadius * cos); + vertices[2 * n - 1 - i] = new(centerX + innerRadius * sin, CenterZ + innerRadius * cos); } + Points = vertices; + } + + var len = Points.Length; + var result = new List(len); + for (var i = 0; i < len; ++i) + result.Add(Points[i] - center); + return result; + } + + public override string ToString() => $"DonutV:{Center.X},{Center.Z},{InnerRadius},{OuterRadius},{Edges}"; +} - for (var i = Edges; i >= 0; --i) +// Approximates an ellipse with a customizable number of edges +public sealed record class Ellipse(WPos Center, float HalfWidth, float HalfHeight, int Edges, Angle Rotation = default) : Shape +{ + public override List Contour(WPos center) + { + if (Points == null) + { + var angleIncrement = Angle.DoublePI / Edges; + var (sinRotation, cosRotation) = ((float, float))Math.SinCos(Rotation.Rad); + var vertices = new WDir[Edges]; + var halfWidth = HalfWidth; + var halfHeight = HalfHeight; + for (var i = 0; i < Edges; ++i) { - var (sin, cos) = ((float, float))Math.SinCos(i * angleIncrement); - vertices.Add(new(Center.X + InnerRadius * sin, Center.Z + InnerRadius * cos)); + var currentAngle = i * angleIncrement; + var (sin, cos) = ((float, float))Math.SinCos(currentAngle); + var x = halfWidth * cos; + var y = halfHeight * sin; + var rotatedX = x * cosRotation - y * sinRotation; + var rotatedY = x * sinRotation + y * cosRotation; + + vertices[i] = new(rotatedX, rotatedY); } - Points = [.. vertices]; + Points = vertices; } + var len = Points.Length; + var offset = Center - center; var result = new List(len); + for (var i = 0; i < len; ++i) - result.Add(Points[i] - center); + result.Add(Points[i] + offset); + return result; } - public override string ToString() => $"DonutV:{Center.X},{Center.Z},{InnerRadius},{OuterRadius},{Edges}"; + public override string ToString() => $"Ellipse:{Center.X},{Center.Z},{HalfWidth},{HalfHeight},{Edges},{Rotation}"; +} + +// Capsule shape defined by center, halfheight, halfwidth (radius), rotation, and number of edges. in this case the halfheight is the distance from capsule center to semicircle centers, +// the edges are per semicircle +public sealed record class Capsule(WPos Center, float HalfHeight, float HalfWidth, int Edges, Angle Rotation = default) : Shape +{ + public override List Contour(WPos center) + { + if (Points == null) + { + var vertices = new WDir[2 * Edges]; + var angleIncrement = MathF.PI / Edges; + var (sinRot, cosRot) = ((float, float))Math.SinCos(Rotation.Rad); + var halfWidth = HalfWidth; + var halfHeight = HalfHeight; + for (var i = 0; i < Edges; ++i) + { + var (sin, cos) = ((float, float))Math.SinCos(i * angleIncrement); + var halfWidthCos = halfWidth * cos; + var halfWidthSin = halfWidth * sin + halfHeight; + var rxTop = halfWidthCos * cosRot - halfWidthSin * sinRot; + var ryTop = halfWidthCos * sinRot + halfWidthSin * cosRot; + vertices[i] = new(rxTop, ryTop); + var rxBot = -rxTop; + var ryBot = -ryTop; + vertices[Edges + i] = new(rxBot, ryBot); + } + Points = vertices; + } + + var offset = Center - center; + var result = new List(Points.Length); + for (var i = 0; i < Points.Length; ++i) + result.Add(Points[i] + offset); + + return result; + } + + public override string ToString() => $"Capsule:{Center.X},{Center.Z},{HalfHeight},{HalfWidth},{Rotation},{Edges}"; } diff --git a/BossMod/Components/Cleave.cs b/BossMod/Components/Cleave.cs index ca2a0b4847..ff163dbd6c 100644 --- a/BossMod/Components/Cleave.cs +++ b/BossMod/Components/Cleave.cs @@ -2,14 +2,14 @@ // generic component for cleaving autoattacks; shows shape outline and warns when anyone other than main target is inside // enemy OID == 0 means 'primary actor' -public class Cleave(BossModule module, ActionID aid, AOEShape shape, uint enemyOID = 0, bool activeForUntargetable = false, bool originAtTarget = false, bool activeWhileCasting = true) : CastCounter(module, aid) +public class Cleave(BossModule module, ActionID aid, AOEShape shape, uint[]? enemyOID = null, bool activeForUntargetable = false, bool originAtTarget = false, bool activeWhileCasting = true) : CastCounter(module, aid) { public readonly AOEShape Shape = shape; public readonly bool ActiveForUntargetable = activeForUntargetable; public readonly bool ActiveWhileCasting = activeWhileCasting; public readonly bool OriginAtTarget = originAtTarget; public DateTime NextExpected; - public readonly List Enemies = module.Enemies(enemyOID != 0 ? enemyOID : module.PrimaryActor.OID); + public readonly List Enemies = module.Enemies(enemyOID ?? [module.PrimaryActor.OID]); public override void AddHints(int slot, Actor actor, TextHints hints) { diff --git a/BossMod/Components/Tethers.cs b/BossMod/Components/Tethers.cs index 533040657d..d1b7576ea2 100644 --- a/BossMod/Components/Tethers.cs +++ b/BossMod/Components/Tethers.cs @@ -105,20 +105,126 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) if (target == null) return null; - var (player, enemy) = source.Type is ActorType.Player or ActorType.Buddy ? (source, target) : (target, source); - if (player.Type is not ActorType.Player and not ActorType.Buddy || enemy.Type is ActorType.Player or ActorType.Buddy) + var (player, enemy) = Raid.WithoutSlot().Contains(source) ? (source, target) : (target, source); + var playerSlot = Raid.FindSlot(player.InstanceID); + return (playerSlot, player, enemy); + } +} + +// generic component for AOE at tethered targets; players are supposed to intercept tethers and gtfo from the raid +public class InterceptTetherAOE(BossModule module, ActionID aid, uint tetherID, float radius) : CastCounter(module, aid) +{ + // TODO: add forbidden players/NPCs logic + public readonly uint TID = tetherID; + public readonly float Radius = radius; + public readonly List<(Actor Player, Actor Enemy)> Tethers = []; + private BitMask _tetheredPlayers; + private BitMask _inAnyAOE; // players hit by aoe, excluding selves + public DateTime Activation; + + public bool Active => _tetheredPlayers.Any(); + + public override void Update() + { + _inAnyAOE = new(); + foreach (var slot in _tetheredPlayers.SetBits()) { - ReportError($"Unexpected tether pair: {source.InstanceID:X} -> {target.InstanceID:X}"); - return null; + var target = Raid[slot]; + if (target != null) + _inAnyAOE |= Raid.WithSlot().InRadiusExcluding(target, Radius).Mask(); } + } - var playerSlot = Raid.FindSlot(player.InstanceID); - if (playerSlot < 0) + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!Active) + return; + if (!_tetheredPlayers[slot]) { - ReportError($"Non-party-member player is tethered: {source.InstanceID:X} -> {target.InstanceID:X}"); - return null; + hints.Add("Grab the tether!"); + } + else if (Raid.WithoutSlot().InRadiusExcluding(actor, Radius).Any()) + { + hints.Add("GTFO from raid!"); + } + else + { + if (_tetheredPlayers[slot]) + { + hints.Add("Hit by baited AOE"); + } + if (_inAnyAOE[slot]) + { + hints.Add("GTFO from baited AOE!"); + } + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var count = Tethers.Count; + if (count == 0) + return; + var raid = Raid.WithoutSlot(); + for (var i = 0; i < count; ++i) + { + var tether = Tethers[i]; + if (tether.Player != actor) + hints.AddForbiddenZone(ShapeDistance.Circle(tether.Player.Position, Radius), Activation); + else + for (var j = 0; j < raid.Length; ++j) + { + ref var member = ref raid[i]; + if (member != actor) + hints.AddForbiddenZone(ShapeDistance.Circle(member.Position, Radius), Activation); + } + } + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + // show tethered targets with circles + var count = Tethers.Count; + if (count == 0) + return; + for (var i = 0; i < count; ++i) + { + var side = Tethers[i]; + Arena.AddLine(side.Enemy.Position, side.Player.Position, side.Player.OID == 0 ? Colors.Safe : 0); + Arena.AddCircle(side.Player.Position, Radius); } + } + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + var sides = DetermineTetherSides(source, tether); + if (sides != null) + { + Tethers.Add((sides.Value.Player, sides.Value.Enemy)); + _tetheredPlayers.Set(sides.Value.PlayerSlot); + } + } + + public override void OnUntethered(Actor source, ActorTetherInfo tether) + { + var sides = DetermineTetherSides(source, tether); + if (sides != null) + { + Tethers.Remove((sides.Value.Player, sides.Value.Enemy)); + _tetheredPlayers.Clear(sides.Value.PlayerSlot); + } + } + + // we support both player->enemy and enemy->player tethers + private (int PlayerSlot, Actor Player, Actor Enemy)? DetermineTetherSides(Actor source, ActorTetherInfo tether) + { + if (tether.ID != TID) + return null; + var target = WorldState.Actors.Find(tether.Target); + if (target == null) + return null; + var (player, enemy) = Raid.WithoutSlot().Contains(source) ? (source, target) : (target, source); + var playerSlot = Raid.FindSlot(player.InstanceID); return (playerSlot, player, enemy); } } diff --git a/BossMod/Config/ColorConfig.cs b/BossMod/Config/ColorConfig.cs index 9df791062a..72f4b2a827 100644 --- a/BossMod/Config/ColorConfig.cs +++ b/BossMod/Config/ColorConfig.cs @@ -44,7 +44,7 @@ public sealed class ColorConfig : ConfigNode public Color ArenaMeleeRangeIndicator = new(0xffff0000); [PropertyDisplay("Arena: other")] - public Color[] ArenaOther = [new(0xffff0080), new(0xff8080ff), new(0xff80ff80), new(0xffff8040), new(0xff40c0c0), new(0x40008080), new(0xffffff00), new(0xffff8000)]; + public Color[] ArenaOther = [new(0xffff0080), new(0xff8080ff), new(0xff80ff80), new(0xffff8040), new(0xff40c0c0), new(0x40008080), new(0xffffff00), new(0xffff8000), new(0xffffa080)]; [PropertyDisplay("Arena: interesting player, important for a mechanic")] public Color ArenaPlayerInteresting = new(0xffc0c0c0); diff --git a/BossMod/Config/ConfigConverter.cs b/BossMod/Config/ConfigConverter.cs index 9503ea4516..f38b124a0a 100644 --- a/BossMod/Config/ConfigConverter.cs +++ b/BossMod/Config/ConfigConverter.cs @@ -6,7 +6,7 @@ namespace BossMod; public static class ConfigConverter { - public static VersionedJSONSchema Schema = BuildSchema(); + public static readonly VersionedJSONSchema Schema = BuildSchema(); private static VersionedJSONSchema BuildSchema() { diff --git a/BossMod/Config/ConfigRoot.cs b/BossMod/Config/ConfigRoot.cs index cd3f25b989..3a7b1f6fd0 100644 --- a/BossMod/Config/ConfigRoot.cs +++ b/BossMod/Config/ConfigRoot.cs @@ -1,12 +1,13 @@ using System.IO; using System.Reflection; +using System.Text.Json; namespace BossMod; public class ConfigRoot { public Event Modified = new(); - + private const int _version = 10; public readonly Dictionary _nodes = []; public List Nodes => [.. _nodes.Values]; diff --git a/BossMod/Config/ModuleViewer.cs b/BossMod/Config/ModuleViewer.cs index a92791f2b4..0531c5f506 100644 --- a/BossMod/Config/ModuleViewer.cs +++ b/BossMod/Config/ModuleViewer.cs @@ -59,7 +59,6 @@ public ModuleViewer(PlanDatabase? planDB, WorldState ws) Customize(BossModuleInfo.Category.DeepDungeon, contentType.GetRow(21)); Customize(BossModuleInfo.Category.Ultimate, contentType.GetRow(28)); Customize(BossModuleInfo.Category.VariantCriterion, contentType.GetRow(30)); - Customize(BossModuleInfo.Category.Chaotic, contentType.GetRow(37)); var playStyle = Service.LuminaSheet()!; Customize(BossModuleInfo.Category.Foray, playStyle.GetRow(6)); diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index 12ec801f4d..6bfcde68b3 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -133,7 +133,8 @@ public int PendingHPDiffence get { var sum = 0; - for (var i = 0; i < PendingHPDifferences.Count; ++i) + var count = PendingHPDifferences.Count; + for (var i = 0; i < count; ++i) { sum += PendingHPDifferences[i].Value; } @@ -146,7 +147,8 @@ public int PendingMPDiffence get { var sum = 0; - for (var i = 0; i < PendingMPDifferences.Count; ++i) + var count = PendingMPDifferences.Count; + for (var i = 0; i < count; ++i) { sum += PendingMPDifferences[i].Value; } diff --git a/BossMod/Data/DeepDungeonState.cs b/BossMod/Data/DeepDungeonState.cs index 622c3ee9f2..e0979358fc 100644 --- a/BossMod/Data/DeepDungeonState.cs +++ b/BossMod/Data/DeepDungeonState.cs @@ -45,23 +45,25 @@ public readonly record struct PomanderState(byte Count, byte Flags) 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() + public List CompareToInitial() { + var ops = new List(6); if (DungeonId != DungeonType.None) { - yield return new OpProgressChange(DungeonId, Progress); - yield return new OpMapDataChange(Rooms); - yield return new OpPartyStateChange(Party); - yield return new OpPomandersChange(Pomanders); - yield return new OpChestsChange(Chests); - yield return new OpMagiciteChange(Magicite); + ops.Add(new OpProgressChange(DungeonId, Progress)); + ops.Add(new OpMapDataChange(Rooms)); + ops.Add(new OpPartyStateChange(Party)); + ops.Add(new OpPomandersChange(Pomanders)); + ops.Add(new OpChestsChange(Chests)); + ops.Add(new OpMagiciteChange(Magicite)); } + return ops; } public Event ProgressChanged = new(); public sealed record class OpProgressChange(DungeonType DungeonId, DungeonProgress Value) : WorldState.Operation { - protected override void Exec(WorldState ws) + protected override void Exec(ref WorldState ws) { ws.DeepDungeon.DungeonId = DungeonId; ws.DeepDungeon.Progress = Value; @@ -87,7 +89,7 @@ public sealed record class OpMapDataChange(RoomFlags[] Rooms) : WorldState.Opera { public readonly RoomFlags[] Rooms = Rooms; - protected override void Exec(WorldState ws) + protected override void Exec(ref WorldState ws) { Array.Copy(Rooms, ws.DeepDungeon.Rooms, NumRooms); ws.DeepDungeon.MapDataChanged.Fire(this); @@ -105,7 +107,7 @@ public sealed record class OpPartyStateChange(PartyMember[] Value) : WorldState. { public readonly PartyMember[] Value = Value; - protected override void Exec(WorldState ws) + protected override void Exec(ref WorldState ws) { Array.Copy(Value, ws.DeepDungeon.Party, NumPartyMembers); ws.DeepDungeon.PartyStateChanged.Fire(this); @@ -123,7 +125,7 @@ public sealed record class OpPomandersChange(PomanderState[] Value) : WorldState { public readonly PomanderState[] Value = Value; - protected override void Exec(WorldState ws) + protected override void Exec(ref WorldState ws) { Array.Copy(Value, ws.DeepDungeon.Pomanders, NumPomanderSlots); ws.DeepDungeon.PomandersChanged.Fire(this); @@ -141,7 +143,7 @@ public sealed record class OpChestsChange(Chest[] Value) : WorldState.Operation { public readonly Chest[] Value = Value; - protected override void Exec(WorldState ws) + protected override void Exec(ref WorldState ws) { Array.Copy(Value, ws.DeepDungeon.Chests, NumChests); ws.DeepDungeon.ChestsChanged.Fire(this); @@ -159,7 +161,7 @@ public sealed record class OpMagiciteChange(byte[] Value) : WorldState.Operation { public readonly byte[] Value = Value; - protected override void Exec(WorldState ws) + protected override void Exec(ref WorldState ws) { Array.Copy(Value, ws.DeepDungeon.Magicite, NumMagicites); ws.DeepDungeon.MagiciteChanged.Fire(this); diff --git a/BossMod/Data/WorldState.cs b/BossMod/Data/WorldState.cs index 7271ee6870..01dc1d1cfa 100644 --- a/BossMod/Data/WorldState.cs +++ b/BossMod/Data/WorldState.cs @@ -59,8 +59,8 @@ public List CompareToInitial() var party = Party.CompareToInitial(); var client = Client.CompareToInitial(); var network = Network.CompareToInitial(); - - List ops = new(RSVEntries.Count + waymarks.Count + actors.Count + party.Count + client.Count + network.Count + 2); + var deepdungeon = DeepDungeon.CompareToInitial(); + List ops = new(RSVEntries.Count + waymarks.Count + actors.Count + party.Count + client.Count + network.Count + deepdungeon.Count + 2); if (CurrentTime != default) ops.Add(new OpFrameStart(Frame, default, Client.GaugePayload, Client.CameraAzimuth)); @@ -73,9 +73,8 @@ public List CompareToInitial() ops.AddRange(party); ops.AddRange(client); ops.AddRange(network); + ops.AddRange(deepdungeon); return ops; - foreach (var o in DeepDungeon.CompareToInitial()) - yield return o; } // implementation of operations public Event FrameStarted = new(); diff --git a/BossMod/Framework/Plugin.cs b/BossMod/Framework/Plugin.cs index 00f4d5cf1c..f4b0d073dc 100644 --- a/BossMod/Framework/Plugin.cs +++ b/BossMod/Framework/Plugin.cs @@ -202,7 +202,7 @@ private static void ResetColors() for (var i = 0; i < fields.Length; ++i) { - var field = fields[i]; + ref var field = ref fields[i]; var value = field.GetValue(defaultConfig); if (value is Color or Color[]) field.SetValue(currentConfig, value); @@ -245,7 +245,7 @@ private void DrawUI() _dtr.Update(); Camera.Instance?.Update(); - _wsSync.Update(_prevUpdateTime); + _wsSync.Update(ref _prevUpdateTime); _bossmod.Update(); _zonemod.ActiveModule?.Update(); _hintsBuilder.Update(_hints, PartyState.PlayerSlot, maxCastTime); diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs index 970c34d1ba..9eea81f29a 100644 --- a/BossMod/Framework/WorldStateGameSync.cs +++ b/BossMod/Framework/WorldStateGameSync.cs @@ -143,7 +143,7 @@ public void Dispose() _interceptor.Dispose(); } - public unsafe void Update(TimeSpan prevFramePerf) + public unsafe void Update(ref TimeSpan prevFramePerf) { var fwk = Framework.Instance(); _ws.Execute(new WorldState.OpFrameStart @@ -202,9 +202,10 @@ private unsafe void UpdateWaymarks() private unsafe void UpdateActors() { var mgr = GameObjectManager.Instance(); - for (var i = 0; i < _actorsByIndex.Length; ++i) + var len = _actorsByIndex.Length; + for (var i = 0; i < len; ++i) { - var actor = _actorsByIndex[i]; + ref var actor = ref _actorsByIndex[i]; var obj = mgr->Objects.IndexSorted[i].Value; if (obj != null && obj->EntityId == InvalidEntityId) @@ -218,8 +219,7 @@ private unsafe void UpdateActors() if (actor != null && (obj == null || actor.InstanceID != obj->EntityId)) { - _actorsByIndex[i] = null; - RemoveActor(actor); + RemoveActor(ref actor); actor = null; } @@ -229,7 +229,7 @@ private unsafe void UpdateActors() { Service.Log($"[WorldState] Actor position mismatch for #{i} {actor}"); } - UpdateActor(obj, i, actor); + UpdateActor(ref obj, i, ref actor); } } @@ -238,13 +238,14 @@ private unsafe void UpdateActors() _actorOps.Clear(); } - private void RemoveActor(Actor actor) + private void RemoveActor(ref Actor actor) { - DispatchActorEvents(actor.InstanceID); - _ws.Execute(new ActorState.OpDestroy(actor.InstanceID)); + var id = actor.InstanceID; + DispatchActorEvents(id); + _ws.Execute(new ActorState.OpDestroy(id)); } - private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) + private unsafe void UpdateActor(ref GameObject* obj, int index, ref Actor? act) { var chr = obj->IsCharacter() ? (Character*)obj : null; var name = obj->NameString; @@ -332,7 +333,7 @@ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) TotalTime = castInfo->BaseCastTime, Interruptible = castInfo->Interruptible != 0, } : null; - UpdateActorCastInfo(act, curCast); + UpdateActorCastInfo(ref act, ref curCast); } var sm = chr != null ? chr->GetStatusManager() : null; @@ -352,21 +353,22 @@ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) curStatus.Extra = s.Param; curStatus.ExpireAt = _ws.CurrentTime.AddSeconds(dur); } - UpdateActorStatus(act, i, curStatus); + UpdateActorStatus(ref act, i, ref curStatus); } } var aeh = chr != null ? chr->GetActionEffectHandler() : null; if (aeh != null) { - for (int i = 0; i < aeh->IncomingEffects.Length; ++i) + var len = aeh->IncomingEffects.Length; + for (var i = 0; i < len; ++i) { ref var eff = ref aeh->IncomingEffects[i]; ref var prev = ref act.IncomingEffects[i]; if ((prev.GlobalSequence, prev.TargetIndex) != (eff.GlobalSequence != 0 ? (eff.GlobalSequence, eff.TargetIndex) : (0, 0))) { var effects = new ActionEffects(); - for (int j = 0; j < ActionEffects.MaxCount; ++j) + for (var j = 0; j < ActionEffects.MaxCount; ++j) effects[j] = *(ulong*)eff.Effects.Effects.GetPointer(j); _ws.Execute(new ActorState.OpIncomingEffect(act.InstanceID, i, new(eff.GlobalSequence, eff.TargetIndex, eff.Source, new((ActionType)eff.ActionType, eff.ActionId), effects))); } @@ -374,16 +376,17 @@ private unsafe void UpdateActor(GameObject* obj, int index, Actor? act) } } - private void UpdateActorCastInfo(Actor act, ActorCastInfo? cast) + private void UpdateActorCastInfo(ref Actor act, ref ActorCastInfo? cast) { - if (cast == null && act.CastInfo == null) + ref var castInfo = ref act.CastInfo; + if (cast == null && castInfo == null) return; // was not casting and is not casting - if (cast != null && act.CastInfo != null && cast.Action == act.CastInfo.Action && cast.TargetID == act.CastInfo.TargetID && cast.TotalTime == act.CastInfo.TotalTime && Math.Abs(cast.ElapsedTime - act.CastInfo.ElapsedTime) < 0.2) + if (cast != null && castInfo != null && cast.Action == castInfo.Action && cast.TargetID == castInfo.TargetID && cast.TotalTime == castInfo.TotalTime && Math.Abs(cast.ElapsedTime - castInfo.ElapsedTime) < 0.2) { // continuing casting same spell // TODO: consider *not* ignoring elapsed differences, these probably mean we're doing something wrong... - act.CastInfo.ElapsedTime = cast.ElapsedTime; + castInfo.ElapsedTime = cast.ElapsedTime; return; } @@ -391,7 +394,7 @@ private void UpdateActorCastInfo(Actor act, ActorCastInfo? cast) _ws.Execute(new ActorState.OpCastInfo(act.InstanceID, cast)); } - private void UpdateActorStatus(Actor act, int index, ActorStatus value) + private void UpdateActorStatus(ref Actor act, int index, ref ActorStatus value) { // note: some statuses have non-zero remaining time but never tick down (e.g. FC buffs); currently we ignore that fact, to avoid log spam... // note: RemainingTime is not monotonously decreasing (I assume because it is really calculated by game and frametime fluctuates...), we ignore 'slight' duration increases (<1 sec) @@ -509,9 +512,10 @@ private unsafe void UpdatePartyNormal(GroupManager.Group* group, PartyMember* pl } // consider buddies as party members too var ui = UIState.Instance(); - for (var i = 0; i < ui->Buddy.DutyHelperInfo.ENpcIds.Length; ++i) + var len = ui->Buddy.DutyHelperInfo.ENpcIds.Length; + for (var i = 0; i < len; ++i) { - var instanceID = ui->Buddy.DutyHelperInfo.DutyHelpers[i].EntityId; + ref var instanceID = ref ui->Buddy.DutyHelperInfo.DutyHelpers[i].EntityId; if (instanceID != InvalidEntityId && _ws.Party.FindSlot(instanceID) < 0) { var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(instanceID); @@ -566,7 +570,8 @@ private unsafe void UpdatePartyNPCs() private unsafe bool HasBuddy(ulong instanceID) { var ui = UIState.Instance(); - for (var i = 0; i < ui->Buddy.DutyHelperInfo.ENpcIds.Length; ++i) + var len = ui->Buddy.DutyHelperInfo.ENpcIds.Length; + for (var i = 0; i < len; ++i) if (ui->Buddy.DutyHelperInfo.DutyHelpers[i].EntityId == instanceID) return true; return false; @@ -725,9 +730,12 @@ private void DispatchActorEvents(ulong instanceID) var ops = _actorOps.GetValueOrDefault(instanceID); if (ops == null) return; - - foreach (var op in ops) + var count = ops.Count; + for (var i = 0; i < count; ++i) + { + var op = ops[i]; _ws.Execute(op); + } _actorOps.Remove(instanceID); } @@ -737,7 +745,7 @@ private void DispatchActorEvents(ulong instanceID) var res = new List<(int, Cooldown)>(max); for (int i = 0, cnt = max; i < cnt; ++i) { - var value = values[i]; + ref var value = ref values[i]; if (value != reference[i]) res.Add((i, value)); } @@ -750,7 +758,7 @@ private void DispatchActorEvents(ulong instanceID) var res = new List<(BozjaHolsterID, byte)>(len); for (var i = 0; i < len; ++i) { - var content = contents[i]; + ref var content = ref contents[i]; if (content != 0) res.Add(((BozjaHolsterID)i, content)); } diff --git a/BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/MindOverManor.cs b/BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/MindOverManor.cs similarity index 98% rename from BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/MindOverManor.cs rename to BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/MindOverManor.cs index 9b39e3ff5e..73f9189c4f 100644 --- a/BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/MindOverManor.cs +++ b/BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/MindOverManor.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.JobQuests.Pictomancer.MindOverManor; +namespace BossMod.Dawntrail.Quest.Job.Pictomancer.MindOverManor; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/FlightOfTheGriffin.cs b/BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/FlightOfTheGriffin.cs similarity index 98% rename from BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/FlightOfTheGriffin.cs rename to BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/FlightOfTheGriffin.cs index ea79876c31..caf2978263 100644 --- a/BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/FlightOfTheGriffin.cs +++ b/BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/FlightOfTheGriffin.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.JobQuests.Pictomancer.SomewhereOnlySheKnows.FlightOfTheGriffin; +namespace BossMod.Dawntrail.Quest.Job.Pictomancer.SomewhereOnlySheKnows.FlightOfTheGriffin; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/JanquetilaquesPortrait.cs b/BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/JanquetilaquesPortrait.cs similarity index 98% rename from BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/JanquetilaquesPortrait.cs rename to BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/JanquetilaquesPortrait.cs index f8e32487f5..c856c3202c 100644 --- a/BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/JanquetilaquesPortrait.cs +++ b/BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/JanquetilaquesPortrait.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.JobQuests.Pictomancer.SomewhereOnlySheKnows.JanquetilaquesPortrait; +namespace BossMod.Dawntrail.Quest.Job.Pictomancer.SomewhereOnlySheKnows.JanquetilaquesPortrait; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/TheWingedSteed.cs b/BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/TheWingedSteed.cs similarity index 97% rename from BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/TheWingedSteed.cs rename to BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/TheWingedSteed.cs index 2410fa024d..3f865585ba 100644 --- a/BossMod/Modules/Dawntrail/Quest/JobQuests/Pictomancer/SomewhereOnlySheKnows/TheWingedSteed.cs +++ b/BossMod/Modules/Dawntrail/Quest/Job/Pictomancer/SomewhereOnlySheKnows/TheWingedSteed.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.JobQuests.Pictomancer.SomewhereOnlySheKnows.TheWingedSteed; +namespace BossMod.Dawntrail.Quest.Job.Pictomancer.SomewhereOnlySheKnows.TheWingedSteed; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/JobQuests/Viper/FangsOfTheViper.cs b/BossMod/Modules/Dawntrail/Quest/Job/Viper/FangsOfTheViper.cs similarity index 98% rename from BossMod/Modules/Dawntrail/Quest/JobQuests/Viper/FangsOfTheViper.cs rename to BossMod/Modules/Dawntrail/Quest/Job/Viper/FangsOfTheViper.cs index 2f3b3e2bef..9d77380d7f 100644 --- a/BossMod/Modules/Dawntrail/Quest/JobQuests/Viper/FangsOfTheViper.cs +++ b/BossMod/Modules/Dawntrail/Quest/Job/Viper/FangsOfTheViper.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.JobQuests.Viper.FangsOfTheViper; +namespace BossMod.Dawntrail.Quest.Job.Viper.FangsOfTheViper; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/JobQuests/Viper/VengeanceOfTheViper.cs b/BossMod/Modules/Dawntrail/Quest/Job/Viper/VengeanceOfTheViper.cs similarity index 99% rename from BossMod/Modules/Dawntrail/Quest/JobQuests/Viper/VengeanceOfTheViper.cs rename to BossMod/Modules/Dawntrail/Quest/Job/Viper/VengeanceOfTheViper.cs index 99a2e039ac..01890d1951 100644 --- a/BossMod/Modules/Dawntrail/Quest/JobQuests/Viper/VengeanceOfTheViper.cs +++ b/BossMod/Modules/Dawntrail/Quest/Job/Viper/VengeanceOfTheViper.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.JobQuests.Viper.VengeanceOfTheViper; +namespace BossMod.Dawntrail.Quest.Job.Viper.VengeanceOfTheViper; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/RoleQuests/AHunterTrue.cs b/BossMod/Modules/Dawntrail/Quest/Role/AHunterTrue.cs similarity index 99% rename from BossMod/Modules/Dawntrail/Quest/RoleQuests/AHunterTrue.cs rename to BossMod/Modules/Dawntrail/Quest/Role/AHunterTrue.cs index fa143323d5..9a78325314 100644 --- a/BossMod/Modules/Dawntrail/Quest/RoleQuests/AHunterTrue.cs +++ b/BossMod/Modules/Dawntrail/Quest/Role/AHunterTrue.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.RoleQuests.AHunterTrue; +namespace BossMod.Dawntrail.Quest.Role.AHunterTrue; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/RoleQuests/AnAntidoteForAnarchy.cs b/BossMod/Modules/Dawntrail/Quest/Role/AnAntidoteForAnarchy.cs similarity index 99% rename from BossMod/Modules/Dawntrail/Quest/RoleQuests/AnAntidoteForAnarchy.cs rename to BossMod/Modules/Dawntrail/Quest/Role/AnAntidoteForAnarchy.cs index 0945693de7..c3fea46824 100644 --- a/BossMod/Modules/Dawntrail/Quest/RoleQuests/AnAntidoteForAnarchy.cs +++ b/BossMod/Modules/Dawntrail/Quest/Role/AnAntidoteForAnarchy.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.RoleQuests.AnAntidoteForAnarchy; +namespace BossMod.Dawntrail.Quest.Role.AnAntidoteForAnarchy; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/RoleQuests/DreamsOfANewDay.cs b/BossMod/Modules/Dawntrail/Quest/Role/DreamsOfANewDay.cs similarity index 99% rename from BossMod/Modules/Dawntrail/Quest/RoleQuests/DreamsOfANewDay.cs rename to BossMod/Modules/Dawntrail/Quest/Role/DreamsOfANewDay.cs index 07be8f87b2..793dd5c1b0 100644 --- a/BossMod/Modules/Dawntrail/Quest/RoleQuests/DreamsOfANewDay.cs +++ b/BossMod/Modules/Dawntrail/Quest/Role/DreamsOfANewDay.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.RoleQuests.DreamsOfANewDay; +namespace BossMod.Dawntrail.Quest.Role.DreamsOfANewDay; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Quest/RoleQuests/HeroesAndPretenders.cs b/BossMod/Modules/Dawntrail/Quest/Role/HeroesAndPretenders.cs similarity index 97% rename from BossMod/Modules/Dawntrail/Quest/RoleQuests/HeroesAndPretenders.cs rename to BossMod/Modules/Dawntrail/Quest/Role/HeroesAndPretenders.cs index a43667b886..6b234883bf 100644 --- a/BossMod/Modules/Dawntrail/Quest/RoleQuests/HeroesAndPretenders.cs +++ b/BossMod/Modules/Dawntrail/Quest/Role/HeroesAndPretenders.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.RoleQuests.HeroesAndPretenders; +namespace BossMod.Dawntrail.Quest.Role.HeroesAndPretenders; public enum OID : uint { @@ -63,6 +63,8 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) if ((AID)spell.Action.ID is AID.ForeseenFlurryFirst or AID.ForeseenFlurryRest) { var index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1)); + if (index < 0) + return; AdvanceLine(Lines[index], caster.Position); if (Lines[index].ExplosionsLeft == 0) Lines.RemoveAt(index); diff --git a/BossMod/Modules/Dawntrail/Quest/RoleQuests/TheMightiestShield.cs b/BossMod/Modules/Dawntrail/Quest/Role/TheMightiestShield.cs similarity index 99% rename from BossMod/Modules/Dawntrail/Quest/RoleQuests/TheMightiestShield.cs rename to BossMod/Modules/Dawntrail/Quest/Role/TheMightiestShield.cs index 14f2ab7d6e..2474cd15a8 100644 --- a/BossMod/Modules/Dawntrail/Quest/RoleQuests/TheMightiestShield.cs +++ b/BossMod/Modules/Dawntrail/Quest/Role/TheMightiestShield.cs @@ -1,4 +1,4 @@ -namespace BossMod.Dawntrail.Quest.RoleQuests.TheMightiestShield; +namespace BossMod.Dawntrail.Quest.Role.TheMightiestShield; public enum OID : uint { diff --git a/BossMod/Modules/Dawntrail/Raid/M01NBIackCat/PredaceousPounce.cs b/BossMod/Modules/Dawntrail/Raid/M01NBIackCat/PredaceousPounce.cs index 51fc7e091e..41c5a1ffb1 100644 --- a/BossMod/Modules/Dawntrail/Raid/M01NBIackCat/PredaceousPounce.cs +++ b/BossMod/Modules/Dawntrail/Raid/M01NBIackCat/PredaceousPounce.cs @@ -18,14 +18,14 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) var count = _aoes.Count; if (count == 0) return []; - List aoes = new(count); + var aoes = new AOEInstance[count]; for (var i = 0; i < count; ++i) { var aoe = _aoes[i]; if (i < 2) - aoes.Add(count > 2 ? aoe with { Color = Colors.Danger } : aoe); + aoes[i] = count > 2 ? aoe with { Color = Colors.Danger } : aoe; else - aoes.Add(aoe); + aoes[i] = aoe; } return aoes; } diff --git a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1Byakko.cs b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1Byakko.cs index a3ede32c94..7e9af9c8c8 100644 --- a/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1Byakko.cs +++ b/BossMod/Modules/Dawntrail/Unreal/Un1Byakko/Un1Byakko.cs @@ -4,7 +4,7 @@ class StormPulseRepeat(BossModule module) : Components.CastCounter(module, Actio class HeavenlyStrike(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.HeavenlyStrike), new AOEShapeCircle(3), true); class FireAndLightningBoss(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FireAndLightningBoss), new AOEShapeRect(54.3f, 10)); class FireAndLightningAdd(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FireAndLightningAdd), new AOEShapeRect(54.75f, 10)); -class SteelClaw(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.SteelClaw), new AOEShapeCone(17.75f, 30.Degrees()), (uint)OID.Hakutei); // TODO: verify angle +class SteelClaw(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.SteelClaw), new AOEShapeCone(17.75f, 60.Degrees()), [(uint)OID.Hakutei]); class WhiteHerald(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.WhiteHerald, ActionID.MakeSpell(AID.WhiteHerald), 15, 5.1f); // TODO: verify falloff class DistantClap(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DistantClap), new AOEShapeDonut(4, 25)); class SweepTheLegBoss(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SweepTheLegBoss), new AOEShapeCone(28.3f, 135.Degrees())); diff --git a/BossMod/Modules/Endwalker/Alliance/A13Azeyma/WildfireWard.cs b/BossMod/Modules/Endwalker/Alliance/A13Azeyma/WildfireWard.cs index cfef957f32..39a61cc311 100644 --- a/BossMod/Modules/Endwalker/Alliance/A13Azeyma/WildfireWard.cs +++ b/BossMod/Modules/Endwalker/Alliance/A13Azeyma/WildfireWard.cs @@ -3,10 +3,9 @@ class WildfireWard(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.IlluminatingGlimpse), 15, false, 1, kind: Kind.DirLeft); class ArenaBounds(BossModule module) : Components.GenericAOEs(module) { - private static readonly WPos triangleCenter = new(-750, -753.325f); - private static readonly TriangleE triangle = new(triangleCenter, 24); - private static readonly AOEShapeCustom triangleCutOut = new([new Circle(A13Azeyma.NormalCenter, 29.5f)], [triangle]); - private static readonly ArenaBoundsComplex triangleBounds = new([triangle]); + private static readonly Polygon[] triangle = [new(A13Azeyma.NormalCenter, 13.279f, 3, 180.Degrees())]; + private static readonly AOEShapeCustom triangleCutOut = new([new Square(A13Azeyma.NormalCenter, 29.5f)], triangle); + private static readonly ArenaBoundsComplex triangleBounds = new(triangle); private AOEInstance? _aoe; @@ -22,10 +21,13 @@ public override void OnEventEnvControl(byte index, uint state) { _aoe = null; Arena.Bounds = triangleBounds; - Arena.Center = triangleCenter; + Arena.Center = triangleBounds.Center; } else if (state == 0x00080004) + { Arena.Bounds = A13Azeyma.NormalBounds; + Arena.Center = A13Azeyma.NormalCenter; + } } } } diff --git a/BossMod/Modules/Endwalker/Alliance/A31Thaliak/Tetraktys.cs b/BossMod/Modules/Endwalker/Alliance/A31Thaliak/Tetraktys.cs index 38de1bac0a..313cac2ea3 100644 --- a/BossMod/Modules/Endwalker/Alliance/A31Thaliak/Tetraktys.cs +++ b/BossMod/Modules/Endwalker/Alliance/A31Thaliak/Tetraktys.cs @@ -4,10 +4,9 @@ class TetraktysBorder(BossModule module) : Components.GenericAOEs(module) { public static readonly WPos NormalCenter = new(-945, 945); public static readonly ArenaBoundsSquare NormalBounds = new(24); - private static readonly WPos TriangleCenter = new(-945, 941.5f); - private static readonly TriangleE triangle = new(TriangleCenter, 48); - private static readonly ArenaBoundsComplex TriangleBounds = new([triangle]); - private static readonly AOEShapeCustom transition = new([new Square(NormalCenter, 24)], [triangle]); + private static readonly Polygon[] triangle = [new(new(-945, 948.71267f), 27.71281f, 3, 180.Degrees())]; + private static readonly ArenaBoundsComplex TriangleBounds = new(triangle); + private static readonly AOEShapeCustom transition = new([new Square(NormalCenter, 24)], triangle); private AOEInstance? _aoe; public bool Active; @@ -25,7 +24,7 @@ public override void OnEventEnvControl(byte index, uint state) case 0x00020001: _aoe = null; Arena.Bounds = TriangleBounds; - Arena.Center = TriangleCenter; + Arena.Center = TriangleBounds.Center; Active = true; break; case 0x00080004: diff --git a/BossMod/Modules/Endwalker/Quest/Job/Reaper/TheKillingArt.cs b/BossMod/Modules/Endwalker/Quest/Job/Reaper/TheKillingArt.cs new file mode 100644 index 0000000000..d07ca0bf89 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/Job/Reaper/TheKillingArt.cs @@ -0,0 +1,109 @@ +namespace BossMod.Endwalker.Quest.Job.Reaper.TheKillingArt; + +public enum OID : uint +{ + Boss = 0x3664, // R1.5 + VoidHecteyes = 0x3666, // R1.2 + VoidPersona = 0x3667, // R1.2 + Voidzone = 0x1E963D, + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack1 = 870, // Boss/VoidPersona->player/Drusilla, no cast, single-target + AutoAttack2 = 872, // VoidHecteyes->player/Drusilla, no cast, single-target + + VoidCall = 27589, // Boss->self, 4.0s cast, single-target + MeatySliceVisual = 27590, // Boss->self, 3.4+0.6s cast, single-target + MeatySlice = 27591, // Helper->self, 4.0s cast, range 50 width 12 rect + CleaverVisual = 27594, // Boss->self, 3.5+0.5s cast, single-target + Cleaver = 27595, // Helper->self, 4.0s cast, range 40 120-degree cone + FlankCleaverVisual = 27596, // Boss->self, 3.5+0.5s cast, single-target + FlankCleaver = 27597, // Helper->self, 4.0s cast, range 40 120-degree cone + Explosion1 = 27606, // VoidHecteyes->self, 20.0s cast, range 60 circle + Explosion2 = 27607, // VoidPersona->self, 20.0s cast, range 50 circle + FocusInferiVisual = 27592, // Boss->self, 2.9+0.6s cast, single-target + FocusInferi = 27593, // Helper->location, 3.5s cast, range 6 circle + CarnemLevareVisual = 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 12-17 180-degree donut sector + CarnemLevare3 = 27600, // Helper->self, 3.5s cast, range 2-7 180-degree donut sector + CarnemLevare4 = 27603, // Helper->self, 3.5s cast, range 17-22 180-degree donut sector + CarnemLevare5 = 27601, // Helper->self, 3.5s cast, range 7-12 180-degree donut sector + 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.SimpleAOEs(module, ActionID.MakeSpell(AID.VoidMortar1), 13); +class FocusInferi(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 6, ActionID.MakeSpell(AID.FocusInferi), m => m.Enemies(OID.Voidzone).Where(x => x.EventState != 7), 0); +class CarnemLevareCross(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.CarnemLevare1), new AOEShapeCross(40, 4)); + +class CarnemLevareDonut(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = new(4); + private static readonly Angle a90 = 90.Degrees(); + private static readonly AOEShapeDonutSector[] sectors = [new(12, 17, a90), new(2, 7, a90), new(17, 22, a90), new(7, 12, a90)]; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var count = _aoes.Count; + if (count == 0) + return []; + var max = count > 4 ? 4 : count; + var aoes = new AOEInstance[max]; + for (var i = 0; i < max; ++i) + aoes[i] = _aoes[i]; + return aoes; + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + var shape = (AID)spell.Action.ID switch + { + AID.CarnemLevare2 => sectors[0], + AID.CarnemLevare3 => sectors[1], + AID.CarnemLevare4 => sectors[2], + AID.CarnemLevare5 => sectors[3], + _ => null + }; + + if (shape != null) + _aoes.Add(new(shape, spell.LocXZ, spell.Rotation, Module.CastFinishAt(spell))); + } + + 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) + _aoes.RemoveAt(0); + } +} +class MeatySlice(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MeatySlice), new AOEShapeRect(50, 6)); + +abstract class Cleave(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCone(40, 60.Degrees())); +class Cleaver(BossModule module) : Cleave(module, AID.Cleaver); +class FlankCleaver(BossModule module) : Cleave(module, AID.FlankCleaver); + +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, arena.Center, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(-69.569f, -388), 19.5f, 64)], [new Rectangle(new(-69, -368), 20, 0.94f)]); +} diff --git a/BossMod/Modules/Endwalker/Quest/Job/Sage/LifeEphemeralPathEternal/AncelAndMahaud.cs b/BossMod/Modules/Endwalker/Quest/Job/Sage/LifeEphemeralPathEternal/AncelAndMahaud.cs new file mode 100644 index 0000000000..fb02e799c1 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/Job/Sage/LifeEphemeralPathEternal/AncelAndMahaud.cs @@ -0,0 +1,95 @@ +namespace BossMod.Endwalker.Quest.Job.Sage.LifeEphemeralPathEternal.AncelAndMahaud; + +public enum OID : uint +{ + Boss = 0x35C5, // R0.5 + MahaudFlamehand = 0x35C4, // R0.5 + ChiBomb = 0x35C7, // R1.0 + Helper = 0x233C, // R0.5 +} + +public enum AID : uint +{ + AutoAttack = 872, // Boss->Lalah, no cast, single-target + Teleport1 = 26868, // Boss->location, no cast, single-target + Teleport2 = 26869, // MahaudFlamehand->location, no cast, single-target + + ChiBlastVisual = 26838, // Boss->self, 5.0s cast, single-target + ChiBlast = 26839, // Helper->self, 5.0s cast, range 100 circle + ChiBomb = 26835, // Boss->self, 5.0s cast, single-target + + Explosion = 26837, // ChiBomb->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 + 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 +} + +class DemifireII(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.DemifireII)); +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(spell.LocXZ, Module.CastFinishAt(spell)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + var order = (AID)spell.Action.ID switch + { + AID.RawRockbreaker1 => 0, + AID.RawRockbreaker2 => 1, + _ => -1 + }; + AdvanceSequence(order, spell.LocXZ, WorldState.FutureTime(2)); + } +} + +class ChiBlast(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ChiBlast)); +class Explosion(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Explosion), 6); +class ArmOfTheScholar(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ArmOfTheScholar), 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.SimpleAOEs(module, ActionID.MakeSpell(AID.ClassicalBlizzard), 6); +class ClassicalStone(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ClassicalStone), 15); + +class AncelAndMahaudStates : StateMachineBuilder +{ + public AncelAndMahaudStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69608, NameID = 10732)] +public class AncelAndMahaud(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaBounds.Center, ArenaBounds) +{ + public static readonly ArenaBoundsComplex ArenaBounds = new([new Polygon(new(224.8f, -855.8f), 19.5f, 20)]); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(Enemies([(uint)OID.Boss, (uint)OID.MahaudFlamehand])); + } +} diff --git a/BossMod/Modules/Endwalker/Quest/Job/Sage/LifeEphemeralPathEternal/Guildivain.cs b/BossMod/Modules/Endwalker/Quest/Job/Sage/LifeEphemeralPathEternal/Guildivain.cs new file mode 100644 index 0000000000..dfc4f71164 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/Job/Sage/LifeEphemeralPathEternal/Guildivain.cs @@ -0,0 +1,125 @@ +namespace BossMod.Endwalker.Quest.Job.Sage.LifeEphemeralPathEternal.Guildivain; + +public enum OID : uint +{ + Boss = 0x35C6, // R4.998, x1 + StrengthenedNoulith = 0x35C8, // R1.0 + EnhancedNoulith = 0x3859, // R1.0 + Helper = 0x233C +} + +public enum AID : uint +{ + Nouliths = 26851, // BossP2->self, 5.0s cast, single-target + AetherstreamTank = 26852, // StrengthenedNoulith->Lalah, no cast, range 50 width 4 rect + AetherstreamPlayer = 26853, // StrengthenedNoulith->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 + LeftRightScalpel1 = 26864, // BossP2->self, 7.0s cast, range 15 210-degree cone + LeftRightScalpel2 = 26865, // BossP2->self, 3.0s cast, range 15 210-degree cone + RightLeftScalpel1 = 26862, // BossP2->self, 7.0s cast, range 15 210-degree cone + RightLeftScalpel2 = 26863, // BossP2->self, 3.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 + CryonicsVisual = 26859, // BossP2->self, 8.0s cast, single-target + Cryonics = 26860, // Helper->players, 8.0s cast, range 6 circle + + Craniotomy = 28386, // BossP2->self, 8.0s cast, range 40 circle + + FrigotherapyVisual = 26866, // BossP2->self, 5.0s cast, single-target + Frigotherapy = 26867 // Helper->players/Mahaud/Loifa, 7.0s cast, range 5 circle +} + +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 +} + +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.SimpleAOEs +{ + 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 = AncelAndMahaud.AncelAndMahaud.ArenaBounds; + }); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (spell.Action == WatchedAction) + Arena.Bounds = Guildivain.SmallBounds; + } +} + +abstract class Scalpel(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCone(15, 105.Degrees())); +class RightScalpel(BossModule module) : Scalpel(module, AID.RightScalpel); +class LeftScalpel(BossModule module) : Scalpel(module, AID.LeftScalpel); +class RightLeftScalpel1(BossModule module) : Scalpel(module, AID.RightLeftScalpel1); +class RightLeftScalpel2(BossModule module) : Scalpel(module, AID.RightLeftScalpel2); +class LeftRightScalpel1(BossModule module) : Scalpel(module, AID.LeftRightScalpel1); +class LeftRightScalpel2(BossModule module) : Scalpel(module, AID.LeftRightScalpel2); + +class Laparotomy(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Laparotomy), new AOEShapeCone(15, 60.Degrees())); +class Amputation(BossModule module) : Components.SimpleAOEs(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 Frigotherapy(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Frigotherapy), 5); + +class GuildivainStates : StateMachineBuilder +{ + public GuildivainStates(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 = 69608, NameID = 10733)] +public class Guildivain(WorldState ws, Actor primary) : BossModule(ws, primary, SmallBounds.Center, AncelAndMahaud.AncelAndMahaud.ArenaBounds) +{ + public static readonly ArenaBoundsComplex SmallBounds = new([new Polygon(new(224.8f, -855.8f), 10, 20)]); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(Enemies(OID.EnhancedNoulith)); + Arena.Actor(PrimaryActor); + } +} diff --git a/BossMod/Modules/Endwalker/Quest/Job/Sage/SagesFocus.cs b/BossMod/Modules/Endwalker/Quest/Job/Sage/SagesFocus.cs new file mode 100644 index 0000000000..438ebdde0c --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/Job/Sage/SagesFocus.cs @@ -0,0 +1,83 @@ +namespace BossMod.Endwalker.Quest.Job.Sage.SagesFocus; + +public enum OID : uint +{ + Boss = 0x3587, // R0.5 + Loifa = 0x3588, // R0.5 + Mahaud = 0x3586, // R0.5 + StrengthenedNoulith = 0x358E, // R1.0 + ChiBomb = 0x358D, // R1.0 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack1 = 872, // Boss/Loifa->Lalah, no cast, single-target + Teleport1 = 26544, // Boss->location, no cast, single-target + Teleport2 = 26556, // Loifa->location, no cast, single-target + Teleport3 = 26557, // Mahaud->location, no cast, single-target + Teleport4 = 26540, // StrengthenedNoulith->location, no cast, single-target + + Demifire = 26558, // Mahaud->Lalah, no cast, single-target + TripleThreat = 26535, // Boss->Lalah, 8.0s cast, single-target + ChiBomb = 26536, // Boss->self, 5.0s cast, single-target + Explosion = 26537, // ChiBomb->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, // Mahaud->self, 5.0s cast, single-target + DemiblizzardIII1 = 26546, // Helper->self, 5.0s cast, range 10-40 donut + Demigravity1 = 26539, // Mahaud->location, 5.0s cast, range 6 circle + Demigravity2 = 26550, // Helper->location, 5.0s cast, range 6 circle + DemifireIII = 26547, // Mahaud->self, 5.0s cast, single-target + DemifireIII1 = 26548, // Helper->self, 5.6s cast, range 40 circle + DemifireIIVisual = 26552, // Mahaud->self, 7.0s cast, single-target + DemifireIISpread = 26553, // Helper->player/Lalah, 5.0s cast, range 5 circle + DemifireIIAOE = 26554, // Helper->location, 5.0s cast, range 14 circle + Diagnosis = 26555, // Loifa->Mahaud, 3.0s cast, single-target + DosisIII = 26551, // Loifa->Lalah, 8.0s cast, single-target +} + +class DosisIII(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.DosisIII)); +class DemifireSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.DemifireIISpread), 5); +class DemifireIIAOE(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DemifireIIAOE), 14); +class DemifireIII(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DemifireIII1)); +class Noubelea(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Noubelea1), new AOEShapeRect(50, 2)); +class Demigravity1(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Demigravity1), 6); +class Demigravity2(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Demigravity2), 6); +class Demiblizzard(BossModule module) : Components.SimpleAOEs(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.SimpleAOEs(module, ActionID.MakeSpell(AID.Explosion), 6); +class ArmOfTheScholar(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ArmOfTheScholar), 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, arena.Center, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(0, -82.146f), 18.5f, 20)]); + private static readonly uint[] bosses = [(uint)OID.Boss, (uint)OID.Mahaud, (uint)OID.Loifa]; + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(Enemies(bosses)); + } +} + diff --git a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs deleted file mode 100644 index 51163d5057..0000000000 --- a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index d9879ffa03..0000000000 --- a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 5e12572506..0000000000 --- a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs +++ /dev/null @@ -1,91 +0,0 @@ -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/MSQ/AsTheHeavensBurn/P1TerminusIdolizer.cs b/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P1TerminusIdolizer.cs index 448b391208..87b0106b20 100644 --- a/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P1TerminusIdolizer.cs +++ b/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P1TerminusIdolizer.cs @@ -11,8 +11,8 @@ public enum OID : uint public enum AID : uint { - AutoAttack = 26994, // Boss->Estinien, no cast, single-target + DeadlyTentacles = 26998, // Boss->Estinien, no cast, single-target TentacleWhip = 27005, // Boss->self, no cast, single-target Shout = 27000, // Boss->self, no cast, single-target diff --git a/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P2TerminusLacerator.cs b/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P2TerminusLacerator.cs index 20122d43e1..807163d29d 100644 --- a/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P2TerminusLacerator.cs +++ b/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P2TerminusLacerator.cs @@ -30,7 +30,7 @@ public enum AID : uint Explosion = 27026 // Meteorite->self, 3.0s cast, range 6 circle } -class TheBlackDeath(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.TheBlackDeath), new AOEShapeCone(25, 60.Degrees()), (uint)OID.Boss, activeWhileCasting: false); +class TheBlackDeath(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.TheBlackDeath), new AOEShapeCone(25, 60.Degrees()), [(uint)OID.Boss], activeWhileCasting: false); class Burst(BossModule module) : Components.CastTowers(module, ActionID.MakeSpell(AID.Burst), 5); class DeadlyImpact(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DeadlyImpact), 10, 6); class BlackStar(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.BlackStar)); diff --git a/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P3TerminusVanquisher.cs b/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P3TerminusVanquisher.cs index 1bf537d1c5..2137360540 100644 --- a/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P3TerminusVanquisher.cs +++ b/BossMod/Modules/Endwalker/Quest/MSQ/AsTheHeavensBurn/P3TerminusVanquisher.cs @@ -40,8 +40,8 @@ public enum AID : uint ForceOfLoathing = 27031 // TerminusVanquisher->self, no cast, range 10 120-degree cone } -class TheBlackDeath(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.TheBlackDeath), new AOEShapeCone(25, 60.Degrees()), (uint)OID.Boss, activeWhileCasting: false); -class ForceOfLoathing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.ForceOfLoathing), new AOEShapeCone(10, 60.Degrees()), (uint)OID.TerminusVanquisher, activeWhileCasting: false); +class TheBlackDeath(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.TheBlackDeath), new AOEShapeCone(25, 60.Degrees()), [(uint)OID.Boss], activeWhileCasting: false); +class ForceOfLoathing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.ForceOfLoathing), new AOEShapeCone(10, 60.Degrees()), [(uint)OID.TerminusVanquisher], activeWhileCasting: false); class DeadlyImpact(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DeadlyImpact), 10, 6); class BlackStar(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.BlackStar)); diff --git a/BossMod/Modules/Endwalker/Quest/SagesFocus.cs b/BossMod/Modules/Endwalker/Quest/SagesFocus.cs deleted file mode 100644 index 72a5e90d80..0000000000 --- a/BossMod/Modules/Endwalker/Quest/SagesFocus.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace BossMod.Endwalker.Quest.SagesFocus; - -public enum OID : uint -{ - Boss = 0x3587, - Helper = 0x233C, - Mahaud = 0x3586, - Loifa = 0x3588, -} - -public enum AID : uint -{ - 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 deleted file mode 100644 index 7c93da35e1..0000000000 --- a/BossMod/Modules/Endwalker/Quest/TheKillingArt.cs +++ /dev/null @@ -1,87 +0,0 @@ -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/Ultimate/DSW2/DSW2.cs b/BossMod/Modules/Endwalker/Ultimate/DSW2/DSW2.cs index 36687de614..ddaa97b68b 100644 --- a/BossMod/Modules/Endwalker/Ultimate/DSW2/DSW2.cs +++ b/BossMod/Modules/Endwalker/Ultimate/DSW2/DSW2.cs @@ -1,16 +1,22 @@ namespace BossMod.Endwalker.Ultimate.DSW2; class P2AscalonsMercyConcealed(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AscalonsMercyConcealedAOE), new AOEShapeCone(50, 15.Degrees())); -class P2AscalonMight(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.AscalonsMight), new AOEShapeCone(50, 30.Degrees()), (uint)OID.BossP2); + +abstract class AscalonMight(BossModule module, OID oid) : Components.Cleave(module, ActionID.MakeSpell(AID.AscalonsMight), new AOEShapeCone(50, 30.Degrees()), [(uint)oid]); +class P2AscalonMight(BossModule module) : AscalonMight(module, OID.BossP2); + class P2UltimateEnd(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.UltimateEndAOE)); class P3Drachenlance(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DrachenlanceAOE), new AOEShapeCone(13, 45.Degrees())); class P3SoulTether(BossModule module) : Components.TankbusterTether(module, ActionID.MakeSpell(AID.SoulTether), (uint)TetherID.HolyShieldBash, 5); class P4Resentment(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Resentment)); class P5TwistingDive(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TwistingDive), new AOEShapeRect(60, 5)); -class P5Cauterize1(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Cauterize1), new AOEShapeRect(48, 10)); -class P5Cauterize2(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Cauterize2), new AOEShapeRect(48, 10)); + +abstract class Cauterize(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(48, 10)); +class P5Cauterize1(BossModule module) : Cauterize(module, AID.Cauterize1); +class P5Cauterize2(BossModule module) : Cauterize(module, AID.Cauterize2); + class P5SpearOfTheFury(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SpearOfTheFuryP5), new AOEShapeRect(50, 5)); -class P5AscalonMight(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.AscalonsMight), new AOEShapeCone(50, 30.Degrees()), (uint)OID.BossP5); +class P5AscalonMight(BossModule module) : AscalonMight(module, OID.BossP5); class P5Surrender(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Surrender)); class P6SwirlingBlizzard(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SwirlingBlizzard), new AOEShapeDonut(20, 35)); class P7Shockwave(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.ShockwaveP7)); diff --git a/BossMod/Modules/Endwalker/Unreal/Un4Zurvan/Un4Zurvan.cs b/BossMod/Modules/Endwalker/Unreal/Un4Zurvan/Un4Zurvan.cs index 31e5085bb9..5efe79813e 100644 --- a/BossMod/Modules/Endwalker/Unreal/Un4Zurvan/Un4Zurvan.cs +++ b/BossMod/Modules/Endwalker/Unreal/Un4Zurvan/Un4Zurvan.cs @@ -1,9 +1,11 @@ namespace BossMod.Endwalker.Unreal.Un4Zurvan; -class P1MetalCutter(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.MetalCutterP1), new AOEShapeCone(37.44f, 45.Degrees()), (uint)OID.BossP1); +abstract class MetalCutter(BossModule module, AID aid, OID oid) : Components.Cleave(module, ActionID.MakeSpell(aid), new AOEShapeCone(37.44f, 45.Degrees()), [(uint)oid]); +class P1MetalCutter(BossModule module) : MetalCutter(module, AID.MetalCutterP1, OID.BossP1); + class P1FlareStar(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FlareStarAOE), 6); class P1Purge(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Purge)); -class P2MetalCutter(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.MetalCutterP2), new AOEShapeCone(37.44f, 45.Degrees()), (uint)OID.BossP2); +class P2MetalCutter(BossModule module) : MetalCutter(module, AID.MetalCutterP2, OID.BossP2); class P2IcyVoidzone(BossModule module) : Components.PersistentVoidzone(module, 5, m => m.Enemies(OID.IcyVoidzone).Where(z => z.EventState != 7)); class P2BitingHalberd(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.BitingHalberd), new AOEShapeCone(55.27f, 135.Degrees())); class P2TailEnd(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TailEnd), 15); @@ -11,7 +13,7 @@ class P2TailEnd(BossModule module) : Components.SimpleAOEs(module, ActionID.Make class P2SouthernCross(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SouthernCrossAOE), 6); class P2SouthernCrossVoidzone(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.SouthernCrossVoidzone).Where(z => z.EventState != 7)); class P2WaveCannon(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.WaveCannonSolo), new AOEShapeRect(55.27f, 5)); -class P2TyrfingFire(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.TyrfingFire), new AOEShapeCircle(5), (uint)OID.BossP2, originAtTarget: true); +class P2TyrfingFire(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.TyrfingFire), new AOEShapeCircle(5), [(uint)OID.BossP2], originAtTarget: true); [ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.RemovedUnreal, GroupID = 951, NameID = 5567, PlanLevel = 90)] public class Un4Zurvan(WorldState ws, Actor primary) : BossModule(ws, primary, default, new ArenaBoundsCircle(20)) diff --git a/BossMod/Modules/Heavensward/Dungeon/D04TheVault/D042SerGrinnaux.cs b/BossMod/Modules/Heavensward/Dungeon/D04TheVault/D042SerGrinnaux.cs index d6d2e677db..fa58e57039 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D04TheVault/D042SerGrinnaux.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D04TheVault/D042SerGrinnaux.cs @@ -38,7 +38,7 @@ public enum AID : uint BossPhase2Vanish = 4256 // SerGrinnauxTheBull->self, no cast, single-target } -class HeavySwing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.HeavySwing), new AOEShapeCone(6.5f, 45.Degrees()), (uint)OID.SerGrinnauxTheBull); +class HeavySwing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.HeavySwing), new AOEShapeCone(6.5f, 45.Degrees()), [(uint)OID.SerGrinnauxTheBull]); class Overpower(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(10.2f, 45.Degrees())); class DimensionalRip(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 5, ActionID.MakeSpell(AID.DimensionalRip), m => m.Enemies(OID.StellarImplodeArea).Where(e => e.EventState != 7), 1.1f); diff --git a/BossMod/Modules/Heavensward/Dungeon/D05GreatGubalLibrary/D053TheEverlivingBibliotaph.cs b/BossMod/Modules/Heavensward/Dungeon/D05GreatGubalLibrary/D053TheEverlivingBibliotaph.cs index 08693c3d50..ca7ab684d2 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D05GreatGubalLibrary/D053TheEverlivingBibliotaph.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D05GreatGubalLibrary/D053TheEverlivingBibliotaph.cs @@ -92,7 +92,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) class DeepDarkness(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DeepDarkness), new AOEShapeDonut(12, 25)); class MagicBurst(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagicBurst), 15); class VoidBlizzardIIIAOE(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.VoidBlizzardIIIAOE), 5); -class AbyssalSwing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.AbyssalSwing), new AOEShapeCone(7.5f, 45.Degrees()), (uint)OID.Biblioklept); +class AbyssalSwing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.AbyssalSwing), new AOEShapeCone(7.5f, 45.Degrees()), [(uint)OID.Biblioklept]); class AbyssalCharge(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AbyssalCharge), new AOEShapeRect(41, 2)); class VoidCall(BossModule module) : Components.GenericTowers(module, prioritizeInsufficient: true) diff --git a/BossMod/Modules/Heavensward/Dungeon/D06AetherochemicalResearchFacility/D060Trash1.cs b/BossMod/Modules/Heavensward/Dungeon/D06AetherochemicalResearchFacility/D060Trash1.cs index fca4934aa0..a4ec0566d8 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D06AetherochemicalResearchFacility/D060Trash1.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D06AetherochemicalResearchFacility/D060Trash1.cs @@ -27,9 +27,9 @@ public enum AID : uint DefensiveManeuvers = 607 // ScrambledIronClaw->self, 3.0s cast, single-target, apply stoneskin } -class PassiveInfraredGuidanceSystem(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.PassiveInfraredGuidanceSystem), new AOEShapeCircle(6), (uint)OID.Boss, originAtTarget: true); +class PassiveInfraredGuidanceSystem(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.PassiveInfraredGuidanceSystem), new AOEShapeCircle(6), [(uint)OID.Boss], originAtTarget: true); -abstract class HeadSpin(BossModule module, AID aid, uint enemy) : Components.Cleave(module, ActionID.MakeSpell(aid), new AOEShapeCircle(5.225f), enemy) +abstract class HeadSpin(BossModule module, AID aid, OID oid) : Components.Cleave(module, ActionID.MakeSpell(aid), new AOEShapeCircle(5.225f), [(uint)oid]) { public override void AddHints(int slot, Actor actor, TextHints hints) { @@ -49,8 +49,8 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) base.DrawArenaForeground(pcSlot, pc); } } -class Headspin1(BossModule module) : HeadSpin(module, AID.Headspin1, (uint)OID.ScrambledPaladin); -class Headspin2(BossModule module) : HeadSpin(module, AID.Headspin2, (uint)OID.ScrambledEngineer); +class Headspin1(BossModule module) : HeadSpin(module, AID.Headspin1, OID.ScrambledPaladin); +class Headspin2(BossModule module) : HeadSpin(module, AID.Headspin2, OID.ScrambledEngineer); class GrandSword(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GrandSword), new AOEShapeCone(16, 60.Degrees())); class TheHand(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TheHand), new AOEShapeCone(7.5f, 60.Degrees())); diff --git a/BossMod/Modules/Heavensward/Dungeon/D13SohrKhai/D0130CloudGardener.cs b/BossMod/Modules/Heavensward/Dungeon/D13SohrKhai/D0130CloudGardener.cs index da72f1e168..92df9ddaf9 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D13SohrKhai/D0130CloudGardener.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D13SohrKhai/D0130CloudGardener.cs @@ -25,7 +25,7 @@ public enum AID : uint class RiseAndFall(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RiseAndFall), new AOEShapeCone(9, 135.Degrees())); class TightTornado(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TightTornado), new AOEShapeRect(18, 2)); -class Venom(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Venom), new AOEShapeCone(10.9f, 60.Degrees()), (uint)OID.SanctuarySkipper); +class Venom(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Venom), new AOEShapeCone(10.9f, 60.Degrees()), [(uint)OID.SanctuarySkipper]); class DarkBlizzardIII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DarkBlizzardIII), 5); class D130CloudGardenerStates : StateMachineBuilder diff --git a/BossMod/Modules/Heavensward/Extreme/Ext3Thordan/Ex3Thordan.cs b/BossMod/Modules/Heavensward/Extreme/Ext3Thordan/Ex3Thordan.cs index b0fb07f3c4..a451de6705 100644 --- a/BossMod/Modules/Heavensward/Extreme/Ext3Thordan/Ex3Thordan.cs +++ b/BossMod/Modules/Heavensward/Extreme/Ext3Thordan/Ex3Thordan.cs @@ -2,7 +2,7 @@ namespace BossMod.Heavensward.Extreme.Ex3Thordan; class AscalonsMight(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.AscalonsMight), new AOEShapeCone(11.8f, 45.Degrees())); -abstract class HeavenlySlash(BossModule module, OID oid) : Components.Cleave(module, ActionID.MakeSpell(AID.HeavenlySlash), new AOEShapeCone(10.2f, 45.Degrees()), (uint)oid); +abstract class HeavenlySlash(BossModule module, OID oid) : Components.Cleave(module, ActionID.MakeSpell(AID.HeavenlySlash), new AOEShapeCone(10.2f, 45.Degrees()), [(uint)oid]); class HeavenlySlashAdelphel(BossModule module) : HeavenlySlash(module, OID.SerAdelphel); class HeavenlySlashJanlenoux(BossModule module) : HeavenlySlash(module, OID.SerJanlenoux); diff --git a/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs b/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs deleted file mode 100644 index c81f39bf27..0000000000 --- a/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs +++ /dev/null @@ -1,63 +0,0 @@ -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/Job/Dragoon/DragoonsFate.cs similarity index 61% rename from BossMod/Modules/Heavensward/Quest/DragoonsFate.cs rename to BossMod/Modules/Heavensward/Quest/Job/Dragoon/DragoonsFate.cs index 6ffdd0b21f..623bbe27e3 100644 --- a/BossMod/Modules/Heavensward/Quest/DragoonsFate.cs +++ b/BossMod/Modules/Heavensward/Quest/Job/Dragoon/DragoonsFate.cs @@ -1,32 +1,30 @@ -namespace BossMod.Heavensward.Quest.DragoonsFate; +namespace BossMod.Heavensward.Quest.Job.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) + Boss = 0x10B9, // R7.0 + Icicle = 0x10BC, // R2.5 + Graoully = 0x10BA, // R7.0 } 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 + PillarImpact = 3095, // Icicle->self, 3.0s cast, range 4+R circle + PillarPierce = 4259, // Icicle->self, 2.0s cast, range 80+R width 4 rect + Cauterize = 4260, // Graoully->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 + ThinIce = 905 // Boss->player/10BB, extra=0x1/0x2/0x3 } -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 SheetOfIce(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SheetOfIce), 5); +class PillarImpact(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.PillarImpact), 6.5f); +class PillarPierce(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.PillarPierce), new AOEShapeRect(82.5f, 2)); +class Cauterize(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Cauterize), new AOEShapeRect(55, 10)); class Prey(BossModule module) : BossComponent(module) { @@ -71,7 +69,7 @@ public override void OnStatusLose(Actor actor, ActorStatus status) 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); + Cleave.Outline(Arena, primary.Position, primary.AngleTo(p), Colors.Danger); } } @@ -89,9 +87,7 @@ public GraoullyStates(BossModule module) : base(module) } [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 class Graoully(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) { - 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))); + private static readonly ArenaBoundsComplex arena = new([new PolygonCustom([new(-483.91f, -299.22f), new(-519.70f, -272.85f), new(-546.66f, -309.50f), new(-510.38f, -336.53f)])]); } diff --git a/BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs b/BossMod/Modules/Heavensward/Quest/MSQ/ASpectacleForTheAges.cs similarity index 75% rename from BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs rename to BossMod/Modules/Heavensward/Quest/MSQ/ASpectacleForTheAges.cs index 3d1a98e541..1835e06965 100644 --- a/BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs +++ b/BossMod/Modules/Heavensward/Quest/MSQ/ASpectacleForTheAges.cs @@ -1,4 +1,4 @@ -namespace BossMod.Heavensward.Quest.ASpectacleForTheAges; +namespace BossMod.Heavensward.Quest.MSQ.ASpectacleForTheAges; public enum OID : uint { @@ -12,8 +12,8 @@ public enum AID : uint 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 FlamingTizona(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FlamingTizona), 6); +class TheCurse(BossModule module) : Components.SimpleAOEs(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); diff --git a/BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs b/BossMod/Modules/Heavensward/Quest/MSQ/CloseEncountersOfTheVIthKind.cs similarity index 79% rename from BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs rename to BossMod/Modules/Heavensward/Quest/MSQ/CloseEncountersOfTheVIthKind.cs index b8941f2050..000030cef2 100644 --- a/BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs +++ b/BossMod/Modules/Heavensward/Quest/MSQ/CloseEncountersOfTheVIthKind.cs @@ -1,4 +1,4 @@ -namespace BossMod.Heavensward.Quest.CloseEncountersOfTheVIthKind; +namespace BossMod.Heavensward.Quest.MSQ.CloseEncountersOfTheVIthKind; public enum OID : uint { @@ -21,28 +21,27 @@ public RegulaVanHydrusStates(BossModule module) : base(module) TrivialPhase() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - ; + .ActivateOnEnter(); } } -class HandOfTheEmpire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HandOfTheEmpire), 2); - +class HandOfTheEmpire(BossModule module) : Components.SimpleAOEs(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 static readonly AOEShapeRect rect = new(40, 2); 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); + Arena.Actors(Adds, Colors.Danger, true); } public override IEnumerable ActiveAOEs(int slot, Actor actor) - => _active ? Adds.Select(x => new AOEInstance(new AOEShapeRect(40, 2), x.Position, x.Rotation)) : []; + => _active ? Adds.Select(x => new AOEInstance(rect, x.Position, x.Rotation)) : []; public override void OnCastStarted(Actor caster, ActorCastInfo spell) { diff --git a/BossMod/Modules/Heavensward/Quest/MSQ/DivineIntervention.cs b/BossMod/Modules/Heavensward/Quest/MSQ/DivineIntervention.cs new file mode 100644 index 0000000000..171951eda5 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/MSQ/DivineIntervention.cs @@ -0,0 +1,70 @@ +namespace BossMod.Heavensward.Quest.MSQ.DivineIntervention; + +public enum OID : uint +{ + Boss = 0x1010, + IshgardianSteelChain = 0x102C, // R1.0 + SerPaulecrainColdfire = 0x1011, // R0.5 + ThunderPicket = 0xEC4, // R1.0 + Helper = 0x233C +} + +public enum AID : uint +{ + LightningBolt = 3993, // ThunderPicket->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, // SerPaulecrainColdfire->self, 3.0s cast, range 6+R circle + Rive = 1135, // Boss->self, 2.5s cast, range 30+R width 2 rect + Heartstopper = 866, // SerPaulecrainColdfire->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.SimpleAOEs(module, ActionID.MakeSpell(AID.IronTempest), 5.5f); +class Overpower(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6.5f, 45.Degrees())); +class RingOfFrost(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RingOfFrost), 6.5f); +class Rive(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Rive), new AOEShapeRect(30.5f, 1)); +class Heartstopper(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Heartstopper), new AOEShapeRect(3.5f, 1.5f)); +class Chain(BossModule module) : Components.Adds(module, (uint)OID.IshgardianSteelChain, 1); + +class SerGrinnauxStates : StateMachineBuilder +{ + public SerGrinnauxStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.Enemies(SerGrinnaux.Bosses).All(x => x.IsDeadOrDestroyed); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67133, NameID = 3850)] +public class SerGrinnaux(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Capsule(new(0, 1.979f), 3.66f, 11.45f, 50, 90.Degrees())], [new Rectangle(new(0, -9.995f), 4, 0.7f)]); + public static readonly uint[] Bosses = [(uint)OID.Boss, (uint)OID.SerPaulecrainColdfire]; + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(Enemies(Bosses)); + Arena.Actors(Enemies(OID.IshgardianSteelChain), Colors.Object); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + e.Priority = (OID)e.Actor.OID switch + { + OID.IshgardianSteelChain => 1, + _ => 0 + }; + } + } +} diff --git a/BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs b/BossMod/Modules/Heavensward/Quest/MSQ/FlyFreeMyPretty.cs similarity index 67% rename from BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs rename to BossMod/Modules/Heavensward/Quest/MSQ/FlyFreeMyPretty.cs index 965839ee10..4b7812c68f 100644 --- a/BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs +++ b/BossMod/Modules/Heavensward/Quest/MSQ/FlyFreeMyPretty.cs @@ -1,15 +1,11 @@ -<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs -namespace BossMod.Heavensward.Quest.MSQ.Heliodrome; -======== -namespace BossMod.Heavensward.Quest.FlyFreeMyPretty; ->>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs +namespace BossMod.Heavensward.Quest.MSQ.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) + GrynewahtP2 = 0x195F, // R0.5 + ImperialColossus = 0x1966, // R3.0 + Helper = 0x233C } public enum AID : uint @@ -30,8 +26,8 @@ class MagitekMissiles(BossModule module) : Components.SimpleAOEs(module, ActionI class ShrapnelShell(BossModule module) : Components.SimpleAOEs(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.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); -class Suffering(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), 6.5f); +class AugmentedUprising(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); +class AugmentedSuffering(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), 6.5f); class Heartstopper(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Heartstopper), new AOEShapeRect(3.5f, 1.5f)); class Overpower(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6, 45.Degrees())); class GrandSword(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GrandSword), new AOEShapeCone(21, 60.Degrees())); @@ -42,8 +38,11 @@ class Adds(BossModule module) : Components.AddsMulti(module, [0x1960, 0x1961, 0x { public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var e in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; e.Priority = (OID)e.Actor.OID == OID.ImperialColossus ? 5 : e.Actor.TargetID == actor.InstanceID ? 1 : 0; + } } } @@ -52,7 +51,7 @@ 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); + Arena.Bounds = Grynewaht.CircleBounds; } } @@ -77,8 +76,8 @@ public GrynewahtStates(BossModule module) : base(module) { State build(uint id) => SimpleState(id, 10000, "Enrage") .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() @@ -89,37 +88,17 @@ State build(uint id) => SimpleState(id, 10000, "Enrage") .ActivateOnEnter(); SimplePhase(1, id => build(id).ActivateOnEnter(), "P1") - .Raw.Update = () => module.Enemies(OID.GrynewahtP2).Count != 0; + .Raw.Update = () => Module.Enemies(OID.GrynewahtP2).Count != 0; DeathPhase(0x100, id => build(id).ActivateOnEnter().OnEnter(() => { - module.Arena.Bounds = Grynewaht.CircleBounds; + Module.Arena.Bounds = Grynewaht.CircleBounds; })); } } -<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs -[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 222, NameID = 5576)] -public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, default, hexBounds) -======== [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) ->>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs +public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, default, hexBounds) { private static readonly ArenaBoundsComplex hexBounds = new([new Polygon(default, 10.675f, 6, 30.Degrees())]); public static readonly ArenaBoundsComplex CircleBounds = new([new Polygon(default, 20, 20)]); - -<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs - protected override bool CheckPull() => Raid.Player()!.InCombat; -======== - 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)); - } ->>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs } diff --git a/BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs b/BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs deleted file mode 100644 index 965839ee10..0000000000 --- a/BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs +++ /dev/null @@ -1,125 +0,0 @@ -<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs -namespace BossMod.Heavensward.Quest.MSQ.Heliodrome; -======== -namespace BossMod.Heavensward.Quest.FlyFreeMyPretty; ->>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs - -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.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekMissiles), 15); -class ShrapnelShell(BossModule module) : Components.SimpleAOEs(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.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); -class Suffering(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), 6.5f); -class Heartstopper(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Heartstopper), new AOEShapeRect(3.5f, 1.5f)); -class Overpower(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6, 45.Degrees())); -class GrandSword(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GrandSword), new AOEShapeCone(21, 60.Degrees())); -class MagitekRay(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekRay), 6); -class GrandStrike(BossModule module) : Components.SimpleAOEs(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).Count != 0; - DeathPhase(0x100, id => build(id).ActivateOnEnter().OnEnter(() => - { - module.Arena.Bounds = Grynewaht.CircleBounds; - })); - } -} - -<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs -[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 222, NameID = 5576)] -public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, default, hexBounds) -======== -[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) ->>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs -{ - private static readonly ArenaBoundsComplex hexBounds = new([new Polygon(default, 10.675f, 6, 30.Degrees())]); - public static readonly ArenaBoundsComplex CircleBounds = new([new Polygon(default, 20, 20)]); - -<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs - protected override bool CheckPull() => Raid.Player()!.InCombat; -======== - 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)); - } ->>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs -} diff --git a/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs b/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs index 344ee0a806..10af1ca419 100644 --- a/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs +++ b/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs @@ -62,8 +62,9 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { var playerIsAttacked = false; - foreach (var e in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var e = hints.PotentialTargets[i]; if (e.Actor.TargetID == actor.InstanceID) { playerIsAttacked = true; @@ -85,8 +86,9 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } else { - foreach (var e in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var e = hints.PotentialTargets[i]; if (e.Actor == Knight) e.Priority = 2; else if (e.Actor == Covered) diff --git a/BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs b/BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs deleted file mode 100644 index e9f3b69463..0000000000 --- a/BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs +++ /dev/null @@ -1,50 +0,0 @@ -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/Heavensward/Quest/TheWarringTriad/ABloodyReunion.cs b/BossMod/Modules/Heavensward/Quest/TheWarringTriad/ABloodyReunion.cs new file mode 100644 index 0000000000..c3b51241dd --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/TheWarringTriad/ABloodyReunion.cs @@ -0,0 +1,59 @@ +namespace BossMod.Heavensward.Quest.WarringTriad.ABloodyReunion; + +public enum OID : uint +{ + Boss = 0x161E, + MagitekTurretI = 0x161F, // R0.6 + MagitekTurretII = 0x1620, // R0.6 + TerminusEst = 0x1621, // R1.0 + Helper = 0x233C +} + +public enum AID : uint +{ + MagitekSlug = 6026, // Boss->self, 2.5s cast, range 60+R width 4 rect + AetherochemicalGrenado = 6031, // MagitekTurretII->location, 3.0s cast, range 8 circle + SelfDetonate = 6032, // MagitekTurretI/MagitekTurretII->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.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekSlug), new AOEShapeRect(60, 2)); +class AetherochemicalGrenado(BossModule module) : Components.SimpleAOEs(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) + { + var count = hints.PotentialTargets.Count; + for (var i = 0; i < count; ++i) + { + var h = hints.PotentialTargets[i]; + if (h.Actor.CastInfo?.Action == WatchedAction) + h.Priority = 5; + } + } +} +class MagitekSpread(BossModule module) : Components.SimpleAOEs(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.CFC, GroupID = 173, NameID = 3818)] +public class RegulaVanHydrus(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(230, 79), 20.256f, 24)]); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(Enemies([(uint)OID.MagitekTurretI, (uint)OID.MagitekTurretII])); + Arena.Actor(PrimaryActor); + } +} diff --git a/BossMod/Modules/RealmReborn/Extreme/Ex2Garuda/Ex2Garuda.cs b/BossMod/Modules/RealmReborn/Extreme/Ex2Garuda/Ex2Garuda.cs index 36b27c56e9..d919291c37 100644 --- a/BossMod/Modules/RealmReborn/Extreme/Ex2Garuda/Ex2Garuda.cs +++ b/BossMod/Modules/RealmReborn/Extreme/Ex2Garuda/Ex2Garuda.cs @@ -2,9 +2,9 @@ class DownburstBoss(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Downburst1), new AOEShapeCone(11.7f, 60.Degrees())); // TODO: verify angle -abstract class Downburst(BossModule module, AID aid, uint oid) : Components.Cleave(module, ActionID.MakeSpell(aid), new AOEShapeCone(11.36f, 60.Degrees()), oid); // TODO: verify angle -class DownburstSuparna(BossModule module) : Downburst(module, AID.Downburst1, (uint)OID.Suparna); // TODO: verify angle -class DownburstChirada(BossModule module) : Downburst(module, AID.Downburst2, (uint)OID.Chirada); // TODO: verify angle +abstract class Downburst(BossModule module, AID aid, OID oid) : Components.Cleave(module, ActionID.MakeSpell(aid), new AOEShapeCone(11.36f, 60.Degrees()), [(uint)oid]); // TODO: verify angle +class DownburstSuparna(BossModule module) : Downburst(module, AID.Downburst1, OID.Suparna); // TODO: verify angle +class DownburstChirada(BossModule module) : Downburst(module, AID.Downburst2, OID.Chirada); // TODO: verify angle class Slipstream(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Slipstream), new AOEShapeCone(11.7f, 45.Degrees())); class FrictionAdds(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FrictionAdds), 5); diff --git a/BossMod/Modules/RealmReborn/Quest/OperationArchon.cs b/BossMod/Modules/RealmReborn/Quest/MSQ/OperationArchon.cs similarity index 54% rename from BossMod/Modules/RealmReborn/Quest/OperationArchon.cs rename to BossMod/Modules/RealmReborn/Quest/MSQ/OperationArchon.cs index 2b69db0e91..aacea470b6 100644 --- a/BossMod/Modules/RealmReborn/Quest/OperationArchon.cs +++ b/BossMod/Modules/RealmReborn/Quest/MSQ/OperationArchon.cs @@ -1,11 +1,11 @@ -namespace BossMod.RealmReborn.Quest.OperationArchon; +namespace BossMod.RealmReborn.Quest.MSQ.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) + Helper = 0x233C } public enum SID : uint @@ -24,24 +24,25 @@ public enum AID : uint 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 Adds(BossModule module) : Components.AddsMulti(module, [(uint)OID.ImperialPilusPrior, (uint)OID.ImperialCenturion]); -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) +class MagitekMissiles(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekMissiles), 7); +class DrillShot(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DrillShot), new AOEShapeRect(30, 2.5f)); +class TartareanShockwave(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TartareanShockwave), 7); +class BigTartareanShockwave(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TartareanShockwave1), 14); +class GalesOfTartarus(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GalesOfTartarus), new AOEShapeRect(30, 2.5f)); +class BigGalesOfTartarus(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GalesOfTartarus1), new AOEShapeRect(30, 15)); +class DirectionalParry(BossModule module) : Components.DirectionalParry(module, [(uint)OID.Boss]) { + private static readonly Angle a45 = 45.Degrees(); + 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)); + hints.AddForbiddenZone(ShapeDistance.Cone(Module.PrimaryActor.Position, 100, Module.PrimaryActor.Rotation, a45), WorldState.FutureTime(10)); } } -class TartareanTomb(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TartareanTomb), new AOEShapeCircle(11)); +class TartareanTomb(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TartareanTomb), 11); class RhitahtynSasArvinaStates : StateMachineBuilder { @@ -54,11 +55,9 @@ public RhitahtynSasArvinaStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - ; + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/RealmReborn/Quest/MSQ/TheStepsOfFaith.cs b/BossMod/Modules/RealmReborn/Quest/MSQ/TheStepsOfFaith.cs new file mode 100644 index 0000000000..eb89d11dbe --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/MSQ/TheStepsOfFaith.cs @@ -0,0 +1,340 @@ +namespace BossMod.RealmReborn.Quest.MSQ.TheStepsOfFaith; + +public enum OID : uint +{ + Boss = 0x3A5F, // R30.0 + HordeWyvern1 = 0x3CD5, // R3.6 + HordeWyvern2 = 0x3AA2, // R3.6 + HordeWyvern3 = 0x3AA4, // R3.6 + HordeWyvern4 = 0x3AA5, // R3.6 + HordeWyvern5 = 0x3AAC, // R3.6 + HordeWyvern6 = 0x3AA7, // R3.6 + HordeWyvern7 = 0x3AAF, // R3.6 + HordeWyvern8 = 0x3ABF, // R3.6 + HordeWyvern9 = 0x3AA8, // R3.6 + HordeWyvern10 = 0x3AA9, // R3.6 + HordeDragonfly1 = 0x3A94, // R0.8 + HordeDragonfly2 = 0x3A93, // R0.8 + HordeDragonfly3 = 0x3A95, // R0.8 + HordeDragonfly4 = 0x3A96, // R0.8 + HordeDragonfly5 = 0x3A97, // R0.8 + HordeDragonfly6 = 0x3ABB, // R0.8 + HordeDragonfly7 = 0x3ABC, // R0.8 + HordeDragonfly8 = 0x3AB0, // R0.8 + HordeAevis1 = 0x3A9A, // R2.2 + HordeAevis2 = 0x3A9B, // R2.2 + HordeAevis3 = 0x3A9C, // R2.2 + HordeAevis4 = 0x3A9D, // R2.2 + HordeAevis5 = 0x3AA0, // R2.2 + HordeAevis6 = 0x3A9E, // R2.2 + HordeAevis7 = 0x3A9F, // R2.2 + HordeAevis8 = 0x3AC0, // R2.2 + HordeAevis9 = 0x3AB1, // R2.2 + HordeBiast1 = 0x3AB2, // R2.7 + HordeBiast2 = 0x3AB3, // R2.7 + HordeBiast3 = 0x3AB4, // R2.7 + HordeBiast4 = 0x3AAB, // R2.7 + HordeBiast5 = 0x3AB6, // R2.7 + HordeBiast6 = 0x3AC2, // R2.7 + HordeBiast7 = 0x3AB8, // R2.7 + HordeBiast8 = 0x3AB9, // R2.7 + HordeBiast9 = 0x3AB7, // R2.7 + HordeArmoredDragon = 0x3ABA, // R6.25 + HordeTranscendent = 0x3ABD, // R3.4 + HordeShieldDragon = 0x3AC1, // R5.3 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack1 = 6499, // horde dragons->allies, no cast, single-target + + BlazingShriekVisual = 30882, // Boss->self, no cast, single-target + BlazingShriek = 26407, // Helper->self, 0.8s cast, range 100 width 44 rect + FlameBreathVisual = 30877, // Boss->self, 3.3+1,7s cast, single-target + FlameBreath = 26812, // Helper->self, 35.0s cast, range 1 width 2 rect + FlameBreath1 = 30185, // Helper->self, 5.0s cast, range 1 width 2 rect + FlameBreath2 = 26411, // Boss->self, 3.8+1.2s cast, range 60 width 20 rect + FlameBreath3 = 30186, // Helper->self, 5.0s cast, range 60 width 20 rect + FlameBreathChannel = 30884, // Helper->self, no cast, range 40 width 20 rect + CauterizeVisual = 30878, // Boss->self, 30.5+4.5s cast, single-target + Cauterize = 30885, // Helper->self, no cast, range 40 width 44 rect + Touchdown = 26408, // Helper->self, 6.0s cast, range 80 circle + FireballVisual1 = 30874, // Boss->self, 3.0+3,0s cast, single-target + FireballVisual2 = 30876, // Boss->self, 3.0s cast, single-target + FireballVisual3 = 28975, // Boss->self, 3.0s cast, single-target + FireballSpread = 30875, // Helper->allies, 6.0s cast, range 6 circle + FireballAOE = 30894, // HordeTranscendent->location, 3.5s cast, range 6 circle + BlazingFire = 30211, // Boss->location, no cast, range 10 circle + + BodySlamVisual = 26400, // Boss->self, 4.7+1,3s cast, single-target + BodySlam = 26401, // Helper->self, 6.0s cast, range 80 width 44 rect + Flamisphere = 30883, // Helper->location, 8.0s cast, range 10 circle + + RipperClaw = 31262, // HordeTranscendent->self, 3.7s cast, range 9 90-degree cone + EarthshakerAOE = 30880, // Boss->self, 4.5s cast, range 31 circle + Earthshaker = 30887, // Helper->self, 6.5s cast, range 80 30-degree cone + EarthrisingAOE = 26410, // Boss->self, 4.5s cast, range 31 circle + EarthrisingCast = 30888, // Helper->self, 7.0s cast, range 8 circle + EarthrisingRepeat = 26412, // Helper->self, no cast, range 8 circle + SidewiseSlice = 30879, // Boss->self, 8.0s cast, range 50 120-degree cone + ScorchingBreathVisual = 29785, // Boss->self, 15.0+5.0s cast, single-target + ScorchingBreath = 29789, // Helper->self, no cast, range 40 width 20 rect + SeismicShriekVisual = 30881, // Boss->self, no cast, single-target + SeismicShriek1 = 26405, // Boss->self, no cast, range 100 circle + SeismicShriek2 = 26406, // Boss->self, no cast, range 80 circle + Twingaze = 28971, // Boss->self, no cast, single-target + Levinshower = 30892, // HordeBiast2/HordeBiast4/HordeBiast5/HordeBiast6/HordeBiast7->self, no cast, range 6 120-degree cone + DragonStomp = 30893, // HordeArmoredDragon->self, 2.0s cast, range 40 circle + BoneShaker = 31258, // HordeTranscendent->self, no cast, range 50 circle + MagmaticSpell = 28974, // Boss->self, no cast, single-target + Rake = 30898, // HordeShieldDragon->player, no cast, single-target + FallOfMan = 30187, // Helper->self, 20.0s cast, range 90 width 20 rect +} + +class RipperClaw(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RipperClaw), new AOEShapeCone(9, 45.Degrees())); +class Levinshower(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Levinshower), new AOEShapeCone(6, 60.Degrees()), +[(uint)OID.HordeBiast2, (uint)OID.HordeBiast4, (uint)OID.HordeBiast5, (uint)OID.HordeBiast6, (uint)OID.HordeBiast7]); +class EarthShakerAOE(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.EarthshakerAOE), 31); +class Earthshaker(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Earthshaker), new AOEShapeCone(80, 15.Degrees()), 2); + +class EarthrisingAOE(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.EarthrisingAOE), 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 = spell.LocXZ, 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) + { + var index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1)); + if (index < 0) + return; + AdvanceLine(Lines[index], caster.Position); + if (Lines[index].ExplosionsLeft == 0) + Lines.RemoveAt(index); + } + } +} + +class SidewiseSlice(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SidewiseSlice), new AOEShapeCone(50, 60.Degrees())); + +class FireballSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.FireballSpread), 6); +class FireballAOE(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FireballAOE), 6); +class Flamisphere(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Flamisphere), 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; + private static readonly AOEShapeRect rect = new(500, 10); + + 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.FlameBreath1) + _aoe = new(rect, Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, 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; + private static readonly AOEShapeRect rect = new(60, 10); + + 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.FlameBreath2) + { + NumCasts = 0; + _aoe = new(rect, spell.LocXZ, 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 rect = new(160, 22); + 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(MoveIt, Arena.Center); + else + yield return new(rect, 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) +{ + private static readonly AOEShapeRect rect = new(100, 10, 100); + 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(rect, 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 (state == 0x00020001) + { + if (index == 0x03) + { + ZBounds = (120, 200); + Phase = 2; + } + else if (index == 0x04) + { + ZBounds = (-40, 40); + Phase = 4; + } + else if (index == 0x06) + { + ZBounds = (-200, -120); + Phase = 6; + } + } + else if (state == 0x00800040) + { + if (index == 0x00) + { + ZBounds = (-40, 200); + Phase = 3; + } + else if (index == 0x01) + { + ZBounds = (-200, 40); + Phase = 5; + } + } + } + + 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(ShapeDistance.Rect(Arena.Center, new WDir(0, 1), 38, 22, 40)); + // subsequent state transitions don't trigger until player moves into the area + else if (Phase == 3 && actor.Position.Z > 25 || Phase == 5 && actor.Position.Z > -135) + hints.AddForbiddenZone(ShapeDistance.Rect(Arena.Center, new WDir(0, 1), 40, 22, 38)); + } + + public override void 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() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70127, NameID = 3330)] +public class Vishap(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 245), ScrollingBounds.Bounds) +{ + // vishap doesn't start targetable + private static readonly uint[] opponents = [(uint)OID.HordeWyvern1, (uint)OID.HordeWyvern2, (uint)OID.HordeWyvern3, (uint)OID.HordeWyvern4, (uint)OID.HordeWyvern5, + (uint)OID.HordeWyvern6, (uint)OID.HordeWyvern7, (uint)OID.HordeWyvern8, (uint)OID.HordeWyvern8, (uint)OID.HordeWyvern9, (uint)OID.HordeWyvern10, (uint)OID.HordeDragonfly1, + (uint)OID.HordeDragonfly2, (uint)OID.HordeDragonfly3, (uint)OID.HordeDragonfly4, (uint)OID.HordeDragonfly5, (uint)OID.HordeDragonfly6, (uint)OID.HordeDragonfly7, + (uint)OID.HordeDragonfly8, (uint)OID.HordeAevis1, (uint)OID.HordeAevis2, (uint)OID.HordeAevis3, (uint)OID.HordeAevis4, (uint)OID.HordeAevis5, (uint)OID.HordeAevis6, + (uint)OID.HordeAevis7, (uint)OID.HordeAevis8, (uint)OID.HordeAevis9, (uint)OID.HordeBiast1, (uint)OID.HordeBiast2, (uint)OID.HordeBiast3, (uint)OID.HordeBiast4, + (uint)OID.HordeBiast5, (uint)OID.HordeBiast6, (uint)OID.HordeBiast7, (uint)OID.HordeBiast8, (uint)OID.HordeBiast9, (uint)OID.HordeArmoredDragon, (uint)OID.HordeShieldDragon, + (uint)OID.HordeTranscendent]; + + protected override bool CheckPull() => PrimaryActor.InCombat; + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(Enemies(opponents)); + Arena.Actor(PrimaryActor, allowDeadAndUntargetable: true); + } +} + diff --git a/BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs b/BossMod/Modules/RealmReborn/Quest/MSQ/TheUltimateWeapon.cs similarity index 58% rename from BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs rename to BossMod/Modules/RealmReborn/Quest/MSQ/TheUltimateWeapon.cs index ef6bf5515e..706436b8be 100644 --- a/BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs +++ b/BossMod/Modules/RealmReborn/Quest/MSQ/TheUltimateWeapon.cs @@ -1,10 +1,10 @@ -namespace BossMod.RealmReborn.Quest.TheUltimateWeapon; +namespace BossMod.RealmReborn.Quest.MSQ.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) + Boss = 0x3933, // R1.75 + SeaOfPitch = 0x1EB738, // R0.5 + Firesphere = 0x3934, // R1.0 } public enum AID : uint @@ -27,28 +27,24 @@ class BurstFlare(BossModule module) : Components.KnockbackFromCastTarget(module, { 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)); + if (Casters.Count != 0) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center, 5), Module.CastFinishAt(Casters[0].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 GripOfNight(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GripOfNight), new AOEShapeCone(40, 75.Degrees())); +class AncientCross(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AncientCross), 6, 8); +class AncientEruption(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AncientEruption), 6); +class FluidFlare(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FluidFlare), new AOEShapeCone(40, 30.Degrees())); class FireSphere(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.Burst)) { private DateTime? _predictedCast; + private static readonly AOEShapeCircle circle = new(8); + public override void OnCastFinished(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.FiresphereSummon) @@ -65,60 +61,50 @@ 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); + yield return new(circle, enemy.Position, default, dt); } } -class Nightburn(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Nightburn), "WoLbuster"); +class Nightburn(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Nightburn)); +class AncientFire(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AncientFireIII)); -class AncientFire(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AncientFireIII), hint: "Raidwide + spawn deathwall"); - -class DeathWall(BossModule module) : BossComponent(module) +class ArenaChange(BossModule module) : Components.GenericAOEs(module) { - private bool _active; - private bool _completed; + private bool completed; + private static readonly AOEShapeDonut donut = new(15, 20); + private AOEInstance? _aoe; - 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 IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if (_active) - hints.AddForbiddenZone(new AOEShapeDonut(15, 100), Arena.Center); + if ((AID)spell.Action.ID == AID.AncientFireIII && !completed) + _aoe = new(donut, Arena.Center, default, Module.CastFinishAt(spell, 0.7f)); } public override void OnEventEnvControl(byte index, uint state) { if (index == 0 && state == 0x20001) { - Module.Arena.Bounds = new ArenaBoundsCircle(15); - _completed = true; - _active = false; + Arena.Bounds = new ArenaBoundsCircle(15); + completed = true; } } } -class DarkThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkThunder), new AOEShapeCircle(1)); - +class DarkThunder(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DarkThunder), 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)); +abstract class EoD(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(60, 4)); +class EndOfDays(BossModule module) : EoD(module, AID.EndOfDays); +class EndOfDaysAdds(BossModule module) : EoD(module, AID.EndOfDaysAdds); class LahabreaStates : StateMachineBuilder { public LahabreaStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() @@ -130,8 +116,7 @@ public LahabreaStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - ; + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs b/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs deleted file mode 100644 index ec19d7d21a..0000000000 --- a/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs +++ /dev/null @@ -1,263 +0,0 @@ -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 Vishap(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/Raid/T01Caduceus/T01Caduceus.cs b/BossMod/Modules/RealmReborn/Raid/T01Caduceus/T01Caduceus.cs index 05301f071d..756e869c60 100644 --- a/BossMod/Modules/RealmReborn/Raid/T01Caduceus/T01Caduceus.cs +++ b/BossMod/Modules/RealmReborn/Raid/T01Caduceus/T01Caduceus.cs @@ -30,7 +30,7 @@ public enum SID : uint SteelScales = 349, // Boss->Boss, extra=1-8 (num stacks) } -class HoodSwing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.HoodSwing), new AOEShapeCone(11, 60.Degrees()), (uint)OID.Boss) // TODO: verify angle +class HoodSwing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.HoodSwing), new AOEShapeCone(11, 60.Degrees())) // TODO: verify angle { private DateTime _lastBossCast; // assume boss/add cleaves are synchronized?.. public float SecondsUntilNextCast() => Math.Max(0, 18 - (float)(WorldState.CurrentTime - _lastBossCast).TotalSeconds); diff --git a/BossMod/Modules/RealmReborn/Raid/T04Gauntlet/T04Gauntlet.cs b/BossMod/Modules/RealmReborn/Raid/T04Gauntlet/T04Gauntlet.cs index 1feeccd45b..86195b4c86 100644 --- a/BossMod/Modules/RealmReborn/Raid/T04Gauntlet/T04Gauntlet.cs +++ b/BossMod/Modules/RealmReborn/Raid/T04Gauntlet/T04Gauntlet.cs @@ -28,7 +28,7 @@ public enum AID : uint EmergencyOverride = 1258 // DriveCylinder->self, no cast, soft enrage raidwide } -class Rotoswipe(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Rotoswipe), new AOEShapeCone(11, 60.Degrees()), (uint)OID.ClockworkDreadnaught); // TODO: verify angle +class Rotoswipe(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Rotoswipe), new AOEShapeCone(11, 60.Degrees()), [(uint)OID.ClockworkDreadnaught]); // TODO: verify angle class GravityThrustPox(BossModule module) : Components.GenericAOEs(module, default, "Move behind rook!") { diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D08AkadaemiaAnyder/D082MorbolMarquis.cs b/BossMod/Modules/Shadowbringers/Dungeon/D08AkadaemiaAnyder/D082MorbolMarquis.cs index 6af05cb58b..a3eeb93ba5 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D08AkadaemiaAnyder/D082MorbolMarquis.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D08AkadaemiaAnyder/D082MorbolMarquis.cs @@ -44,7 +44,8 @@ class SapShower(BossModule module) : Components.SpreadFromCastTargets(module, Ac class ExtensibleTendrilsPutridBreath(BossModule module) : Components.GenericAOEs(module) { private static readonly AOEShapeCross cross = new(25, 3); - private static readonly AOEShapeCone cone = new(25, D082MorbolMarquis.A45); + private static readonly Angle a45 = 45.Degrees(); + private static readonly AOEShapeCone cone = new(25, a45); private AOEInstance? _aoe; private DateTime activation; private int remainingCasts; @@ -60,7 +61,7 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) { var delay1 = activation.AddSeconds((5 - remainingCasts) * 6.1f); if ((delay1 - WorldState.CurrentTime).TotalSeconds <= 2.5f) - yield return new(cross, Module.PrimaryActor.Position, Module.PrimaryActor.Rotation + D082MorbolMarquis.A45, delay1); + yield return new(cross, Module.PrimaryActor.Position, Module.PrimaryActor.Rotation + a45, delay1); } var delay2 = activation.AddSeconds(27.1f); if (activation != default && (delay2 - WorldState.CurrentTime).TotalSeconds <= 4.9f) @@ -128,14 +129,14 @@ public D082MorbolMarquisStates(BossModule module) : base(module) public class D082MorbolMarquis(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultBounds.Center, DefaultBounds) { private const int X = -224, InnerRadius = 10, OuterRadius = 15, Radius = 25, Edges = 12; - private static readonly WPos ArenaCenter = new(X, -38); - public static readonly Angle A45 = 45.Degrees(), a135 = 135.Degrees(); - private static readonly Polygon[] defaultCircle = [new(ArenaCenter, 24.5f * CosPI.Pi48th, 48)]; + private static readonly WPos arenaCenter = new(X, -38); + private static readonly Angle a45 = 45.Degrees(), a135 = 135.Degrees(); + private static readonly Polygon[] defaultCircle = [new(arenaCenter, 24.5f * CosPI.Pi48th, 48)]; private static readonly Rectangle[] defaultDifference = [new(new(X, -13), Radius, 1.1f), new(new(X, -63), Radius, 1.1f)]; - private static readonly Shape[] blueBlossom = [new ConeV(ArenaCenter, InnerRadius, A45, A45, Edges), new ConeV(ArenaCenter, InnerRadius, -a135, A45, Edges), - new DonutSegmentV(ArenaCenter, OuterRadius, Radius, A45, A45, Edges), new DonutSegmentV(ArenaCenter, OuterRadius, Radius, -a135, A45, Edges)]; - private static readonly Shape[] yellowBlossom = [new ConeV(ArenaCenter, InnerRadius, -A45, A45, Edges), new ConeV(ArenaCenter, InnerRadius, a135, A45, Edges), - new DonutSegmentV(ArenaCenter, OuterRadius, Radius, -A45, A45, Edges), new DonutSegmentV(ArenaCenter, OuterRadius, Radius, a135, A45, Edges)]; + private static readonly Shape[] blueBlossom = [new ConeV(arenaCenter, InnerRadius, a45, a45, Edges), new ConeV(arenaCenter, InnerRadius, -a135, a45, Edges), + new DonutSegmentV(arenaCenter, OuterRadius, Radius, a45, a45, Edges), new DonutSegmentV(arenaCenter, OuterRadius, Radius, -a135, a45, Edges)]; + private static readonly Shape[] yellowBlossom = [new ConeV(arenaCenter, InnerRadius, -a45, a45, Edges), new ConeV(arenaCenter, InnerRadius, a135, a45, Edges), + new DonutSegmentV(arenaCenter, OuterRadius, Radius, -a45, a45, Edges), new DonutSegmentV(arenaCenter, OuterRadius, Radius, a135, a45, Edges)]; public static readonly ArenaBoundsComplex DefaultBounds = new(defaultCircle, defaultDifference); public static readonly ArenaBoundsComplex BlueBlossomBounds = new(defaultCircle, [.. defaultDifference, .. blueBlossom]); public static readonly ArenaBoundsComplex YellowBlossomBounds = new(defaultCircle, [.. defaultDifference, .. yellowBlossom]); diff --git a/BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs b/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/GambolingForGil.cs similarity index 75% rename from BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs rename to BossMod/Modules/Shadowbringers/Quest/Job/Dancer/GambolingForGil.cs index 29fcdbbe6e..424252596e 100644 --- a/BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/GambolingForGil.cs @@ -1,9 +1,9 @@ -namespace BossMod.Shadowbringers.Quest.GambolingForGil; +namespace BossMod.Shadowbringers.Quest.Job.Dancer.GambolingForGil; public enum OID : uint { - Boss = 0x29D2, // R0.500, x1 - Whirlwind = 0x29D5, // R1.000, x0 (spawn during fight) + Boss = 0x29D2, // R0.5 + Whirlwind = 0x29D5, // R1.0 } public enum AID : uint @@ -58,11 +58,11 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } } 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 WarDance(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.WarDance), 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 HannishFire(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HannishFire1), 6); +class HannishWaters(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HannishWaters), new AOEShapeCone(40, 15.Degrees())); +class RanaasFinish(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RanaasFinish), 15); class RanaaMihgoStates : StateMachineBuilder { @@ -80,9 +80,8 @@ public RanaaMihgoStates(BossModule module) : base(module) } } -[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) +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68786, NameID = 8489)] +public class RanaaMihgo(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) { - public static readonly ArenaBoundsCustom WeirdBounds = new(17.5f, new(CurveApprox.Ellipse(17.5f, 16f, 0.01f))); + public static readonly ArenaBoundsComplex arena = new([new Ellipse(new(520.47f, 124.99f), 17.5f, 16, 50)]); } - diff --git a/BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs b/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/SaveTheLastDanceForMe.cs similarity index 65% rename from BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs rename to BossMod/Modules/Shadowbringers/Quest/Job/Dancer/SaveTheLastDanceForMe.cs index b69b7bc9cd..7f81b119ae 100644 --- a/BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/SaveTheLastDanceForMe.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.SaveTheLastDanceForMe; +namespace BossMod.Shadowbringers.Quest.Job.Dancer.SaveTheLastDanceForMe; public enum OID : uint { @@ -16,45 +16,33 @@ public enum AID : uint 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 Dread(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Dread), 5); +class BitterLove(BossModule module) : Components.SimpleAOEs(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 - }); + Lines.Add(new() { Next = spell.LocXZ, Advance = spell.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}"); + var index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1)); + if (index < 0) return; - } - AdvanceLine(Lines[index], caster.Position); + if (Lines[index].ExplosionsLeft == 0) + Lines.RemoveAt(index); } } } 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 ForebodingAura(BossModule module) : Components.PersistentVoidzone(module, 8, m => m.Enemies(OID.ForebodingAura).Where(e => !e.IsDead)); class AethericShadowStates : StateMachineBuilder { @@ -75,7 +63,7 @@ public AethericShadowStates(BossModule module) : base(module) { 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) + if (actor.FindStatus(DNC.SID.ClosedPosition) == null && Raid.WithoutSlot(false, false).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/Job/Gunbreaker/SteelAgainstSteel.cs b/BossMod/Modules/Shadowbringers/Quest/Job/Gunbreaker/SteelAgainstSteel.cs new file mode 100644 index 0000000000..ad615b07b9 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Job/Gunbreaker/SteelAgainstSteel.cs @@ -0,0 +1,99 @@ +namespace BossMod.Shadowbringers.Quest.Job.Gunbreaker.SteelAgainstSteel; + +public enum OID : uint +{ + Boss = 0x2A45, + Fustuarium = 0x2AD8, // R0.5 + CullingBlade = 0x2AD3, // R0.5 + IndustrialForce = 0x2BCE, // R0.5 + TerminusEst = 0x2A46, // R1.0 + CaptiveBolt = 0x2AD7, // R0.5 + Helper = 0x233C +} + +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 + Exsanguination1 = 17563, // 2AD4->self, 5.0s cast, range 2-7 180-degree donut segment + Exsanguination2 = 17564, // 2AD5->self, 5.0s cast, range 7-12 180-degree donut segment + Exsanguination3 = 17565, // 2AD6->self, 5.0s cast, range 12-17 180-degree donut segment + 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.SimpleAOEs(module, ActionID.MakeSpell(AID.ToTheSlaughter), new AOEShapeCone(40, 90.Degrees())); +class Exsanguination1(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Exsanguination1), new AOEShapeDonutSector(2, 7, 90.Degrees())); +class Exsanguination2(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Exsanguination2), new AOEShapeDonutSector(7, 12, 90.Degrees())); +class Exsanguination3(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Exsanguination3), new AOEShapeDonutSector(12, 17, 90.Degrees())); +class CaptiveBolt(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.CaptiveBolt), new AOEShapeRect(50, 5), 4); +class AetherochemicalGrenado(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AetherochemicalGrenado), 8); +class DiffractiveLaser(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), new AOEShapeRect(45, 2)); +class SnakeShot(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SnakeShot), new AOEShapeCone(20, 120.Degrees())); +class CullingBlade(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.CullingBlade1), new AOEShapeCone(60, 15.Degrees())); + +class TerminusEst(BossModule module) : Components.GenericAOEs(module) +{ + private Actor? Caster; + private readonly List Actors = []; + private static readonly AOEShapeRect rect = new(40, 2); + + 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(rect, 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 ((AID)spell.Action.ID == AID.TheOrder && Actors.Count > 0) + Caster = caster; + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == 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() + .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/AFeastOfLies.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/AFeastOfLies.cs similarity index 56% rename from BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/AFeastOfLies.cs index f605961e83..1ad83452db 100644 --- a/BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/AFeastOfLies.cs @@ -1,58 +1,70 @@ -namespace BossMod.Shadowbringers.Quest.AFeastOfLies; +namespace BossMod.Shadowbringers.Quest.MSQ.AFeastOfLies; public enum OID : uint { Boss = 0x295A, - Helper = 0x233C, + 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 + NexusOfThunder2 = 16296, // 29E6->self, 6.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 + ShatteredSkyVisual = 17191, // Boss->self, 4.0s cast, single-target + ShatteredSky = 16282, // 29E6->self, 0.5s cast, range 40 circle + MercilessLeftVisual = 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 + MercilessRightVisual = 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 } -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 UnceremoniousBeheading(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.UnceremoniousBeheading), 10); +class KatunCycle(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.KatunCycle), new AOEShapeDonut(5, 40)); + +abstract class Cleaves(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCone(40, 60.Degrees())); +class MercilessRight(BossModule module) : Cleaves(module, AID.MercilessRight1); +class MercilessRight1(BossModule module) : Cleaves(module, AID.MercilessRight2); +class MercilessLeft(BossModule module) : Cleaves(module, AID.MercilessLeft1); +class MercilessLeft1(BossModule module) : Cleaves(module, AID.MercilessLeft2); +class Evisceration(BossModule module) : Cleaves(module, AID.Evisceration); + +class HotPursuit(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); + +abstract class NoT(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(45, 2.5f)); +class NexusOfThunder1(BossModule module) : NoT(module, AID.NexusOfThunder1); +class NexusOfThunder2(BossModule module) : NoT(module, AID.NexusOfThunder2); + +class Burn(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Burn), 8, 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)) +class Electrocution(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Electrocution), 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) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; e.Priority = e == enemy ? 1 : 0; + } } else { @@ -76,13 +88,12 @@ public RanjitStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - ; + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/ComingClean.cs similarity index 63% rename from BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/ComingClean.cs index 6e464d9242..4c5fcd0d3c 100644 --- a/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/ComingClean.cs @@ -1,11 +1,14 @@ using BossMod.QuestBattle; -namespace BossMod.Shadowbringers.Quest.FullSteamAhead; +namespace BossMod.Shadowbringers.Quest.MSQ.ComingClean; // quest name: Full Steam Ahead public enum OID : uint { - Boss = 0x295D, - LightningVoidzone = 0x1E9685 + Boss = 0x295C, // R1.0 + Ranjit = 0x295D, // R1.0 + SerpentHead = 0x295F, // R1.0 + LightningVoidzone = 0x1E9685, // R0.5 + Helper = 0x233C, // R0.5 } public enum AID : uint @@ -14,20 +17,20 @@ public enum AID : uint 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 + NexusOfThunderVisual = 16404, // Boss->self, 3.0s cast, single-target + NexusOfThunder = 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 + VeilOfGukumatz1 = 16423, // 2998->self, no cast, single-target + VeilOfGukumatz2 = 16422, // 295D->self, no cast, single-target + VeilOfGukumatz3 = 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 + MercilessLeftVisual = 16415, // 295D->self, 4.0s cast, single-target + MercilessLeft = 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 @@ -41,17 +44,20 @@ public enum AID : uint public enum SID : uint { Smackdown = 2068, + DownForTheCount = 783 // 295E->player, extra=0xEC7 } -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 KatunCycle(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.KatunCycle), new AOEShapeDonut(5, 40)); -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)); +abstract class Cleaves(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCone(40, 60.Degrees())); +class MercilessLeft(BossModule module) : Cleaves(module, AID.MercilessLeft); +class MercilessRight(BossModule module) : Cleaves(module, AID.MercilessRight); +class Evisceration(BossModule module) : Cleaves(module, AID.Evisceration); + +class UnceremoniousBeheading(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.UnceremoniousBeheading), 10); +class HotPursuit(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); +class NexusOfThunder(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder), new AOEShapeRect(60, 2.5f)); +class CoiledLevin(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.CoiledLevin1), 6); class LightningVoidzone(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.LightningVoidzone).Where(x => x.EventState != 7)); class ThancredAI(BossModule module) : RotationModule(module); @@ -112,15 +118,18 @@ public RanjitStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - ; + .Raw.Update = () => module.Enemies(OID.Ranjit) is var boss && boss.Count != 0 && boss[0].FindStatus(SID.DownForTheCount) != null || module.WorldState.CurrentCFCID != 680; } } [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)) +public class Ranjit(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) { + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(-203, 395), 19.5f, 20)]); + protected override void DrawArenaForeground(int pcSlot, Actor pc) { - Arena.Actors(Enemies(0x295C), ArenaColor.Enemy); + Arena.Actors(Enemies(OID.Ranjit)); + Arena.Actor(PrimaryActor); } } diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P1TelotekGamma.cs similarity index 80% rename from BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P1TelotekGamma.cs index b36a861858..8f560c56ef 100644 --- a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P1TelotekGamma.cs @@ -1,6 +1,6 @@ using BossMod.QuestBattle.Shadowbringers.MSQ; -namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P1; +namespace BossMod.Shadowbringers.Quest.MSQ.DeathUntoDawn.P1; public enum AID : uint { @@ -15,7 +15,7 @@ enum OID : uint 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); +class MRVMissile(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MRVMissile), 12, 6); public class TelotekGammaStates : StateMachineBuilder { @@ -31,5 +31,5 @@ public TelotekGammaStates(BossModule module) : base(module) [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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P2LunarOdin.cs similarity index 63% rename from BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P2LunarOdin.cs index ebc0e28f31..223d7290c4 100644 --- a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P2LunarOdin.cs @@ -1,7 +1,7 @@ using BossMod.QuestBattle; using RID = BossMod.Roleplay.AID; -namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P2; +namespace BossMod.Shadowbringers.Quest.MSQ.DeathUntoDawn.P2; public enum OID : uint { @@ -28,14 +28,14 @@ class UriangerAI(WorldState ws) : UnmanagedRotation(ws, 25) protected override void Exec(Actor? primaryTarget) { - var partyPositions = World.Party.WithoutSlot().Select(p => p.Position).ToList(); + var partyPositions = World.Party.WithoutSlot(false, false).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))) + if (World.Party.WithoutSlot(false, false).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) + if (World.Party.WithoutSlot(false, false).FirstOrDefault(p => p.HPMP.CurHP < p.HPMP.MaxHP * 0.4f) is Actor low) UseAction(RID.Benefic, low); UseAction(RID.MaleficIII, primaryTarget); @@ -58,51 +58,38 @@ protected override void Exec(Actor? primaryTarget) } } +class AutoUrianger(BossModule module) : RotationModule(module); class Fetters(BossModule module) : Components.Adds(module, (uint)OID.Fetters); -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)); } -class LunarGungnir(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.LunarGungnir), 6); +class LunarGungnir1(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 Gungnir(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GungnirAOE), 10); +class Gagnrath(BossModule module) : Components.SimpleAOEs(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); - } -} +abstract class Zantetsuken(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(70, 19.5f)); +class RightZantetsuken(BossModule module) : Zantetsuken(module, AID.RightZantetsuken); +class LeftZantetsuken(BossModule module) : Zantetsuken(module, AID.LeftZantetsuken); public class LunarOdinStates : StateMachineBuilder { public LunarOdinStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter(); + .ActivateOnEnter() + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P3LunarRavana.cs similarity index 85% rename from BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P3LunarRavana.cs index f6bdd8e466..eb22c22965 100644 --- a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P3LunarRavana.cs @@ -1,7 +1,7 @@ using BossMod.QuestBattle; using RID = BossMod.Roleplay.AID; -namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P3; +namespace BossMod.Shadowbringers.Quest.MSQ.DeathUntoDawn.P3; public enum OID : uint { @@ -14,12 +14,12 @@ public enum OID : uint public enum AID : uint { - Explosion = 24046, // 3204->self, 5.0s cast, range 80 width 10 cross + Explosion = 24046 // 3204->self, 5.0s cast, range 80 width 10 cross } public enum SID : uint { - Invincibility = 325, + Invincibility = 325 } class GrahaAI(WorldState ws) : UnmanagedRotation(ws, 25) @@ -72,17 +72,16 @@ protected override void Exec(Actor? primaryTarget) } class AutoGraha(BossModule module) : RotationModule(module); -class DirectionalParry(BossModule module) : Components.DirectionalParry(module, 0x3201) +class DirectionalParry(BossModule module) : Components.DirectionalParry(module, [0x3201]) { + private static readonly Angle a45 = 45.Degrees(); 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)); - } + hints.AddForbiddenZone(ShapeDistance.Cone(Module.PrimaryActor.Position, 100, Module.PrimaryActor.Rotation, a45), WorldState.FutureTime(10)); } } -class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), maxCasts: 2); +class Explosion(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), 2); class LunarRavanaStates : StateMachineBuilder { @@ -98,7 +97,7 @@ public LunarRavanaStates(BossModule module) : base(module) [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 DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P4LunarIfrit.cs similarity index 64% rename from BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P4LunarIfrit.cs index 6201f5dbce..ae2405f1bb 100644 --- a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P4LunarIfrit.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P4; +namespace BossMod.Shadowbringers.Quest.MSQ.DeathUntoDawn.P4; public enum OID : uint { @@ -10,20 +10,20 @@ public enum OID : uint 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 + Hellfire1 = 24058, // Boss->self, 36.0s cast, range 40 circle + Hellfire2 = 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 Hellfire2(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Hellfire2)); 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 RadiantPlume(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RadiantPlume1), 8); +class CrimsonCyclone(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.CrimsonCyclone), new AOEShapeRect(49, 9), 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 Explosion(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), 2); class LunarIfritStates : StateMachineBuilder { @@ -32,8 +32,8 @@ public LunarIfritStates(BossModule module) : base(module) TrivialPhase() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter(); diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Ardbert.cs similarity index 50% rename from BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Ardbert.cs index d70c8c34b0..c32559efa3 100644 --- a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Ardbert.cs @@ -1,11 +1,12 @@ -namespace BossMod.Shadowbringers.Quest.FadedMemories; +namespace BossMod.Shadowbringers.Quest.MSQ.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 Overcome(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Overcome), new AOEShapeCone(8, 60.Degrees()), 2); +class Skydrive(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Skydrive), 5); class SkyHighDrive(BossModule module) : Components.GenericRotatingAOE(module) { Angle angle; + private static readonly AOEShapeRect rect = new(40, 4); public override void OnCastStarted(Actor caster, ActorCastInfo spell) { @@ -20,7 +21,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) 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)); + Sequences.Add(new(rect, spell.LocXZ, spell.Rotation, angle, Module.CastFinishAt(spell, 0.5f), 0.6f, 10, 4)); } break; } @@ -37,10 +38,10 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -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) +class AvalancheAxe1(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe1), 10); +class AvalancheAxe2(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe2), 10); +class AvalancheAxe3(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe3), 10); +class OvercomeAllOdds(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.OvercomeAllOdds), new AOEShapeCone(60, 15.Degrees()), 1) { public override void OnCastFinished(Actor caster, ActorCastInfo spell) { @@ -49,43 +50,37 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) MaxCasts = 2; } } -class Soulflash(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Soulflash1), new AOEShapeCircle(4)); +class Soulflash(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Soulflash1), 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 Soulflash2(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Soulflash2), 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) + if ((AID)spell.Action.ID == 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 - }); + Lines.Add(new() { Next = spell.LocXZ, Advance = spell.Rotation.ToDirection() * 6, 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) + if ((AID)spell.Action.ID is AID.GroundbreakerExaFirst or AID.GroundbreakerExaRest) { - var line = Lines.FirstOrDefault(x => x.Next.AlmostEqual(caster.Position, 1)); - if (line != null) - AdvanceLine(line, caster.Position); + var index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1)); + if (index < 0) + return; + AdvanceLine(Lines[index], caster.Position); + if (Lines[index].ExplosionsLeft == 0) + Lines.RemoveAt(index); } } } -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 GroundbreakerCone(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GroundbreakerCone), new AOEShapeCone(40, 45.Degrees())); +class GroundbreakerDonut(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GroundbreakerDonut), new AOEShapeDonut(5, 20)); +class GroundbreakerCircle(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GroundbreakerCircle), new AOEShapeCircle(15)); class ArdbertStates : StateMachineBuilder { @@ -95,7 +90,7 @@ public ArdbertStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FadedMemories.cs similarity index 96% rename from BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FadedMemories.cs index f7e49aae1f..8944c958e7 100644 --- a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FadedMemories.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.FadedMemories; +namespace BossMod.Shadowbringers.Quest.MSQ.FadedMemories; public enum OID : uint { @@ -7,7 +7,7 @@ public enum OID : uint Nidhogg = 0x2F21, Zenos = 0x2F28, Ardbert = 0x2F2E, - Helper = 0x233C, + Helper = 0x233C } public enum AID : uint diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FlameGeneralAldynn.cs similarity index 62% rename from BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FlameGeneralAldynn.cs index 54d236eee2..16c9ff7bfe 100644 --- a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FlameGeneralAldynn.cs @@ -1,18 +1,19 @@ -namespace BossMod.Shadowbringers.Quest.FadedMemories; +namespace BossMod.Shadowbringers.Quest.MSQ.FadedMemories; -class FlamingTizona(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FlamingTizona), 6); +class FlamingTizona(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FlamingTizona), 6); class FlameGeneralAldynnStates : StateMachineBuilder { public FlameGeneralAldynnStates(BossModule module) : base(module) { - TrivialPhase().ActivateOnEnter(); + 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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/KingThordan.cs similarity index 72% rename from BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/KingThordan.cs index 4eb731e55c..45b8e8a317 100644 --- a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/KingThordan.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.FadedMemories; +namespace BossMod.Shadowbringers.Quest.MSQ.FadedMemories; class DragonsGaze(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.TheDragonsGaze)); @@ -6,18 +6,22 @@ class KingThordanStates : StateMachineBuilder { public KingThordanStates(BossModule module) : base(module) { - TrivialPhase().ActivateOnEnter(); + 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 DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var h in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var h = hints.PotentialTargets[i]; 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/MSQ/FadedMemories/Nidhogg.cs similarity index 50% rename from BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Nidhogg.cs index a7cc3c74c6..9a40b095ad 100644 --- a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Nidhogg.cs @@ -1,13 +1,15 @@ -namespace BossMod.Shadowbringers.Quest.FadedMemories; +namespace BossMod.Shadowbringers.Quest.MSQ.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 HighJump(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HighJump), 8); +class Geirskogul(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Geirskogul), 62, 4); class NidhoggStates : StateMachineBuilder { public NidhoggStates(BossModule module) : base(module) { - TrivialPhase().ActivateOnEnter().ActivateOnEnter(); + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Zenos.cs similarity index 61% rename from BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Zenos.cs index 848cdcc2a3..1d8f589817 100644 --- a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Zenos.cs @@ -1,9 +1,9 @@ -namespace BossMod.Shadowbringers.Quest.FadedMemories; +namespace BossMod.Shadowbringers.Quest.MSQ.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 EntropicFlame(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.EntropicFlame), new AOEShapeRect(50, 4)); +class VeinSplitter(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), 10); class ZenosYaeGalvusStates : StateMachineBuilder { @@ -12,8 +12,7 @@ public ZenosYaeGalvusStates(BossModule module) : base(module) TrivialPhase() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - ; + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/Shadowbringers/Quest/MSQ/TheGreatShipVylbrand.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/TheGreatShipVylbrand.cs new file mode 100644 index 0000000000..5661b74600 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/TheGreatShipVylbrand.cs @@ -0,0 +1,207 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.TheGreatShipVylbrand; + +public enum OID : uint +{ + Boss = 0x3187, // R0.5 + SecondOrderRocksplitter = 0x3107, // R1.08 + SecondOrderRoundsman = 0x3102, // R0.9 + SecondOrderPickman = 0x3100, // R0.9 + SecondOrderAlchemist = 0x3101, // R0.9 + OghomoroGolem = 0x3103, // R1.1 + Construct2 = 0x3104, // R3.2 + Bomb = 0x3105, // R0.9 + Grenade1 = 0x318C, // R1.8-3.6 + Grenade2 = 0x3106, // R1.8 + ChannelAether = 0x1EB0F7, // R0.5 + Alphinaud = 0x30FE, // R0.500, x1 + Helper = 0x233C // R0.5 +} + +public enum AID : uint +{ + AutoAttack1 = 6499, // SecondOrderRocksplitter->player, no cast, single-target + AutoAttack2 = 6497, // SecondOrderRoundsman/SecondOrderPickman/Construct2->allies, no cast, single-target + Teleport = 22947, // Construct2->location, no cast, single-target + + KoboldDrill = 22967, // SecondOrderRocksplitter->player, 4.0s cast, single-target + BulldozeTelegraph1 = 22955, // SecondOrderRocksplitter->location, 8.0s cast, width 6 rect charge + BulldozeTelegraph2 = 22957, // Helper->location, 8.0s cast, width 6 rect charge + Bulldoze = 22956, // SecondOrderRocksplitter->location, no cast, width 6 rect charge + TunnelShakerVisual = 22958, // SecondOrderRocksplitter->self, 5.0s cast, single-target + TunnelShaker1 = 22959, // Helper->self, 5.0s cast, range 60 30-degree cone + StrataSmasher = 22960, // SecondOrderRocksplitter->location, no cast, range 60 circle + Uplift1 = 22961, // Helper->self, 6.0s cast, range 10 circle + Uplift2 = 22962, // Helper->self, 8.0s cast, range 10-20 donut + Uplift3 = 22963, // Helper->self, 10.0s cast, range 20-30 donut + + Stone = 21588, // SecondOrderAlchemist->allies, 1.0s cast, single-target + + Breakthrough = 22948, // Construct2->ally, 11.0s cast, width 8 rect charge, wild charges + + TenTrolleyWallop = 22950, // Construct2->self, 6.0s cast, range 40 60-degree cone + TenTrolleyTorque = 22949, // Construct2->self, 6.0s cast, range 16 circle + TenTrolleyTap = 23362, // Construct2->self, 3.5s cast, range 8 120-degree cone + ExplosiveChemistry = 23497, // Grenade1/Grenade2->self, 12.0s cast, single-target + SelfDestructVisual = 23500, // Grenade2->self, no cast, single-target + SelfDestruct1 = 22952, // Grenade1/Grenade2->self, no cast, range 6 circle + SelfDestruct2 = 23501, // Boss->self, 3.5s cast, range 10 circle + + Quakedown = 22953, // SecondOrderRocksplitter->location, no cast, range 60 circle, phase transition, excavate happens while player is stunned and thus useles to draw + ExcavateVisual = 23132, // SecondOrderRocksplitter->self, 17.0s cast, single-target + Excavate = 22954 // SecondOrderRocksplitter->ally, no cast, width 6 rect charge +} + +public enum TetherID : uint +{ + BombTether = 97 // Grenade2->Alphinaud +} + +class TenTrolleyTorque(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TenTrolleyTorque), 16); +class TenTrolleyTap(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TenTrolleyTap), new AOEShapeCone(8, 60.Degrees())); +class TenTrolleyWallop(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TenTrolleyWallop), new AOEShapeCone(40, 30.Degrees())); +class SelfDestruct2(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SelfDestruct2), 10); + +class Bulldoze(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = new(4); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var count = _aoes.Count; + if (count == 0) + return []; + var aoes = new AOEInstance[count]; + for (var i = 0; i < count; ++i) + { + var aoe = _aoes[i]; + if (i == 0) + aoes[i] = count > 1 ? aoe with { Color = Colors.Danger } : aoe; + else + aoes[i] = aoe; + } + return aoes; + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.BulldozeTelegraph2) + { + var dir = spell.LocXZ - caster.Position; + _aoes.Add(new(new AOEShapeRect(dir.Length(), 3), caster.Position, Angle.FromDirection(dir), Module.CastFinishAt(spell))); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (_aoes.Count != 0 && (AID)spell.Action.ID is AID.BulldozeTelegraph1 or AID.Bulldoze) + _aoes.RemoveAt(0); + } +} + +class TunnelShaker(BossModule module) : Components.SimpleAOEs(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 ((AID)spell.Action.ID == AID.Uplift1) + AddSequence(spell.LocXZ, Module.CastFinishAt(spell)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (Sequences.Count != 0) + { + var order = (AID)spell.Action.ID switch + { + AID.Uplift1 => 0, + AID.Uplift2 => 1, + AID.Uplift3 => 2, + _ => -1 + }; + AdvanceSequence(order, spell.LocXZ, WorldState.FutureTime(2)); + } + } +} + +class BombTether(BossModule module) : Components.InterceptTetherAOE(module, ActionID.MakeSpell(AID.SelfDestruct1), (uint)TetherID.BombTether, 6) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Tethers.Count != 0) + { + base.AddAIHints(slot, actor, assignment, hints); + var tether = Tethers[0]; + if (tether.Player != Module.Raid.Player()) + { + var source = tether.Enemy; + var target = Module.Enemies(OID.Alphinaud)[0]; + hints.AddForbiddenZone(ShapeDistance.InvertedRect(target.Position + (target.HitboxRadius + 0.1f) * target.DirectionTo(source), source.Position, 0.6f), Activation); + } + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + base.OnTethered(source, tether); + if (Activation != default && tether.ID == TID) + Activation = WorldState.FutureTime(15); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action == WatchedAction) + Activation = default; + } +} + +public class SecondOrderRocksplitterStates : StateMachineBuilder +{ + public SecondOrderRocksplitterStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.Enemies(OID.SecondOrderRocksplitter) is var boss && boss.Count != 0 && boss[0].HPMP.CurHP == 1 || module.WorldState.CurrentCFCID != 764; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69551)] +public class SecondOrderRocksplitter(WorldState ws, Actor primary) : BossModule(ws, primary, default, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Polygon(default, 26.5f, 24)]); + private static readonly uint[] opponents = [(uint)OID.Grenade1, (uint)OID.Grenade2, (uint)OID.SecondOrderRoundsman, (uint)OID.SecondOrderRocksplitter, (uint)OID.SecondOrderPickman, + (uint)OID.SecondOrderAlchemist, (uint)OID.Bomb, (uint)OID.Construct2, (uint)OID.OghomoroGolem]; + + protected override bool CheckPull() => Raid.Player()!.InCombat; + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(Enemies(opponents)); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var aether = Enemies(OID.ChannelAether); + var aethercount = aether.Count; + if (aethercount != 0) + for (var i = 0; i < aethercount; ++i) + { + var interact = aether[i]; + if (interact.IsTargetable) + { + hints.InteractWithTarget = interact; + break; + } + } + + if (Enemies(OID.Grenade2).Count != 0) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + if ((OID)e.Actor.OID == OID.Grenade2) + e.Priority = AIHints.Enemy.PriorityPointless; + } + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/TheOracleOfLight.cs similarity index 67% rename from BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs rename to BossMod/Modules/Shadowbringers/Quest/MSQ/TheOracleOfLight.cs index af4008c356..9dd39210c6 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/TheOracleOfLight.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.TheOracleOfLight; +namespace BossMod.Shadowbringers.Quest.MSQ.TheOracleOfLight; public enum OID : uint { @@ -15,10 +15,13 @@ public enum AID : uint 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 HotPursuit(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); + +abstract class NoT(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(60.5f, 2.5f)); +class NexusOfThunder1(BossModule module) : NoT(module, AID.NexusOfThunder1); +class NexusOfThunder2(BossModule module) : NoT(module, AID.NexusOfThunder2); + +class Burn(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Burn), 8, 8); class UnbridledWrath(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.UnbridledWrath), 20, kind: Kind.DirForward, stopAtWall: true); class RanjitStates : StateMachineBuilder @@ -37,4 +40,3 @@ public RanjitStates(BossModule module) : base(module) [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/MSQ/VowsOfVitrueDeedsOfCruelty.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs index 180476c5a6..e991d9b645 100644 --- a/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs @@ -1,19 +1,15 @@ -<<<<<<<< HEAD:BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs -namespace BossMod.Shadowbringers.Quest.MSQ.VowsOfVitrueDeedsOfCruelty; -======== -using BossMod.QuestBattle; +using BossMod.QuestBattle; -namespace BossMod.Shadowbringers.Quest.VowsOfVirtueDeedsOfCruelty; ->>>>>>>> merge:BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs +namespace BossMod.Shadowbringers.Quest.MSQ.VowsOfVirtueDeedsOfCruelty; public enum OID : uint { - Boss = 0x2C85, // R6.000, x1 - TerminusEstVisual = 0x2C98, // R1.000, x3 - 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) - BossHelper = 0x233C + Boss = 0x2C85, // R6.0 + TerminusEstVisual = 0x2C98, // R1.0 + SigniferPraetorianus = 0x2C9A, // R0.5 + LembusPraetorianus = 0x2C99, // R2.4 + MagitekBit = 0x2C9C, // R0.6 + Helper = 0x233C } public enum AID : uint @@ -26,15 +22,15 @@ public enum AID : uint 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 + TerminusEstLocationHelper = 18889, // Helper->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 + MetalCutter = 18794, // Helper->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 + AtomicRay = 18796, // Helper->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 } @@ -128,22 +124,21 @@ public ArchUltimaStates(BossModule module) : base(module) } [ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "croizat", GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69218, NameID = 9189)] -<<<<<<<< HEAD:BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs -public class VowsOfVirtueDeedsOfCruelty(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(19.5f)); -======== -public class ArchUltima(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(20)) +public class ArchUltima(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(19.5f)) { protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var h in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var h = hints.PotentialTargets[i]; 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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } ->>>>>>>> merge:BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs diff --git a/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs b/BossMod/Modules/Shadowbringers/Quest/Role/ATearfulReunion.cs similarity index 76% rename from BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/ATearfulReunion.cs index 41bf920c9b..b6a5ac7e29 100644 --- a/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/ATearfulReunion.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.ATearfulReunion; +namespace BossMod.Shadowbringers.Quest.Role.ATearfulReunion; public enum OID : uint { @@ -18,10 +18,10 @@ public enum AID : uint 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 SanctifiedBlizzardIV(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardIV), new AOEShapeDonut(5, 20)); +class SanctifiedBlizzardII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardII), 5); +class SanctifiedFireIII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedFireIII), 6); +class SanctifiedBlizzardIII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardIII), new AOEShapeCone(40.5f, 22.5f.Degrees())); 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); @@ -30,9 +30,9 @@ class SanctifiedFlare(BossModule module) : Components.StackWithCastTargets(modul 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) + if (ActiveStacks.Count != 0 && WorldState.Actors.First(x => x.OID == 0x29C3) is Actor cerigg) { - hints.AddForbiddenZone(new AOEShapeDonut(6, 100), cerigg.Position, default, ActiveStacks.First().Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(cerigg.Position, 6), ActiveStacks.First().Activation); } } } @@ -51,7 +51,7 @@ public override void OnTethered(Actor source, ActorTetherInfo tether) public override void DrawArenaForeground(int pcSlot, Actor pc) { foreach (var b in Balls) - Arena.AddLine(pc.Position, b.Position, ArenaColor.Danger); + Arena.AddLine(pc.Position, b.Position, Colors.Danger); } public override void Update() @@ -64,6 +64,8 @@ public override void Update() var closestBall = Balls.OrderBy(player.DistanceToHitbox).FirstOrDefault(); Modify(closestBall?.Position, Hollows); + Safezones.Clear(); + AddSafezone(NextExplosion, default); } public override void AddHints(int slot, Actor actor, TextHints hints) @@ -90,8 +92,7 @@ public PhronesisStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - ; + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs b/BossMod/Modules/Shadowbringers/Quest/Role/CourageBornOfFear.cs similarity index 73% rename from BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/CourageBornOfFear.cs index e6976f2ef1..83421c72f8 100644 --- a/BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/CourageBornOfFear.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.CourageBornOfFear; +namespace BossMod.Shadowbringers.Quest.Role.CourageBornOfFear; public enum OID : uint { @@ -20,19 +20,19 @@ public enum AID : uint 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 + BodkinVolley = 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 ArrowOfFortitude(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ArrowOfFortitude), new AOEShapeRect(30, 4)); +class BodkinVolley(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.BodkinVolley), 5, minStackSize: 1); +class RainOfLight(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RainOfLight), 4); +class ThePathOfLight(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ThePathOfLight), 15); +class InquisitorsBlade(BossModule module) : Components.SimpleAOEs(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 MythrilCycloneDonut(BossModule module) : Components.SimpleAOEs(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 UncloudedAscension(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.UncloudedAscension1), 10); +class Overcome(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Overcome), new AOEShapeCone(8.5f, 60.Degrees())); class SanctifiedFireII(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(5), 23, centerAtTarget: true) { @@ -92,11 +92,14 @@ public ImmaculateWarriorStates(BossModule module) : base(module) [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 DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var h in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var h = hints.PotentialTargets[i]; h.Priority = h.Actor.TargetID == actor.InstanceID ? 1 : 0; + } } } diff --git a/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs b/BossMod/Modules/Shadowbringers/Quest/Role/NyelbertsLament.cs similarity index 83% rename from BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/NyelbertsLament.cs index f8bbffa5d0..0e85afc0d8 100644 --- a/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/NyelbertsLament.cs @@ -1,13 +1,14 @@ using BossMod.QuestBattle.Shadowbringers.RoleQuests; -namespace BossMod.Shadowbringers.Quest.NyelbertsLament; +namespace BossMod.Shadowbringers.Quest.Role.NyelbertsLament; public enum OID : uint { Boss = 0x2977, - Helper = 0x233C, + BovianBull = 0x2976, - LooseBoulder = 0x2978, // R2.400, x0 (spawn during fight) + LooseBoulder = 0x2978, // R2.4 + Helper = 0x233C } public enum AID : uint @@ -57,16 +58,18 @@ private void Refresh() var blockers = Module.Enemies(OID.LooseBoulder); Modify(ActiveCaster?.CastInfo?.LocXZ, blockers.Select(b => (b.Position, b.HitboxRadius)), Module.CastFinishAt(ActiveCaster?.CastInfo)); + Safezones.Clear(); + AddSafezone(NextExplosion, default); } } -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) +class FallingRock(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FallingRock), 4); +class ZoomIn(BossModule module) : Components.LineStack(module, ActionID.MakeSpell(AID.ZoomTargetSelect), ActionID.MakeSpell(AID.ZoomIn), 5.1f, 42) { 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); + if (ActiveBaits.Any()) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center, 3), ActiveBaits.FirstOrDefault().Activation); } } @@ -84,7 +87,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme 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); + Arena.ZoneCone(Paladin.Position, 0, 8, Paladin.Rotation + 180.Degrees(), 60.Degrees(), Colors.SafeFromAOE); } public override void AddHints(int slot, Actor actor, TextHints hints) @@ -112,5 +115,5 @@ public BovianStates(BossModule module) : base(module) [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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheHardenedHeart.cs similarity index 90% rename from BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/TheHardenedHeart.cs index 9c23a90128..586a95e049 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheHardenedHeart.cs @@ -1,6 +1,6 @@ using BossMod.QuestBattle; -namespace BossMod.Shadowbringers.Quest.TheHardenedHeart; +namespace BossMod.Shadowbringers.Quest.Role.TheHardenedHeart; public enum OID : uint { @@ -27,7 +27,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo 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 AbyssalCharge(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AbyssalCharge1), new AOEShapeRect(40, 2)); class AutoBranden(WorldState ws) : UnmanagedRotation(ws, 3) { @@ -93,7 +93,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) public override void DrawArenaBackground(int pcSlot, Actor pc) { if (DwarfTether is Tether t) - Arena.AddLine(t.Source.Position, t.Target.Position, ArenaColor.Danger); + Arena.AddLine(t.Source.Position, t.Target.Position, Colors.Danger); } public override void OnEventCast(Actor caster, ActorCastEvent spell) @@ -105,7 +105,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) class BrandenAI(BossModule module) : RotationModule(module); -class RustingClaw(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RustingClaw), new AOEShapeCone(10.3f, 45.Degrees())); +class RustingClaw(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RustingClaw), new AOEShapeCone(10.3f, 45.Degrees())); class TadricTheVaingloriousStates : StateMachineBuilder { @@ -124,12 +124,13 @@ public TadricTheVaingloriousStates(BossModule module) : base(module) [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 DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var h in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var h = hints.PotentialTargets[i]; 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) { @@ -140,4 +141,3 @@ protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRoles } } } - diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheHuntersLegacy.cs similarity index 71% rename from BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/TheHuntersLegacy.cs index 4aa94e49a9..b4d97cf589 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheHuntersLegacy.cs @@ -1,6 +1,6 @@ using BossMod.QuestBattle; -namespace BossMod.Shadowbringers.Quest.TheHuntersLegacy; +namespace BossMod.Shadowbringers.Quest.Role.TheHuntersLegacy; public enum OID : uint { @@ -22,13 +22,16 @@ public enum AID : uint } 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)); + +abstract class BB(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCone(38.05f, 135.Degrees())); +class BalamBlaster(BossModule module) : BB(module, AID.BalamBlaster); +class BalamBlasterRear(BossModule module) : BB(module, AID.BalamBlasterRear); + +class ElectricWhisker(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ElectricWhisker), new AOEShapeCone(16.05f, 45.Degrees())); +class RoaringThunder(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RoaringThunder), new AOEShapeDonut(8, 30)); +class StreakLightning(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.StreakLightning), 3); +class StreakLightning1(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.StreakLightning1), 3); +class AlternatingCurrent(BossModule module) : Components.SimpleAOEs(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) @@ -59,13 +62,13 @@ class RonkanAura(BossModule module) : BossComponent(module) public override void DrawArenaBackground(int pcSlot, Actor pc) { if (AuraCenter is Actor a) - Arena.ZoneCircle(a.Position, 10, ArenaColor.SafeFromAOE); + Arena.ZoneCircle(a.Position, 10, Colors.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)); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(a.Position, 10), activation: WorldState.FutureTime(5)); } } diff --git a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Sophrosyne.cs similarity index 83% rename from BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Sophrosyne.cs index f6a259691a..4e3664b59b 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Sophrosyne.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.TheLostAndTheFound.Sophrosyne; +namespace BossMod.Shadowbringers.Quest.Role.TheLostAndTheFound.Sophrosyne; public enum OID : uint { @@ -25,5 +25,5 @@ public SophrosyneStates(BossModule module) : base(module) [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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Yxtlilton.cs similarity index 79% rename from BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Yxtlilton.cs index d6806f5362..a43e1f15b9 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Yxtlilton.cs @@ -1,11 +1,11 @@ using BossMod.QuestBattle; -namespace BossMod.Shadowbringers.Quest.TheLostAndTheFound.Yxtlilton; +namespace BossMod.Shadowbringers.Quest.Role.TheLostAndTheFound.Yxtlilton; public enum OID : uint { Boss = 0x29B0, - Helper = 0x233C, + Helper = 0x233C } public enum AID : uint @@ -20,8 +20,8 @@ class CodexOfGravity(BossModule module) : Components.StackWithCastTargets(module 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); + if (Stacks.Count != 0) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center, 1.5f), Stacks[0].Activation); } } @@ -32,9 +32,19 @@ protected override void Exec(Actor? primaryTarget) if (primaryTarget == null) return; - var party = World.Party.WithoutSlot().ToList(); + var party = World.Party.WithoutSlot(false, false); - Hints.GoalZones.Add(p => party.Count(act => act.Position.InCircle(p, 15 + Player.HitboxRadius + act.HitboxRadius))); + Hints.GoalZones.Add(p => + { + var count = 0; + for (var i = 0; i < party.Length; ++i) + { + var act = party[i]; + if (act.Position.InCircle(p, 15 + Player.HitboxRadius + act.HitboxRadius)) + count++; + } + return count; + }); var lowest = party.MinBy(p => p.PredictedHPRatio)!; var esunable = party.FirstOrDefault(x => x.FindStatus(482) != null); @@ -83,5 +93,5 @@ public YxtliltonStates(BossModule module) : base(module) [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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheSoulOfTemperance.cs similarity index 75% rename from BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/TheSoulOfTemperance.cs index 6de41c42a6..cf67e50ab8 100644 --- a/BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheSoulOfTemperance.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.TheSoulOfTemperance; +namespace BossMod.Shadowbringers.Quest.Role.TheSoulOfTemperance; public enum OID : uint { @@ -20,16 +20,16 @@ public enum AID : uint 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 SanctifiedHoly1(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedHoly1), 8); +class SanctifiedHoly2(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedHoly2), 6); +class ForceOfRestraint(BossModule module) : Components.SimpleAOEs(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 TemperedVirtue(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TemperedVirtue), 15); +class WaterAndWine(BossModule module) : Components.SimpleAOEs(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 SanctifiedAero(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedAero1), new AOEShapeRect(40.5f, 3)); class Repose(BossModule module) : BossComponent(module) { @@ -72,5 +72,5 @@ public SophrosyneStates(BossModule module) : base(module) [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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs b/BossMod/Modules/Shadowbringers/Quest/Role/ToHaveLovedAndLost.cs similarity index 65% rename from BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs rename to BossMod/Modules/Shadowbringers/Quest/Role/ToHaveLovedAndLost.cs index d332164bac..1c47c0cd78 100644 --- a/BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs +++ b/BossMod/Modules/Shadowbringers/Quest/Role/ToHaveLovedAndLost.cs @@ -1,4 +1,4 @@ -namespace BossMod.Shadowbringers.Quest.ToHaveLovedAndLost; +namespace BossMod.Shadowbringers.Quest.Role.ToHaveLovedAndLost; public enum OID : uint { @@ -26,35 +26,41 @@ public enum AID : uint 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 HereticsFork(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HereticsFork), new AOEShapeCross(40, 3)); +class SpiritsWithout(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SpiritsWithout), new AOEShapeRect(3.5f, 1.5f)); +class SeraphBlade(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SeraphBlade), new AOEShapeCone(40, 90.Degrees())); +class HereticsQuoit(BossModule module) : Components.SimpleAOEs(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); + private readonly HashSet casts = [AID.Fracture, AID.Fracture1, AID.Fracture2, AID.Fracture3, AID.Fracture4, AID.Fracture5]; public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if (IsTower(spell.Action)) + if (casts.Contains((AID)spell.Action.ID)) 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)); + if (casts.Contains((AID)spell.Action.ID)) + for (var i = 0; i < Towers.Count; ++i) + { + var tower = Towers[i]; + if (tower.Position == spell.LocXZ) + { + Towers.Remove(tower); + break; + } + } } } -class Bloodstain(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Bloodstain), new AOEShapeCircle(5)); +class Bloodstain(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Bloodstain), 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 SanctifiedHolyII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedHolyII), 5); +class SanctifiedHolyIII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedHolyIII), 6); class DikaiosyneStates : StateMachineBuilder { diff --git a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs deleted file mode 100644 index a0ec6bca18..0000000000 --- a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index dfd2b3530e..0000000000 --- a/BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs +++ /dev/null @@ -1,128 +0,0 @@ -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 deleted file mode 100644 index 44d334849b..0000000000 --- a/BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs +++ /dev/null @@ -1,116 +0,0 @@ -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/SleepNowInSapphire/P1GuidanceSystem.cs b/BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P1GuidanceSystem.cs similarity index 79% rename from BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs rename to BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P1GuidanceSystem.cs index 9395f1677e..abec4011ef 100644 --- a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs +++ b/BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P1GuidanceSystem.cs @@ -1,6 +1,6 @@ using BossMod.QuestBattle.Shadowbringers.SideQuests; -namespace BossMod.Shadowbringers.Quest.SleepNowInSapphire.P1GuidanceSystem; +namespace BossMod.Shadowbringers.Quest.SorrowOfWerlyt.SleepNowInSapphire.P1GuidanceSystem; public enum OID : uint { @@ -13,7 +13,7 @@ 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 AerialBombardment(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AerialBombardment), 12); class GWarrior(BossModule module) : QuestBattle.RotationModule(module); @@ -28,7 +28,7 @@ public GuidanceSystemStates(BossModule module) : base(module) } [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)) +public class GuidanceSystem(WorldState ws, Actor primary) : BossModule(ws, primary, new(-15, 610), new ArenaBoundsSquare(60)) { protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { diff --git a/BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P2SapphireWeapon.cs b/BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P2SapphireWeapon.cs new file mode 100644 index 0000000000..2f710aa131 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P2SapphireWeapon.cs @@ -0,0 +1,70 @@ +using BossMod.Shadowbringers.Quest.SorrowOfWerlyt.SleepNowInSapphire.P1GuidanceSystem; + +namespace BossMod.Shadowbringers.Quest.SorrowOfWerlyt.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 21-60 donut + MagitekSpread = 20336, // RegulasImage->self, 5.0s cast, range 43 240-degree cone + SideraysRight = 20329, // Helper->self, 8.0s cast, range 128 90-degree cone + SideraysLeft = 21021, // Helper->self, 8.0s cast, range 128 90-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.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekRay), new AOEShapeRect(100, 3)); +class ServantRoar(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ServantRoar), new AOEShapeRect(100, 4)); +class TailSwing(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TailSwing), new AOEShapeCircle(46)); +class OptimizedJudgment(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.OptimizedJudgment), new AOEShapeDonut(21, 60)); +class MagitekSpread(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekSpread), new AOEShapeCone(43, 120.Degrees())); +class SapphireRay(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SapphireRay), new AOEShapeRect(120, 20)); + +abstract class Siderays(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCone(128, 45.Degrees())); +class SideraysLeft(BossModule module) : Siderays(module, AID.SideraysLeft); +class SideraysRight(BossModule module) : Siderays(module, AID.SideraysRight); + +class TheSapphireWeaponStates : StateMachineBuilder +{ + public TheSapphireWeaponStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .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)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var h = hints.PotentialTargets[i]; + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } + } +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs b/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs deleted file mode 100644 index 180476c5a6..0000000000 --- a/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs +++ /dev/null @@ -1,149 +0,0 @@ -<<<<<<<< HEAD:BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs -namespace BossMod.Shadowbringers.Quest.MSQ.VowsOfVitrueDeedsOfCruelty; -======== -using BossMod.QuestBattle; - -namespace BossMod.Shadowbringers.Quest.VowsOfVirtueDeedsOfCruelty; ->>>>>>>> merge:BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs - -public enum OID : uint -{ - Boss = 0x2C85, // R6.000, x1 - TerminusEstVisual = 0x2C98, // R1.000, x3 - 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) - BossHelper = 0x233C -} - -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 -} - -abstract class MagitekRay(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(45, 4)); -class MagitekRayRightArm(BossModule module) : MagitekRay(module, AID.MagitekRayRightArm); -class MagitekRayLeftArm(BossModule module) : MagitekRay(module, AID.MagitekRayLeftArm); - -class AngrySalamander(BossModule module) : Components.SimpleAOEs(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, spell.LocXZ, spell.Rotation, Module.CastFinishAt(spell)), - new(_shape, spell.LocXZ, spell.Rotation - 90.Degrees(), Module.CastFinishAt(spell)), - new(_shape, spell.LocXZ, 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.SimpleAOEs(module, ActionID.MakeSpell(AID.TerminusEstLocationHelper), 3); -class FireII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FireII), 5); -class GarleanFire(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GarleanFire), 5); -class MetalCutter(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MetalCutter), new AOEShapeCone(30, 10.Degrees())); -class MagitekRayBits(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekRayBit), new AOEShapeRect(50, 1)); -class AtomicRay(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AtomicRay), 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) : 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)] -<<<<<<<< HEAD:BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs -public class VowsOfVirtueDeedsOfCruelty(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(19.5f)); -======== -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); -} ->>>>>>>> merge:BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs diff --git a/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/SecretKeeper.cs b/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/SecretKeeper.cs index 41099a1090..aedd4cb489 100644 --- a/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/SecretKeeper.cs +++ b/BossMod/Modules/Shadowbringers/TreasureHunt/TheShiftingOubliettesOfLyheGhiah/SecretKeeper.cs @@ -31,7 +31,7 @@ public enum AID : uint class InhalePull(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.InhaleBoss), 20, false, 1, new AOEShapeCone(20, 60.Degrees()), Kind.TowardsOrigin, default, true); class HeavyScrapline(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HeavyScrapline), 11); class MoldyPhlegm(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 6, ActionID.MakeSpell(AID.MoldyPhlegm), m => m.Enemies(OID.ResinVoidzone).Where(z => z.EventState != 7), 1.4f); -class MoldySneeze(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.MoldySneeze), new AOEShapeCone(12, 60.Degrees()), (uint)OID.Boss); +class MoldySneeze(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.MoldySneeze), new AOEShapeCone(12, 60.Degrees())); class Spin(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Spin), 11); class Mash(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Mash), new AOEShapeRect(13, 2)); diff --git a/BossMod/Modules/Shadowbringers/Ultimate/TEA/TEA.cs b/BossMod/Modules/Shadowbringers/Ultimate/TEA/TEA.cs index 180f29f353..295bae529a 100644 --- a/BossMod/Modules/Shadowbringers/Ultimate/TEA/TEA.cs +++ b/BossMod/Modules/Shadowbringers/Ultimate/TEA/TEA.cs @@ -1,7 +1,7 @@ namespace BossMod.Shadowbringers.Ultimate.TEA; class P1FluidSwing(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.FluidSwing), new AOEShapeCone(11.5f, 45.Degrees())); -class P1FluidStrike(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.FluidStrike), new AOEShapeCone(11.6f, 45.Degrees()), (uint)OID.LiquidHand); +class P1FluidStrike(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.FluidStrike), new AOEShapeCone(11.6f, 45.Degrees()), [(uint)OID.LiquidHand]); class P1Sluice(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Sluice), 5); class P1Splash(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Splash)); class P1Drainage(BossModule module) : Components.TankbusterTether(module, ActionID.MakeSpell(AID.DrainageP1), (uint)TetherID.Drainage, 6); @@ -19,7 +19,7 @@ class P2PropellerWind(BossModule module) : Components.CastLineOfSightAOE(module, class P2DoubleRocketPunch(BossModule module) : Components.CastSharedTankbuster(module, ActionID.MakeSpell(AID.DoubleRocketPunch), 3); class P3ChasteningHeat(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.ChasteningHeat), new AOEShapeCircle(5), true); -class P3DivineSpear(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.DivineSpear), new AOEShapeCone(24.2f, 45.Degrees()), (uint)OID.AlexanderPrime); // TODO: verify angle +class P3DivineSpear(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.DivineSpear), new AOEShapeCone(24.2f, 45.Degrees()), [(uint)OID.AlexanderPrime]); // TODO: verify angle class P3DivineJudgmentRaidwide(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.DivineJudgmentRaidwide)); [ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 694, PlanLevel = 80)] diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs index 3b128df505..93ff16ea4d 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs @@ -46,7 +46,7 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) class IvoryPalmExplosion(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.Explosion), "Ivory Palm is enraging!", true); -class EurekanAero(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.EurekanAero), new AOEShapeCone(6, 60.Degrees()), (uint)OID.IvoryPalm) +class EurekanAero(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.EurekanAero), new AOEShapeCone(6, 60.Degrees()), [(uint)OID.IvoryPalm]) { public override List<(Actor origin, Actor target, Angle angle)> OriginsAndTargets() { diff --git a/BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs b/BossMod/Modules/Stormblood/Quest/Job/DarkKnight/TheOrphansAndTheBrokenBlade.cs similarity index 77% rename from BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs rename to BossMod/Modules/Stormblood/Quest/Job/DarkKnight/TheOrphansAndTheBrokenBlade.cs index 0bc2115ab8..96a5b4ec54 100644 --- a/BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/DarkKnight/TheOrphansAndTheBrokenBlade.cs @@ -1,9 +1,9 @@ -namespace BossMod.Stormblood.Quest.TheOrphansAndTheBrokenBlade; +namespace BossMod.Stormblood.Quest.Job.DarkKnight.TheOrphansAndTheBrokenBlade; public enum OID : uint { Boss = 0x1C5E, - Helper = 0x233C, + Helper = 0x233C } public enum AID : uint @@ -11,10 +11,10 @@ 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 + 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 SpiralHell(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SpiralHell), new AOEShapeRect(40, 2)); class HeadsmansDelight(BossModule module) : Components.GenericStackSpread(module) { public override void OnCastStarted(Actor caster, ActorCastInfo spell) @@ -29,7 +29,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) Stacks.Clear(); } } -class ShadowOfDeath(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ShadowOfDeath1), 5); +class ShadowOfDeath(BossModule module) : Components.SimpleAOEs(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) @@ -53,5 +53,5 @@ public OmpagneDeepblackStates(BossModule module) : base(module) [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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Stormblood/Quest/DragonSound.cs b/BossMod/Modules/Stormblood/Quest/Job/Dragoon/DragonSound.cs similarity index 86% rename from BossMod/Modules/Stormblood/Quest/DragonSound.cs rename to BossMod/Modules/Stormblood/Quest/Job/Dragoon/DragonSound.cs index 80b77ec816..e26f12d59c 100644 --- a/BossMod/Modules/Stormblood/Quest/DragonSound.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/Dragoon/DragonSound.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Quest.DragonSound; +namespace BossMod.Stormblood.Quest.Job.Dragoon.DragonSound; public enum OID : uint { @@ -18,8 +18,8 @@ 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 AbyssicBuster(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AbyssicBuster), new AOEShapeCone(31.84f, 45.Degrees())); +class Heavensfall(BossModule module) : Components.SimpleAOEs(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 diff --git a/BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs b/BossMod/Modules/Stormblood/Quest/Job/Monk/ThePowerToProtect.cs similarity index 79% rename from BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs rename to BossMod/Modules/Stormblood/Quest/Job/Monk/ThePowerToProtect.cs index ce71d1b239..4045d102e9 100644 --- a/BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/Monk/ThePowerToProtect.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Quest.ThePowerToProtect; +namespace BossMod.Stormblood.Quest.Job.Monk.ThePowerToProtect; public enum OID : uint { @@ -26,7 +26,7 @@ public enum AID : uint public enum SID : uint { - ExtremeCaution = 1269, // Boss->player, extra=0x0 + ExtremeCaution = 1269 // Boss->player, extra=0x0 } @@ -44,11 +44,11 @@ public override void OnStatusLose(Actor actor, ActorStatus status) PlayerStates[slot] = default; } } -class IronTempest(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IronTempest), new AOEShapeCircle(5.5f)); +class IronTempest(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.IronTempest), 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 Overpower(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6.5f, 45.Degrees())); +class Rive(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Rive), new AOEShapeRect(30.5f, 1)); +class DiffractiveLaser(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), 5); class IoStates : StateMachineBuilder { @@ -72,6 +72,6 @@ public class Io(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCen 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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs b/BossMod/Modules/Stormblood/Quest/Job/Paladin/RaisingTheSword.cs similarity index 80% rename from BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs rename to BossMod/Modules/Stormblood/Quest/Job/Paladin/RaisingTheSword.cs index 06db3c281d..74ff8dcf10 100644 --- a/BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/Paladin/RaisingTheSword.cs @@ -1,11 +1,11 @@ -namespace BossMod.Stormblood.Quest.RaisingTheSword; +namespace BossMod.Stormblood.Quest.Job.Paladin.RaisingTheSword; public enum OID : uint { Boss = 0x1B51, - Helper = 0x233C, - AldisSwordOfNald = 0x18D6, // R0.500, x10 - TaintedWindSprite = 0x1B52, // R1.000, x0 (spawn during fight) + AldisSwordOfNald = 0x18D6, // R0.5 + TaintedWindSprite = 0x1B52, // R1.0 + Helper = 0x233C } public enum AID : uint @@ -16,8 +16,8 @@ public enum AID : uint 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 VictorySlash(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.VictorySlash), new AOEShapeCone(6.5f, 60.Degrees())); +class ShudderingSwipeCone(BossModule module) : Components.SimpleAOEs(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; @@ -55,7 +55,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme }, Module.CastFinishAt(c.CastInfo)); } } -class NaldsWhisper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NaldsWhisper), new AOEShapeCircle(20)); +class NaldsWhisper(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.NaldsWhisper), 20); class TheFourWinds(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.TaintedWindSprite).Where(x => x.EventState != 7)); class AldisSwordOfNaldStates : StateMachineBuilder diff --git a/BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs b/BossMod/Modules/Stormblood/Quest/Job/Samurai/BloodOnTheDeck.cs similarity index 52% rename from BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs rename to BossMod/Modules/Stormblood/Quest/Job/Samurai/BloodOnTheDeck.cs index e78d012603..2be0e90c0b 100644 --- a/BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/Samurai/BloodOnTheDeck.cs @@ -1,12 +1,13 @@ -namespace BossMod.Stormblood.Quest; +namespace BossMod.Stormblood.Quest.Job.BloodOnTheDeck; + 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) + ShamShinobi = 0x1BE8, // R0.5 + AdjunctOstyrgreinHelper = 0x1BEB, // R0.5 + AdjunctOstyrgrein = 0x1BEA, // R0.5 + Vanara = 0x1BE9, // R3.0 + Helper = 0x233C } public enum AID : uint @@ -17,10 +18,10 @@ public enum AID : uint 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 ScytheTail(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ScytheTail), 7); +class Butcher(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Butcher), new AOEShapeCone(9, 45.Degrees())); +class TenkaGoken(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TenkaGoken), new AOEShapeCone(8.5f, 60.Degrees())); +class Bombslinger(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Bombslinger1), 6); class GurumiBorlumiStates : StateMachineBuilder { @@ -37,6 +38,6 @@ public GurumiBorlumiStates(BossModule module) : base(module) [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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs b/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheBattleOnBekko.cs similarity index 53% rename from BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs rename to BossMod/Modules/Stormblood/Quest/Job/Samurai/TheBattleOnBekko.cs index a25f292749..d202409d0d 100644 --- a/BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheBattleOnBekko.cs @@ -1,11 +1,11 @@ -namespace BossMod.Stormblood.Quest.TheBattleOnBekko; +namespace BossMod.Stormblood.Quest.Job.Samurai.TheBattleOnBekko; public enum OID : uint { Boss = 0x1BF8, - Helper = 0x233C, - UgetsuSlayerOfAThousandSouls = 0x1BF9, // R0.500, x20, Helper type - Voidzone = 0x1E8EA9, // R1.000, x0 (spawn during fight) + UgetsuSlayerOfAThousandSouls = 0x1BF9, // R0.5 + Voidzone = 0x1E8EA9, // R1.0 + Helper = 0x233C } public enum AID : uint @@ -14,42 +14,45 @@ public enum AID : uint 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 + Ugetsuzan1 = 8439, // 1BF9->self, 2.5s cast, range 2-7 180-degree donut sector + Ugetsuzan2 = 8440, // 1BF9->self, 2.5s cast, range 7-12 180-degree donut sector + Ugetsuzan3 = 8441, // 1BF9->self, 2.5s cast, range 12-17 180-degree donut sector + Ugetsuzan4 = 8442, // UgetsuSlayerOfAThousandSouls->self, 2.5s cast, range 17-22 180-degree donut sector 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 KuruiKasha(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.KuruiKasha1), new AOEShapeDonutSector(4.5f, 8.5f, 45.Degrees())); +class KuruiYukikaze(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.KuruiYukikaze), new AOEShapeRect(44, 2), 8); +class HissatsuKyuten(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HissatsuKyuten), 5.5f); +class TenkaGoken(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TenkaGoken), new AOEShapeCone(8.5f, 60.Degrees())); +class ShinGetsubaku(BossModule module) : Components.SimpleAOEs(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())]) +class MijinGiri(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MijinGiri), new AOEShapeRect(80.5f, 5)); + +class Ugetsuzan(BossModule module) : Components.ConcentricAOEs(module, sectors) { + private static readonly Angle a90 = 90.Degrees(); + private static readonly AOEShapeDonutSector[] sectors = [new(2, 7, a90), new(7, 12, a90), new(12, 17, a90), new(17, 22, a90)]; 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); + if ((AID)spell.Action.ID == AID.Ugetsuzan1) + AddSequence(spell.LocXZ, Module.CastFinishAt(spell), spell.Rotation); } - public override void OnEventCast(Actor caster, ActorCastEvent spell) + public override void OnCastFinished(Actor caster, ActorCastInfo spell) { - var idx = (AID)spell.Action.ID switch + var order = (AID)spell.Action.ID switch { - AID.Ugetsuzan => 0, + AID.Ugetsuzan1 => 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); + AdvanceSequence(order, spell.LocXZ, WorldState.FutureTime(2.5f), spell.Rotation); } } diff --git a/BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs b/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheFaceOfTrueEvil.cs similarity index 59% rename from BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs rename to BossMod/Modules/Stormblood/Quest/Job/Samurai/TheFaceOfTrueEvil.cs index 49774827d1..13abdf0986 100644 --- a/BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheFaceOfTrueEvil.cs @@ -1,32 +1,35 @@ -namespace BossMod.Stormblood.Quest.TheFaceOfTrueEvil; +namespace BossMod.Stormblood.Quest.Job.Samurai.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) + Musosai = 0x1BF0, // R1.0 + ViolentWind = 0x1BF1, // R1.0 + Helper2 = 0x1BEF, + Helper = 0x233C } public enum AID : uint { - HissatsuTo1 = 8415, // 1BEF->self, 3.0s cast, range 44+R width 4 rect + HissatsuTo = 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 + ArashiVisual = 8418, // Boss->self, 4.0s cast, single-target + Arashi = 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 + Maiogi = 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)); + +abstract class Hissatsu(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(44.5f, 2)); +class HissatsuKiku(BossModule module) : Hissatsu(module, AID.HissatsuKiku1); +class HissatsuTo(BossModule module) : Hissatsu(module, AID.HissatsuTo); + +class Maiogi(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Maiogi), new AOEShapeCone(80, 25.Degrees())); +class HissatsuKyuten(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HissatsuKyuten), 5.5f); class Arashi(BossModule module) : Components.GenericAOEs(module) { private DateTime? Activation; @@ -36,8 +39,8 @@ 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); + foreach (var e in Module.Enemies(OID.Musosai)) + yield return new(new AOEShapeCircle(4), e.Position, default, Activation.Value); } public override void OnCastStarted(Actor caster, ActorCastInfo spell) @@ -48,7 +51,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if (spell.Action.ID == (uint)AID.Arashi1) + if ((AID)spell.Action.ID == AID.Arashi) Activation = null; } } @@ -71,4 +74,3 @@ public MusosaiStates(BossModule module) : base(module) [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/OurUnsungHeroes.cs b/BossMod/Modules/Stormblood/Quest/Job/Scholar/OurUnsungHeroes.cs similarity index 68% rename from BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs rename to BossMod/Modules/Stormblood/Quest/Job/Scholar/OurUnsungHeroes.cs index 23fff69301..044d7750a3 100644 --- a/BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/Scholar/OurUnsungHeroes.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Quest.OurUnsungHeroes; +namespace BossMod.Stormblood.Quest.Jobs.Scholar.OurUnsungHeroes; public enum OID : uint { @@ -13,25 +13,25 @@ public enum AID : uint 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 + 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 + Stack = 62 // player } public enum SID : uint { - Invincibility = 325, // Boss->Boss, extra=0x0 + 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 CureIV(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.CureIV), 12); +class Glory(BossModule module) : Components.SimpleAOEs(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 DarkII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DarkII), new AOEShapeCone(50.8f, 30.Degrees())); class FallenKuribuStates : StateMachineBuilder { @@ -51,7 +51,10 @@ public FallenKuribuStates(BossModule module) : base(module) { 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; + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var h = hints.PotentialTargets[i]; + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : AIHints.Enemy.PriorityInvincible; + } } } diff --git a/BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs b/BossMod/Modules/Stormblood/Quest/Job/Summoner/AnArtForTheLiving.cs similarity index 69% rename from BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs rename to BossMod/Modules/Stormblood/Quest/Job/Summoner/AnArtForTheLiving.cs index a050938ed8..4a8821290f 100644 --- a/BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs +++ b/BossMod/Modules/Stormblood/Quest/Job/Summoner/AnArtForTheLiving.cs @@ -1,11 +1,11 @@ -namespace BossMod.Stormblood.Quest.AnArtForTheLiving; +namespace BossMod.Stormblood.Quest.Job.Summoner.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) + ExplosiveIndicator = 0x1CD7, // R0.5 + AetherochemicalExplosive = 0x1CD5, // R1.0 + Helper = 0x233C } public enum AID : uint @@ -15,7 +15,7 @@ public enum AID : uint 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 + W11TonzeSwipe = 8699, // 1CD1->self, 3.0s cast, range 5+R 120-degree cone } public enum SID : uint @@ -23,14 +23,16 @@ 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 OneOneOneTonzeSwing(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.W111TonzeSwing), 12); +class OneOneTonzeSwipe(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.W11TonzeSwipe), new AOEShapeCone(9, 60.Degrees())); -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 NerveGas1(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.NerveGas), new AOEShapeCone(35, 60.Degrees())); -class PiercingLaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PiercingLaser), new AOEShapeRect(33.68f, 3)); +abstract class NerveGasLR(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCone(35, 90.Degrees())); +class NerveGasRight(BossModule module) : NerveGasLR(module, AID.NerveGasRight); +class NerveGasLeft(BossModule module) : NerveGasLR(module, AID.NerveGasLeft); + +class PiercingLaser(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.PiercingLaser), new AOEShapeRect(33.68f, 3)); class AetherochemicalExplosive(BossModule module) : Components.GenericAOEs(module) { @@ -66,8 +68,11 @@ class Adds(BossModule module) : Components.AddsMulti(module, [0x1CB6, 0x1CD1, 0x { 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; + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + e.Priority = e.Actor.FindStatus(SID.Invincibility) == null ? 1 : AIHints.Enemy.PriorityInvincible; + } } } @@ -79,12 +84,11 @@ public SummoningNodeStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - ; + .ActivateOnEnter(); } } diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/Enums.cs similarity index 77% rename from BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/Enums.cs index 3b69ab9773..234c77dc13 100644 --- a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/Enums.cs @@ -1,14 +1,15 @@ -namespace BossMod.Stormblood.Quest.ARequiemForHeroes; +namespace BossMod.Stormblood.Quest.MSQ.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) + + AmeNoHabakiri = 0x2692, // R3.0 + TheStorm = 0x2760, // R3.0 + TheSwell = 0x275F, // R3.0 + DarkAether = 0x2694, // R1.2 + Helper = 0x233C } public enum AID : uint diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P1.cs similarity index 96% rename from BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P1.cs index 52ef8c972b..49d6ec1de6 100644 --- a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P1.cs @@ -1,6 +1,6 @@ using BossMod.QuestBattle; -namespace BossMod.Stormblood.Quest.ARequiemForHeroes; +namespace BossMod.Stormblood.Quest.MSQ.ARequiemForHeroes; class AutoHien(WorldState ws) : UnmanagedRotation(ws, 3) { diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P2.cs similarity index 58% rename from BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P2.cs index c42f8422a5..1637b906d4 100644 --- a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P2.cs @@ -1,21 +1,11 @@ -namespace BossMod.Stormblood.Quest.ARequiemForHeroes; +namespace BossMod.Stormblood.Quest.MSQ.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 - }); - } + Lines.Add(new() { Next = spell.LocXZ, Advance = caster.Rotation.ToDirection() * 5, NextExplosion = Module.CastFinishAt(spell), TimeToMove = 1, ExplosionsLeft = 4, MaxShownExplosions = 2 }); } public override void OnEventCast(Actor caster, ActorCastEvent spell) @@ -24,20 +14,19 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) { 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 LightlessSpark2(BossModule module) : Components.SimpleAOEs(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 ArtOfTheStorm(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ArtOfTheStorm), 8); +class EntropicFlame(BossModule module) : Components.SimpleAOEs(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 FloodOfDarkness(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FloodOfDarkness), 6); +class VeinSplitter(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), 10); +class LightlessSpark(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.LightlessSpark), new AOEShapeCone(40, 45.Degrees())); +class SwellUnbound(BossModule module) : Components.SimpleAOEs(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) @@ -46,9 +35,11 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme 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)); + +abstract class ArtOfTheSword(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(40, 3)); +class ArtOfTheSword1(BossModule module) : ArtOfTheSword(module, AID.ArtOfTheSword1); +class ArtOfTheSword2(BossModule module) : ArtOfTheSword(module, AID.ArtOfTheSword2); +class ArtOfTheSword3(BossModule module) : ArtOfTheSword(module, AID.ArtOfTheSword3); class DarkAether(BossModule module) : Components.GenericAOEs(module) { diff --git a/BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs b/BossMod/Modules/Stormblood/Quest/MSQ/BestServedWithColdSteel.cs similarity index 80% rename from BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/BestServedWithColdSteel.cs index 8faa744c35..38cceb0b45 100644 --- a/BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/BestServedWithColdSteel.cs @@ -1,10 +1,10 @@ -namespace BossMod.Stormblood.Quest.BestServedWithColdSteel; +namespace BossMod.Stormblood.Quest.MSQ.BestServedWithColdSteel; public enum OID : uint { - Boss = 0x1A52, // R2.100f, x1 + Boss = 0x1A52, // R2.1 Grynewaht = 0x1A53, - Helper = 0x233C, + Helper = 0x233C } public enum AID : uint @@ -15,20 +15,20 @@ public enum AID : uint 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 + SelfDetonate1 = 9169 // Boss->self, 60.0s cast, range 100 circle } public enum TetherID : uint { - Mine = 54, // 1A56->player + 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 AugmentedUprising(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); +class AugmentedSuffering(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), 6.5f); +class OpenFire(BossModule module) : Components.SimpleAOEs(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 CermetPile(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.CermetPile), new AOEShapeRect(42.1f, 3f)); +class Firebomb(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Firebomb), 4); class MagitekTurret(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.SelfDetonate)) { @@ -59,7 +59,7 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) // 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)); + yield return new(new AOEShapeCircle(6), m.source.Position + projectedExplosion, default, Activation: m.tethered.AddSeconds(12)); } } @@ -83,7 +83,7 @@ public override void OnActorDestroyed(Actor 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); + Arena.AddLine(m.source.Position, pc.Position, Colors.Danger); } } @@ -138,13 +138,14 @@ public class MagitekVanguardIPrototype(WorldState ws, Actor primary) : BossModul protected override void DrawEnemies(int pcSlot, Actor pc) { - Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var h in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var h = hints.PotentialTargets[i]; if (h.Actor.OID == 0x1A52) h.Priority = 1; else if (h.Actor.TargetID == actor.InstanceID) diff --git a/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs b/BossMod/Modules/Stormblood/Quest/MSQ/EmissaryOfTheDawn.cs similarity index 80% rename from BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/EmissaryOfTheDawn.cs index 9936b94e45..eb328c91cb 100644 --- a/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/EmissaryOfTheDawn.cs @@ -1,14 +1,14 @@ using BossMod.QuestBattle.Stormblood.MSQ; -namespace BossMod.Stormblood.Quest.EmissaryOfTheDawn; +namespace BossMod.Stormblood.Quest.MSQ.EmissaryOfTheDawn; public enum OID : uint { Boss = 0x234B, - Helper = 0x233C, + Helper = 0x233C } -class AlphiAI(BossModule module) : QuestBattle.RotationModule(module); +class AlphinaudAI(BossModule module) : QuestBattle.RotationModule(module); class LB(BossModule module) : BossComponent(module) { @@ -24,7 +24,7 @@ class HostileSkyArmorStates : StateMachineBuilder public HostileSkyArmorStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .Raw.Update = () => module.WorldState.CurrentCFCID != 582; } @@ -33,6 +33,6 @@ public HostileSkyArmorStates(BossModule module) : base(module) [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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs b/BossMod/Modules/Stormblood/Quest/MSQ/HisForgottenHome.cs similarity index 75% rename from BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/HisForgottenHome.cs index 67963a398a..662a89a309 100644 --- a/BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/HisForgottenHome.cs @@ -1,22 +1,23 @@ -namespace BossMod.Stormblood.Quest.HisForgottenHome; +namespace BossMod.Stormblood.Quest.MSQ.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) + SoftshellOfTheRed = 0x213B, // R1.6 + SoftshellOfTheRed1 = 0x213C, // R1.6 + SoftshellOfTheRed2 = 0x213D, // R1.6 + Helper = 0x233C } 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 + BlizzardIII = 1087, // 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 Kasaya(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Kasaya), new AOEShapeCone(7.6f, 60.Degrees())); +class WaterIII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.WaterIII), 8); class BlizzardIIIIcon(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(5), 26, centerAtTarget: true) { @@ -58,13 +59,16 @@ public class SlickshellCaptain(WorldState ws, Actor primary) : BossModule(ws, pr 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 DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { // attack anyone targeting isse - foreach (var h in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var h = hints.PotentialTargets[i]; 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/MSQ/HopeOnTheWaves.cs similarity index 66% rename from BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/HopeOnTheWaves.cs index 036208cf37..abc1ced2b3 100644 --- a/BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/HopeOnTheWaves.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Quest.HopeOnTheWaves; +namespace BossMod.Stormblood.Quest.MSQ.HopeOnTheWaves; public enum OID : uint { @@ -17,13 +17,13 @@ public enum AID : uint 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 AssaultCannon(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AssaultCannon), new AOEShapeRect(75, 1)); +class CircleOfDeath(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.CircleOfDeath), 10.24f); +class TwoTonzeMagitekMissile(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.W2TonzeMagitekMissile), 6); +class MagitekMissileProximity(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekMissile1), 11.75f); +class CermetPile(BossModule module) : Components.SimpleAOEs(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 MineSelfDetonate(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SelfDetonate1), 6); class Adds(BossModule module) : BossComponent(module) { @@ -35,8 +35,9 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme ? castInfo.LocXZ : null; - foreach (var e in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var e = hints.PotentialTargets[i]; if (lbCenter != null && e.Actor.OID == 0x2114) { e.ShouldBeTanked = true; @@ -69,12 +70,12 @@ public ImperialCenturionStates(BossModule module) : base(module) } [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 class ImperialCenturion(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) { - public static readonly ArenaBoundsCustom BoundsP2 = new(30, new(CurveApprox.Ellipse(34, 21, 0.05f).Select(p => p.Rotate(140.Degrees())))); + public static readonly ArenaBoundsComplex arena = new([new Ellipse(new(473.25f, 751.75f), 34, 21, 50, 140.Degrees())]); protected override void DrawEnemies(int pcSlot, Actor pc) { - Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } } diff --git a/BossMod/Modules/Stormblood/Quest/Naadam.cs b/BossMod/Modules/Stormblood/Quest/MSQ/Naadam.cs similarity index 73% rename from BossMod/Modules/Stormblood/Quest/Naadam.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/Naadam.cs index 0f2be8492c..d1343640a8 100644 --- a/BossMod/Modules/Stormblood/Quest/Naadam.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/Naadam.cs @@ -1,14 +1,14 @@ -namespace BossMod.Stormblood.Quest.Naadam; +namespace BossMod.Stormblood.Quest.MSQ.Naadam; public enum OID : uint { Boss = 0x1B31, + MagnaiTheOlder = 0x1B38, // R0.5 + StellarChuluu = 0x1B3F, // R1.8 + StellarChuluu1 = 0x1B40, // R1.8 + Grynewaht = 0x1B3A, // R0.5 + Ovoo = 0x1EA4E1, 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 @@ -18,7 +18,7 @@ public enum AID : uint 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 + AugmentedUprising = 8493 // Grynewaht->self, 3.0s cast, range 8+R 120-degree cone } public enum SID : uint @@ -26,13 +26,13 @@ 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 DiffractiveLaser(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), 5); +class AugmentedSuffering(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), 6.5f); +class AugmentedUprising(BossModule module) : Components.SimpleAOEs(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 ViolentEarth(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ViolentEarth), 6); +class DispellingWind(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DispellingWind), new AOEShapeRect(40.5f, 4)); +class Epigraph(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Epigraph), new AOEShapeRect(45, 4)); class DrawOvoo : BossComponent { @@ -45,7 +45,7 @@ public DrawOvoo(BossModule module) : base(module) public override void DrawArenaForeground(int pcSlot, Actor pc) { - Arena.Actor(Ovoo, ArenaColor.Object, true); + Arena.Actor(Ovoo, Colors.Object, true); } } @@ -76,8 +76,9 @@ 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) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var e = hints.PotentialTargets[i]; if (e.Actor.FindStatus(SID.EarthenAccord) != null) e.Priority = 5; else if (e.Actor.OID == (uint)OID.StellarChuluu) @@ -94,8 +95,9 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { var chuluu = WorldState.Actors.Where(x => (OID)x.OID == OID.StellarChuluu1).Select(x => x.InstanceID).ToList(); - foreach (var e in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var e = hints.PotentialTargets[i]; if (chuluu.Contains(e.Actor.TargetID)) e.Priority = 5; else if ((OID)e.Actor.OID == OID.Grynewaht) @@ -137,6 +139,6 @@ public OvooStates(BossModule module) : base(module) { 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); + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } diff --git a/BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs b/BossMod/Modules/Stormblood/Quest/MSQ/ReturnOfTheBull.cs similarity index 84% rename from BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/ReturnOfTheBull.cs index 92d76ac875..942b10b5c4 100644 --- a/BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/ReturnOfTheBull.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Quest.ReturnOfTheBull; +namespace BossMod.Stormblood.Quest.MSQ.ReturnOfTheBull; public enum OID : uint { Boss = 0x1FD2, @@ -18,8 +18,8 @@ public enum AID : uint 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 PathOfLight(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ThePathOfLight), new AOEShapeCone(43.5f, 60.Degrees())); +class BlissfulSpear(BossModule module) : Components.SimpleAOEs(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) @@ -29,7 +29,7 @@ class FordolaShield(BossModule module) : BossComponent(module) public override void DrawArenaBackground(int pcSlot, Actor pc) { if (Shield != null) - Arena.AddCircleFilled(Shield.Position, 4, ArenaColor.SafeFromAOE); + Arena.AddCircleFilled(Shield.Position, 4, Colors.SafeFromAOE); } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) @@ -51,7 +51,7 @@ class Deflect(BossModule module) : BossComponent(module) public override void DrawArenaForeground(int pcSlot, Actor pc) { - Arena.Actors(Spheres, 0xFFFFA080); + Arena.Actors(Spheres, Colors.Other9); } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) @@ -82,8 +82,7 @@ public LakshmiStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - ; + .ActivateOnEnter(); } } @@ -92,13 +91,16 @@ public LakshmiStates(BossModule module) : base(module) { protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var e in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; 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/MSQ/RhalgrsBeacon.cs similarity index 90% rename from BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/RhalgrsBeacon.cs index 8410fa5c46..76d933b75f 100644 --- a/BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/RhalgrsBeacon.cs @@ -1,15 +1,15 @@ -namespace BossMod.Stormblood.Quest.RhalgrsBeacon; +namespace BossMod.Stormblood.Quest.MSQ.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 + Helper = 0x233C } public enum AID : uint @@ -21,7 +21,7 @@ public enum AID : uint 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 DiffractiveLaser(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), new AOEShapeCone(18.6f, 30.Degrees())); class TerminusEst(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.TheOrder)) { @@ -30,13 +30,13 @@ class TerminusEst(BossModule module) : Components.GenericAOEs(module, ActionID.M public override void DrawArenaForeground(int pcSlot, Actor pc) { - Arena.Actors(Module.Enemies(OID.TerminusEst).Where(x => !x.IsDead), ArenaColor.Object, true); + Arena.Actors(Module.Enemies(OID.TerminusEst).Where(x => !x.IsDead), Colors.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)); + yield return new(new AOEShapeRect(41f, 2), t.Position, t.Rotation, Activation: CastFinish ?? WorldState.FutureTime(10)); } public override void OnActorCreated(Actor actor) diff --git a/BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs b/BossMod/Modules/Stormblood/Quest/MSQ/TheMeasureOfHisReach.cs similarity index 58% rename from BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/TheMeasureOfHisReach.cs index 4d392dfffc..03feeb2ea9 100644 --- a/BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/TheMeasureOfHisReach.cs @@ -1,10 +1,10 @@ -namespace BossMod.Stormblood.Quest.TheMeasureOfHisReach; +namespace BossMod.Stormblood.Quest.MSQ.TheMeasureOfHisReach; public enum OID : uint { Boss = 0x1C48, - Helper = 0x233C, - Whitefang = 0x1C5A + Whitefang = 0x1C5A, + Helper = 0x233C } public enum AID : uint @@ -15,19 +15,19 @@ public enum AID : uint 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)) +class Moonlight(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HowlingMoonlight), 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))); + hints.PredictedDamage.Add((Raid.WithSlot(false, false).Mask(), c.Activation)); } } -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 Icewind(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HowlingIcewind), new AOEShapeRect(44, 2)); +class Dragonspirit(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Dragonspirit), 7.5f); +class Bloomshower(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.HowlingBloomshower), new AOEShapeDonutSector(4, 8, 45.Degrees())); class HakuroWhitefangStates : StateMachineBuilder { @@ -37,11 +37,9 @@ public HakuroWhitefangStates(BossModule module) : base(module) .ActivateOnEnter() .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/TheResonant.cs b/BossMod/Modules/Stormblood/Quest/MSQ/TheResonant.cs similarity index 83% rename from BossMod/Modules/Stormblood/Quest/TheResonant.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/TheResonant.cs index 713d29fdec..70bcec2396 100644 --- a/BossMod/Modules/Stormblood/Quest/TheResonant.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/TheResonant.cs @@ -1,12 +1,12 @@ -namespace BossMod.Stormblood.Quest.TheResonant; +namespace BossMod.Stormblood.Quest.MSQ.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) + Helper = 0x233C } public enum AID : uint @@ -23,7 +23,7 @@ public enum SID : uint Resonant = 780, } -class Skullbreaker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Skullbreaker1), new AOEShapeCircle(12)); +class Skullbreaker(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Skullbreaker1), 12); class TerminusEst(BossModule module) : Components.GenericAOEs(module) { @@ -51,15 +51,16 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) 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 MagitekRay(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekRay), new AOEShapeRect(45.6f, 1)); +class ChoppingBlock(BossModule module) : Components.SimpleAOEs(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) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var h = hints.PotentialTargets[i]; if (h.Actor.FindStatus(SID.Resonant) != null) { h.Priority = AIHints.Enemy.PriorityForbidden; diff --git a/BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs b/BossMod/Modules/Stormblood/Quest/MSQ/TheTimeBetweenTheSeconds.cs similarity index 76% rename from BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/TheTimeBetweenTheSeconds.cs index 3e51ac891c..b20e437f2c 100644 --- a/BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/TheTimeBetweenTheSeconds.cs @@ -1,15 +1,15 @@ -namespace BossMod.Stormblood.Quest.TheTimeBetweenTheSeconds; +namespace BossMod.Stormblood.Quest.MSQ.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) + Helper = 0x233C } public enum AID : uint @@ -21,8 +21,8 @@ public enum AID : uint 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 ArtOfTheSword(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword1), new AOEShapeRect(41, 3)); +class VeinSplitter(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), 10); class Concentrativity(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Concentrativity)); class LightlessFlame(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.LightlessFlame)) { @@ -48,13 +48,13 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) Flames.Remove(caster.InstanceID); } } -class LightlessSpark(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LightlessSpark), new AOEShapeCone(40.92f, 45.Degrees())); +class LightlessSpark(BossModule module) : Components.SimpleAOEs(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); + Arena.Actors(Module.Enemies(OID.ZenosYaeGalvus1)); + Arena.Actors(Module.Enemies(OID.DarkReflection)); } } @@ -62,10 +62,10 @@ class ZenosYaeGalvusStates : StateMachineBuilder { public ZenosYaeGalvusStates(BossModule module) : base(module) { - SimplePhase(0, id => BuildState(id, "P1 enrage", 1800), "P1") + SimplePhase(0, id => BuildState(id, ""), "P1") .Raw.Update = () => !Module.PrimaryActor.IsTargetable; - SimplePhase(1, id => BuildState(id, "P2 enrage", 1800).ActivateOnEnter().ActivateOnEnter(), "P2") - .Raw.Update = () => !Module.Enemies(OID.ZenosYaeGalvus1).Any(); + SimplePhase(1, id => BuildState(id, "").ActivateOnEnter().ActivateOnEnter(), "P2") + .Raw.Update = () => Module.Enemies(OID.ZenosYaeGalvus1).Count == 0; } private State BuildState(uint id, string name, float duration = 10000) diff --git a/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs b/BossMod/Modules/Stormblood/Quest/MSQ/TheWillOfTheMoon.cs similarity index 72% rename from BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs rename to BossMod/Modules/Stormblood/Quest/MSQ/TheWillOfTheMoon.cs index b3da138e51..03135e86f7 100644 --- a/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs +++ b/BossMod/Modules/Stormblood/Quest/MSQ/TheWillOfTheMoon.cs @@ -1,17 +1,17 @@ using BossMod.QuestBattle; using RPID = BossMod.Roleplay.AID; -namespace BossMod.Stormblood.Quest.TheWillOfTheMoon; +namespace BossMod.Stormblood.Quest.MSQ.TheWillOfTheMoon; public enum OID : uint { Boss = 0x24A0, Magnai = 0x24A1, - Helper = 0x233C, - KhunShavar = 0x252F, // R1.820, x0 (spawn during fight) + KhunShavar = 0x252F, // R1.82 Hien = 0x24A3, - Daidukul = 0x24A2, // R0.500, x1 - TheScaleOfTheFather = 0x2532, // R1.000, x0 (spawn during fight) + Daidukul = 0x24A2, // R0.5 + TheScaleOfTheFather = 0x2532, // R1.0 + Helper = 0x233C } public enum AID : uint @@ -34,36 +34,36 @@ 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)); +class DispellingWind(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.DispellingWind), new AOEShapeRect(40, 4)); +class Epigraph(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Epigraph), new AOEShapeRect(45, 4)); +class Whisper(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.WhisperOfLivesPast), new AOEShapeDonut(6, 12)); +class Blizzard(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AncientBlizzard), new AOEShapeCone(40, 22.5f.Degrees())); +class Tornado(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Tornado), 6); +class Epigraph1(BossModule module) : Components.SimpleAOEs(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 class FlatlandFury(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FlatlandFury), 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); + if (ActiveCasters.Count == 9) + hints.ForcedTarget = Module.Enemies(OID.TheScaleOfTheFather).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 class FlatlandFuryEnrage(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FlatlandFuryEnrage), 10) { public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (ActiveCasters.Count() < 9) + 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 ViolentEarth(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ViolentEarth), 6); +public class WindChisel(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.WindChisel), new AOEShapeCone(34, 10.Degrees())); public class Scales(BossModule module) : Components.Adds(module, (uint)OID.TheScaleOfTheFather); @@ -107,8 +107,9 @@ 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) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var e = hints.PotentialTargets[i]; if (e.Actor.FindStatus(SID.Invincibility) != null) e.Priority = AIHints.Enemy.PriorityInvincible; @@ -123,8 +124,9 @@ 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) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var e = hints.PotentialTargets[i]; e.Priority = e.Actor.OID == (uint)OID.Magnai ? 1 : 0; } } @@ -142,7 +144,7 @@ public SaduHeavensflameStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .Raw.Update = () => Module.Enemies(OID.Magnai).Any(); + .Raw.Update = () => Module.Enemies(OID.Magnai).Count != 0; TrivialPhase(1) .ActivateOnEnter() .ActivateOnEnter() @@ -164,6 +166,6 @@ public SaduHeavensflameStates(BossModule module) : base(module) { protected override void DrawEnemies(int pcSlot, Actor pc) { - Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly)); } } diff --git a/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs b/BossMod/Modules/Stormblood/Quest/TheFourLords/TortoiseInTime.cs similarity index 78% rename from BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs rename to BossMod/Modules/Stormblood/Quest/TheFourLords/TortoiseInTime.cs index 3a59b827e1..9c39d123bf 100644 --- a/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs +++ b/BossMod/Modules/Stormblood/Quest/TheFourLords/TortoiseInTime.cs @@ -1,12 +1,12 @@ -namespace BossMod.Stormblood.Quest.TortoiseInTime; +namespace BossMod.Stormblood.Quest.FourLords.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) + Soroban = 0x2351, // R0.5 + MonkeyMagick = 0x23C2, // R1.0 + Font = 0x233B, // R4.0 + Helper = 0x233C } public enum AID : uint @@ -19,9 +19,9 @@ public enum AID : uint 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 Whitewater(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Whitewater1), new AOEShapeRect(40.5f, 3.5f)); +class Upwell(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Upwell), new AOEShapeCone(41, 15.Degrees())); +class SpiritBurst(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SpiritBurst), 6); class WaterDrop(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.WaterDrop), 6); class ExplosiveTataru(BossModule module) : BossComponent(module) @@ -51,7 +51,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) public override void DrawArenaBackground(int pcSlot, Actor pc) { if (Tataru != null) - Arena.AddCircle(Tataru.Position, 6, ArenaColor.Danger); + Arena.AddCircle(Tataru.Position, 6, Colors.Danger); } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) @@ -67,7 +67,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) } } -class Eddy(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Eddy1), 6); +class Eddy(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Eddy1), 6); class ShieldHint(BossModule module) : BossComponent(module) { @@ -83,7 +83,7 @@ public override void OnActorEState(Actor actor, ushort state) public override void DrawArenaBackground(int pcSlot, Actor pc) { if (Shield is Actor s) - Arena.ZoneCircle(s.Position, Radius, ArenaColor.SafeFromAOE); + Arena.ZoneCircle(s.Position, Radius, Colors.SafeFromAOE); } public override void OnEventCast(Actor caster, ActorCastEvent spell) diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs index 2cd6a45c1b..d4d5adcded 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs @@ -73,7 +73,7 @@ class RingOfFire(BossModule module) : Components.SimpleAOEs(module, ActionID.Mak class StoneII(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.StoneII)); class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.Abharamu); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.Abharamu]); abstract class Mandragoras(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), 6.84f); class PluckAndPrune(BossModule module) : Mandragoras(module, AID.PluckAndPrune); diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheLostCanalsOfUznair/CanalIcebeast.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheLostCanalsOfUznair/CanalIcebeast.cs index 589130d4fd..b630ffc8b3 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheLostCanalsOfUznair/CanalIcebeast.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheLostCanalsOfUznair/CanalIcebeast.cs @@ -34,7 +34,7 @@ class Freezeover(BossModule module) : Components.SimpleAOEs(module, ActionID.Mak class PlainPound(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.PlainPound), 4.56f); class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.Abharamu); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.Abharamu]); class CanalIcebeastStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarAiravata.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarAiravata.cs index c9c9160ff7..fa837339c2 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarAiravata.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarAiravata.cs @@ -97,7 +97,7 @@ public override void DrawArenaBackground(int pcSlot, Actor pc) class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class AltarAiravataStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarArachne.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarArachne.cs index ca50a56570..490f035367 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarArachne.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarArachne.cs @@ -41,7 +41,7 @@ class Earthquake1(BossModule module) : Components.SimpleAOEs(module, ActionID.Ma class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class AltarArachneStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarBeast.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarBeast.cs index 204ff82f74..4c43d505c7 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarBeast.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarBeast.cs @@ -54,7 +54,7 @@ class Pollen(BossModule module) : Mandragoras(module, AID.Pollen); class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class AltarBeastStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarChimera.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarChimera.cs index 5d8c72a629..0fd1547705 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarChimera.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarChimera.cs @@ -76,7 +76,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class AltarChimeraStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarDiresaur.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarDiresaur.cs index 150aa22852..4fba571698 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarDiresaur.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarDiresaur.cs @@ -82,7 +82,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class AltarDiresaurStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarDullahan.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarDullahan.cs index 0a99d8a1fa..e19b2d7ff2 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarDullahan.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarDullahan.cs @@ -49,7 +49,7 @@ class StygianReleaseKB(BossModule module) : Components.KnockbackFromCastTarget(m class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); abstract class Mandragoras(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), 6.84f); class PluckAndPrune(BossModule module) : Mandragoras(module, AID.PluckAndPrune); diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarKelpie.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarKelpie.cs index 19165a88d2..c2eb0ed05d 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarKelpie.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarKelpie.cs @@ -80,7 +80,7 @@ class RisingSeasKB(BossModule module) : Components.KnockbackFromCastTarget(modul class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); abstract class Mandragoras(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), 6.84f); class PluckAndPrune(BossModule module) : Mandragoras(module, AID.PluckAndPrune); diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarSkatene.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarSkatene.cs index a0613d212f..1b2b6d1dbe 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarSkatene.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarSkatene.cs @@ -32,7 +32,7 @@ class VoidCall(BossModule module) : Components.CastHint(module, ActionID.MakeSpe class RecklessAbandon(BossModule module) : Components.SingleTargetDelayableCast(module, ActionID.MakeSpell(AID.RecklessAbandon)); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class AltarSkateneStates : StateMachineBuilder diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarTotem.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarTotem.cs index 384113e9e4..d07c4b6778 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarTotem.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/AltarTotem.cs @@ -77,7 +77,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class AltarTotemStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/TheOlderOne.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/TheOlderOne.cs index bf34894ce8..19e82d6e7b 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/TheOlderOne.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/TheOlderOne.cs @@ -59,7 +59,7 @@ class Pollen(BossModule module) : Mandragoras(module, AID.Pollen); class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class TheOlderOneStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/TheWinged.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/TheWinged.cs index b440cb06b7..9e19a590f8 100644 --- a/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/TheWinged.cs +++ b/BossMod/Modules/Stormblood/TreasureHunt/TheShiftingAltarsOfUznair/TheWinged.cs @@ -54,7 +54,7 @@ class Pollen(BossModule module) : Mandragoras(module, AID.Pollen); class RaucousScritch(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RaucousScritch), new AOEShapeCone(8.42f, 60.Degrees())); class Hurl(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Hurl), 6); -class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), (uint)OID.AltarMatanga); +class Spin(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Spin), new AOEShapeCone(9.42f, 60.Degrees()), [(uint)OID.AltarMatanga]); class TheWingedStates : StateMachineBuilder { diff --git a/BossMod/Modules/Stormblood/Ultimate/UCOB/UCOB.cs b/BossMod/Modules/Stormblood/Ultimate/UCOB/UCOB.cs index ea144edc9b..fc64d90b9b 100644 --- a/BossMod/Modules/Stormblood/Ultimate/UCOB/UCOB.cs +++ b/BossMod/Modules/Stormblood/Ultimate/UCOB/UCOB.cs @@ -1,9 +1,9 @@ namespace BossMod.Stormblood.Ultimate.UCOB; -class P1Plummet(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Plummet), new AOEShapeCone(12, 60.Degrees()), (uint)OID.Twintania); +class P1Plummet(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Plummet), new AOEShapeCone(12, 60.Degrees()), [(uint)OID.Twintania]); class P1Fireball(BossModule module) : Components.StackWithIcon(module, (uint)IconID.Fireball, ActionID.MakeSpell(AID.Fireball), 4, 5.3f, 4, 4); class P2BahamutsClaw(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.BahamutsClaw)); -class P3FlareBreath(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.FlareBreath), new AOEShapeCone(29.2f, 45.Degrees()), (uint)OID.BahamutPrime); // TODO: verify angle +class P3FlareBreath(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.FlareBreath), new AOEShapeCone(29.2f, 45.Degrees()), [(uint)OID.BahamutPrime]); // TODO: verify angle class P5MornAfah(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.MornAfah), 4, 8, 8); // TODO: verify radius [ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.Twintania, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 280, PlanLevel = 70)] diff --git a/BossMod/Modules/Stormblood/Ultimate/UWU/P4ViscousAetheroplasm.cs b/BossMod/Modules/Stormblood/Ultimate/UWU/P4ViscousAetheroplasm.cs index a516d3b5f1..45abd8e385 100644 --- a/BossMod/Modules/Stormblood/Ultimate/UWU/P4ViscousAetheroplasm.cs +++ b/BossMod/Modules/Stormblood/Ultimate/UWU/P4ViscousAetheroplasm.cs @@ -1,6 +1,6 @@ namespace BossMod.Stormblood.Ultimate.UWU; -class P4ViscousAetheroplasmApply(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.ViscousAetheroplasmApply), new AOEShapeCircle(2), (uint)OID.UltimaWeapon, originAtTarget: true); +class P4ViscousAetheroplasmApply(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.ViscousAetheroplasmApply), new AOEShapeCircle(2), [(uint)OID.UltimaWeapon], originAtTarget: true); // TODO: if aetheroplasm target is the same as homing laser target, assume it is being soaked solo; consider merging these two components class P4ViscousAetheroplasmResolve(BossModule module) : Components.UniformStackSpread(module, 4, 0, 7) diff --git a/BossMod/Modules/Stormblood/Ultimate/UWU/UWU.cs b/BossMod/Modules/Stormblood/Ultimate/UWU/UWU.cs index eedca9d423..d78168c96f 100644 --- a/BossMod/Modules/Stormblood/Ultimate/UWU/UWU.cs +++ b/BossMod/Modules/Stormblood/Ultimate/UWU/UWU.cs @@ -5,15 +5,15 @@ class P1EyeOfTheStorm(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.EyeOfTheStorm), new AOEShapeDonut(12, 25)); class P1Gigastorm(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Gigastorm), 6.5f); class P2RadiantPlume(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.RadiantPlumeAOE), 8); -class P2Incinerate(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Incinerate), new AOEShapeCone(15, 60.Degrees()), (uint)OID.Ifrit); -class P3RockBuster(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.RockBuster), new AOEShapeCone(10.55f, 60.Degrees()), (uint)OID.Titan); // TODO: verify angle -class P3MountainBuster(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.MountainBuster), new AOEShapeCone(15.55f, 45.Degrees()), (uint)OID.Titan); // TODO: verify angle +class P2Incinerate(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Incinerate), new AOEShapeCone(15, 60.Degrees()), [(uint)OID.Ifrit]); +class P3RockBuster(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.RockBuster), new AOEShapeCone(10.55f, 60.Degrees()), [(uint)OID.Titan]); // TODO: verify angle +class P3MountainBuster(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.MountainBuster), new AOEShapeCone(15.55f, 45.Degrees()), [(uint)OID.Titan]); // TODO: verify angle class P3WeightOfTheLand(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.WeightOfTheLandAOE), 6); class P3Upheaval(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.Upheaval), 24, true); class P3Tumult(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Tumult)); class P4Blight(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Blight)); class P4HomingLasers(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.HomingLasers), 4); -class P4DiffractiveLaser(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.DiffractiveLaser), new AOEShapeCone(18, 45.Degrees()), (uint)OID.UltimaWeapon); // TODO: verify angle +class P4DiffractiveLaser(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.DiffractiveLaser), new AOEShapeCone(18, 45.Degrees()), [(uint)OID.UltimaWeapon]); // TODO: verify angle class P5MistralSongCone(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MistralSongCone), new AOEShapeCone(21.7f, 75.Degrees())); abstract class P5AetherochemicalLaser(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(46, 4)); diff --git a/BossMod/Util/Color.cs b/BossMod/Util/Color.cs index 6215b8d310..45c2718907 100644 --- a/BossMod/Util/Color.cs +++ b/BossMod/Util/Color.cs @@ -72,6 +72,7 @@ public static class Colors public static uint Other6 => _config.ArenaOther[5].ABGR; public static uint Other7 => _config.ArenaOther[6].ABGR; public static uint Other8 => _config.ArenaOther[7].ABGR; + public static uint Other9 => _config.ArenaOther[8].ABGR; public static uint Shadows => _config.Shadows.ABGR; public static uint CardinalN => _config.CardinalN.ABGR; public static uint CardinalE => _config.CardinalE.ABGR; diff --git a/BossMod/Util/CurveApprox.cs b/BossMod/Util/CurveApprox.cs index 6c6723ec5a..5a225d7d3d 100644 --- a/BossMod/Util/CurveApprox.cs +++ b/BossMod/Util/CurveApprox.cs @@ -94,18 +94,6 @@ public static WPos[] CircleSector(WPos center, float radius, Angle angleStart, A return points; } - 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 WDir[] Donut(float innerRadius, float outerRadius, float maxError) {