diff --git a/BossMod/ActionTweaks/ActionTweaksConfig.cs b/BossMod/ActionTweaks/ActionTweaksConfig.cs index 5b38395729..6ad6017e83 100644 --- a/BossMod/ActionTweaks/ActionTweaksConfig.cs +++ b/BossMod/ActionTweaks/ActionTweaksConfig.cs @@ -77,6 +77,6 @@ public enum GroundTargetingMode [PropertyDisplay("Automatic target selection for ground-targeted abilities")] public GroundTargetingMode GTMode = GroundTargetingMode.Manual; - [PropertyDisplay("Try to prevent dashing into AOEs", tooltip: "Prevent automatic use of damaging gap closers (like WAR Onslaught) if they would move you into a dangerous area. May not work as expected in instances that do not have modules.")] + [PropertyDisplay("Try to prevent dashing into AOEs", tooltip: "Prevent automatic use of damaging gap closers (like WAR Onslaught) if they would move you into a dangerous area. May not work as expected in instances that do not have modules.", since: "0.0.0.290")] public bool PreventDangerousDash = false; } diff --git a/BossMod/ActionTweaks/AutoAutosTweak.cs b/BossMod/ActionTweaks/AutoAutosTweak.cs index 267b3ebc70..5c9fee9b43 100644 --- a/BossMod/ActionTweaks/AutoAutosTweak.cs +++ b/BossMod/ActionTweaks/AutoAutosTweak.cs @@ -33,7 +33,9 @@ public bool GetDesiredState(bool currentState, ulong targetId) if (_config.PyreticThreshold > 0 && hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && hints.ImminentSpecialMode.activation < ws.FutureTime(_config.PyreticThreshold)) return false; // pyretic => disable autos - if (hints.FindEnemy(target)?.Priority == AIHints.Enemy.PriorityForbidden) + var enemy = hints.FindEnemy(target); + + if (enemy?.Priority == AIHints.Enemy.PriorityForbidden || enemy?.Spikes == true) return false; return player.InCombat || ws.Client.CountdownRemaining <= PrePullThreshold; // no reason not to enable autos! diff --git a/BossMod/Autorotation/MiscAI/AutoFarm.cs b/BossMod/Autorotation/MiscAI/AutoFarm.cs index 1a8464fb2c..f89432f7c6 100644 --- a/BossMod/Autorotation/MiscAI/AutoFarm.cs +++ b/BossMod/Autorotation/MiscAI/AutoFarm.cs @@ -58,7 +58,7 @@ void prioritize(AIHints.Enemy e, int prio) // 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) + if (Utils.IsPlayerSyncedToFate(World) && strategy.Option(Track.Fate).As() == PriorityStrategy.Prioritize) { foreach (var e in Hints.PotentialTargets) { diff --git a/BossMod/Autorotation/MiscAI/AutoPull.cs b/BossMod/Autorotation/MiscAI/AutoPull.cs index d99b20dac0..db0ebf8c24 100644 --- a/BossMod/Autorotation/MiscAI/AutoPull.cs +++ b/BossMod/Autorotation/MiscAI/AutoPull.cs @@ -13,7 +13,7 @@ public static RotationModuleDefinition Definition() def.AbilityTrack(Track.QuestBattle, "Automatically attack solo duty bosses"); def.AbilityTrack(Track.DeepDungeon, "Automatically attack deep dungeon bosses when solo"); def.AbilityTrack(Track.EpicEcho, "Automatically attack all targets if the Epic Echo status is present (i.e. when unsynced)"); - def.AbilityTrack(Track.Hunt, "Automatically attack hunt marks once they have already been pulled"); + def.AbilityTrack(Track.Hunt, "Automatically attack hunt marks once they are below 95% HP"); return def; } diff --git a/BossMod/Autorotation/Standard/xan/AI/TrackPartyHealth.cs b/BossMod/Autorotation/Standard/xan/AI/TrackPartyHealth.cs index 35bf656092..02bad91599 100644 --- a/BossMod/Autorotation/Standard/xan/AI/TrackPartyHealth.cs +++ b/BossMod/Autorotation/Standard/xan/AI/TrackPartyHealth.cs @@ -105,7 +105,7 @@ private PartyHealthState CalculatePartyHealthState(Func filter) private PartyHealthState CalcPartyHealthInArea(WPos center, float radius) => CalculatePartyHealthState(act => act.Position.InCircle(center, radius)); - public (Actor Target, PartyMemberState State)? BestSTHealTarget => PartyHealth.StdDev > AOEBreakpointHPVariance ? (World.Party[PartyHealth.LowestHPSlot]!, PartyMemberStates[PartyHealth.LowestHPSlot]) : null; + public (Actor Target, PartyMemberState State)? BestSTHealTarget => PartyHealth.StdDev > AOEBreakpointHPVariance || PartyHealth.Count == 1 ? (World.Party[PartyHealth.LowestHPSlot]!, PartyMemberStates[PartyHealth.LowestHPSlot]) : null; public bool ShouldHealInArea(WPos center, float radius, float hpThreshold) { diff --git a/BossMod/Autorotation/Standard/xan/Basexan.cs b/BossMod/Autorotation/Standard/xan/Basexan.cs index 77f1523cad..6f9b833dfe 100644 --- a/BossMod/Autorotation/Standard/xan/Basexan.cs +++ b/BossMod/Autorotation/Standard/xan/Basexan.cs @@ -515,9 +515,9 @@ public static RotationModuleDefinition DefineSharedTA(this RotationModuleDefinit return def; } - public static RotationModuleDefinition.ConfigRef DefineSimple(this RotationModuleDefinition def, Index track, string name, int minLevel = 1) where Index : Enum + public static RotationModuleDefinition.ConfigRef DefineSimple(this RotationModuleDefinition def, Index track, string name, int minLevel = 1, float uiPriority = 0) where Index : Enum { - return def.Define(track).As(name) + return def.Define(track).As(name, uiPriority: uiPriority) .AddOption(OffensiveStrategy.Automatic, "Auto", "Use when optimal", minLevel: minLevel) .AddOption(OffensiveStrategy.Delay, "Delay", "Don't use", minLevel: minLevel) .AddOption(OffensiveStrategy.Force, "Force", "Use ASAP", minLevel: minLevel); diff --git a/BossMod/Autorotation/Standard/xan/Casters/RDM.cs b/BossMod/Autorotation/Standard/xan/Casters/RDM.cs index a61bbae00f..6e6afc4dcb 100644 --- a/BossMod/Autorotation/Standard/xan/Casters/RDM.cs +++ b/BossMod/Autorotation/Standard/xan/Casters/RDM.cs @@ -126,6 +126,8 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) if (primaryTarget is { } tar && (Swordplay > 0 || LowestMana >= comboMana || InCombo)) Hints.GoalZones.Add(Hints.GoalSingleTarget(tar.Actor, 3)); + GoalZoneSingle(25); + OGCD(strategy, primaryTarget); if (ComboLastMove is AID.Scorch) diff --git a/BossMod/Autorotation/Standard/xan/Healers/WHM.cs b/BossMod/Autorotation/Standard/xan/Healers/WHM.cs index bb6f7c8c62..ef145907e6 100644 --- a/BossMod/Autorotation/Standard/xan/Healers/WHM.cs +++ b/BossMod/Autorotation/Standard/xan/Healers/WHM.cs @@ -37,7 +37,6 @@ public static RotationModuleDefinition Definition() public int NumHolyTargets; public int NumAssizeTargets; public int NumMiseryTargets; - public int NumSolaceTargets; private Enemy? BestDotTarget; private Enemy? BestMiseryTarget; @@ -59,8 +58,6 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) (BestMiseryTarget, NumMiseryTargets) = SelectTarget(strategy, primaryTarget, 25, IsSplashTarget); (BestDotTarget, TargetDotLeft) = SelectDotTarget(strategy, primaryTarget, DotLeft, 2); - NumSolaceTargets = World.Party.WithoutSlot(excludeAlliance: true).Count(x => Player.DistanceToHitbox(x) <= 20); - if (CountdownRemaining > 0) { if (CountdownRemaining < GetCastTime(AID.Stone1)) @@ -93,7 +90,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) // TODO make a track for this if (Lily == 3 || !CanFitGCD(NextLily, 2) && Lily == 2) - PushGCD(AID.AfflatusSolace, World.Party.WithoutSlot(excludeAlliance: true).Where(m => Player.DistanceToHitbox(m) <= 30).MinBy(PredictedHPRatio)); + PushGCD(AID.AfflatusSolace, Player); if (SacredSight > 0) PushGCD(AID.GlareIV, primaryTarget); diff --git a/BossMod/Autorotation/Standard/xan/Melee/MNK.cs b/BossMod/Autorotation/Standard/xan/Melee/MNK.cs index 48fa9b7b7b..8e2927474e 100644 --- a/BossMod/Autorotation/Standard/xan/Melee/MNK.cs +++ b/BossMod/Autorotation/Standard/xan/Melee/MNK.cs @@ -6,7 +6,7 @@ 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, TN } + public enum Track { BH = SharedTrack.Buffs, RoF, FiresReply, RoW, WindsReply, PB, Nadi, Blitz, SSS, FormShift, Meditation, TC, Potion, Engage, TN, Positional } public enum PotionStrategy { Manual, @@ -27,6 +27,12 @@ public enum FRStrategy Force, Delay } + public enum WRStrategy + { + Automatic, + Force, + PreDowntime + } public enum NadiStrategy { Automatic, @@ -62,6 +68,11 @@ public enum TCStrategy None, GapClose } + public enum PositionalStrategy + { + Automatic, + Ignore + } public enum BlitzStrategy { Automatic, @@ -83,44 +94,33 @@ public static RotationModuleDefinition Definition() var def = new RotationModuleDefinition("xan MNK", "Monk", "Standard rotation (xan)|Melee", "xan", RotationModuleQuality.Good, BitMask.Build(Class.MNK, Class.PGL), 100); def.DefineSharedTA(); - def.Define(Track.Potion).As("Pot") - .AddOption(PotionStrategy.Manual, "Do not automatically use") - .AddOption(PotionStrategy.PreBuffs, "Use ~4 GCDs before raid buff window") - .AddOption(PotionStrategy.Now, "Use ASAP"); - - def.DefineSimple(Track.SSS, "SixSidedStar", minLevel: 80).AddAssociatedActions(AID.SixSidedStar); - - def.Define(Track.Meditation).As("Meditate") - .AddOption(MeditationStrategy.Safe, "Use out of combat, during countdown, or if no enemies are targetable") - .AddOption(MeditationStrategy.Greedy, "Allow using when primary enemy is targetable, but out of range") - .AddOption(MeditationStrategy.Force, "Use even if enemy is in melee range") - .AddOption(MeditationStrategy.Delay, "Do not use") - .AddAssociatedActions(AID.SteeledMeditation); - def.DefineSimple(Track.FormShift, "FormShift", minLevel: 52).AddAssociatedActions(AID.FormShift); + // buffs + def.DefineSimple(Track.BH, "BH", minLevel: 70, uiPriority: 99).AddAssociatedActions(AID.Brotherhood); - def.Define(Track.FiresReply).As("FiresReply") + def.Define(Track.RoF).As("RoF", uiPriority: 96) + .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.Define(Track.FiresReply).As("FiresReply", uiPriority: 95) .AddOption(FRStrategy.Automatic, "Use after Opo GCD", minLevel: 100) .AddOption(FRStrategy.Ranged, "Use when out of melee range, or if about to expire", minLevel: 100) .AddOption(FRStrategy.Force, "Use ASAP", minLevel: 100) .AddOption(FRStrategy.Delay, "Do not use", minLevel: 100) .AddAssociatedActions(AID.FiresReply); - def.Define(Track.Nadi).As("Nadi") - .AddOption(NadiStrategy.Automatic, "Automatically choose best nadi (double lunar opener, otherwise alternate)", minLevel: 60) - .AddOption(NadiStrategy.Lunar, "Lunar", minLevel: 60) - .AddOption(NadiStrategy.Solar, "Solar", minLevel: 60); - - 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, uiPriority: 94).AddAssociatedActions(AID.RiddleOfWind); + def.Define(Track.WindsReply).As("WindsReply", uiPriority: 93) + .AddOption(WRStrategy.Automatic, "Use out of melee range, or if about to expire", minLevel: 96) + .AddOption(WRStrategy.Force, "Use ASAP", minLevel: 96) + .AddOption(WRStrategy.PreDowntime, "Ensure usage at least 2 GCDs before next downtime", minLevel: 96) + .AddAssociatedActions(AID.WindsReply); - def.DefineSimple(Track.RoW, "RoW", minLevel: 72).AddAssociatedActions(AID.RiddleOfWind); - def.Define(Track.PB).As("PB") + // PB-related settings + def.Define(Track.PB).As("PB", uiPriority: 89) .AddOption(PBStrategy.Automatic, "Automatically use after Opo before or during Riddle of Fire", minLevel: 50) .AddOption(PBStrategy.ForceOpo, "Use ASAP after next Opo", minLevel: 50) .AddOption(PBStrategy.Force, "Use ASAP", minLevel: 50) @@ -128,14 +128,11 @@ public static RotationModuleDefinition Definition() .AddOption(PBStrategy.DowntimeSolar, "Downtime prep: Solar", minLevel: 60, effect: 39) .AddOption(PBStrategy.DowntimeLunar, "Downtime prep: Lunar", minLevel: 60, effect: 39) .AddAssociatedActions(AID.PerfectBalance); - - def.DefineSimple(Track.BH, "BH", minLevel: 70).AddAssociatedActions(AID.Brotherhood); - def.Define(Track.TC).As("TC") - .AddOption(TCStrategy.None, "Do not use", minLevel: 35) - .AddOption(TCStrategy.GapClose, "Use if outside melee range", minLevel: 35) - .AddAssociatedActions(AID.Thunderclap); - - def.Define(Track.Blitz).As("Blitz") + def.Define(Track.Nadi).As("Nadi", uiPriority: 88) + .AddOption(NadiStrategy.Automatic, "Automatically choose best nadi (double lunar opener, otherwise alternate)", minLevel: 60) + .AddOption(NadiStrategy.Lunar, "Lunar", minLevel: 60) + .AddOption(NadiStrategy.Solar, "Solar", minLevel: 60); + def.Define(Track.Blitz).As("Blitz", uiPriority: 87) .AddOption(BlitzStrategy.Automatic, "Use ASAP", minLevel: 60) .AddOption(BlitzStrategy.RoF, "Hold blitz until Riddle of Fire is active", minLevel: 60) .AddOption(BlitzStrategy.Multi, "Hold blitz until at least two targets will be hit", minLevel: 60) @@ -143,13 +140,38 @@ public static RotationModuleDefinition Definition() .AddOption(BlitzStrategy.Delay, "Do not use", minLevel: 60) .AddAssociatedActions(AID.ElixirField, AID.FlintStrike, AID.TornadoKick, AID.ElixirBurst, AID.RisingPhoenix, AID.PhantomRush); - def.Define(Track.Engage).As("Engage") + // downtime stuff + def.DefineSimple(Track.SSS, "SixSidedStar", minLevel: 80, uiPriority: 79).AddAssociatedActions(AID.SixSidedStar); + def.DefineSimple(Track.FormShift, "FormShift", minLevel: 52, uiPriority: 78).AddAssociatedActions(AID.FormShift); + def.Define(Track.Meditation).As("Meditate", uiPriority: 77) + .AddOption(MeditationStrategy.Safe, "Use out of combat, during countdown, or if no enemies are targetable") + .AddOption(MeditationStrategy.Greedy, "Allow using when primary enemy is targetable, but out of range") + .AddOption(MeditationStrategy.Force, "Use even if enemy is in melee range") + .AddOption(MeditationStrategy.Delay, "Do not use") + .AddAssociatedActions(AID.SteeledMeditation); + + // other utils + def.Define(Track.TC).As("TC", uiPriority: 69) + .AddOption(TCStrategy.None, "Do not use", minLevel: 35) + .AddOption(TCStrategy.GapClose, "Use if outside melee range", minLevel: 35) + .AddAssociatedActions(AID.Thunderclap); + + def.Define(Track.Potion).As("Pot", uiPriority: 59) + .AddOption(PotionStrategy.Manual, "Do not automatically use") + .AddOption(PotionStrategy.PreBuffs, "Use ~4 GCDs before raid buff window") + .AddOption(PotionStrategy.Now, "Use ASAP"); + + def.Define(Track.Engage).As("Engage", uiPriority: 49) .AddOption(EngageStrategy.TC, "Thunderclap to target") .AddOption(EngageStrategy.Sprint, "Sprint to melee range") .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); + def.DefineSimple(Track.TN, "TrueNorth", minLevel: 50, uiPriority: 48).AddAssociatedActions(AID.TrueNorth); + def.Define(Track.Positional).As("Pos (AI)", uiPriority: 45) + .AddOption(PositionalStrategy.Automatic, "Tell AI mode to navigate to hit positionals") + .AddOption(PositionalStrategy.Ignore, "Tell AI mode to ignore positionals") + .AddAssociatedActions(AID.Demolish, AID.SnapPunch, AID.PouncingCoeurl); return def; } @@ -191,6 +213,8 @@ public enum Form { None, OpoOpo, Raptor, Coeurl } protected override float GetCastTime(AID aid) => 0; + public float EffectiveDowntimeIn => MathF.Max(0, DowntimeIn - GetApplicationDelay(AID.SixSidedStar)); + private (AID action, bool isTargeted) GetCurrentBlitz() { if (BeastCount != 3) @@ -344,7 +368,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) UseBlitz(strategy, currentBlitz); FiresReply(strategy); - WindsReply(); + WindsReply(strategy); if (UseAOE) { @@ -379,14 +403,15 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) PushGCD(AID.SixSidedStar, primaryTarget, GCDPriority.SSS); break; case OffensiveStrategy.Automatic: - if (DowntimeIn > 0 && !CanFitGCD(DowntimeIn - GetApplicationDelay(AID.SixSidedStar), 1)) + if (EffectiveDowntimeIn > 0 && !CanFitGCD(EffectiveDowntimeIn, 1)) PushGCD(AID.SixSidedStar, primaryTarget, GCDPriority.SSS); break; } Prep(strategy); - var pos = NextPositional; + var pos = strategy.Option(Track.Positional).As() == PositionalStrategy.Automatic ? NextPositional : (Positional.Any, false); + UpdatePositionals(primaryTarget, ref pos, TrueNorthLeft > GCD); GoalZoneCombined(strategy, 3, Hints.GoalAOECircle(5), AID.ArmOfTheDestroyer, AOEBreakpoint, positional: pos.Item1, maximumActionRange: 20); @@ -664,7 +689,7 @@ private void FiresReply(StrategyValues strategy) PushGCD(AID.FiresReply, BestRangedTarget, prio); } - private void WindsReply() + private void WindsReply(StrategyValues strategy) { if (WindsReplyLeft <= GCD) return; @@ -676,6 +701,17 @@ private void WindsReply() if (FireLeft > GCD && !CanFitGCD(FireLeft, 1) || !CanFitGCD(WindsReplyLeft, 1)) prio = GCDPriority.WindsReply; + switch (strategy.Option(Track.WindsReply).As()) + { + case WRStrategy.Force: + prio = GCDPriority.WindsReply; + break; + case WRStrategy.PreDowntime: + if (EffectiveDowntimeIn < WindsReplyLeft && !CanFitGCD(EffectiveDowntimeIn, 2)) + prio = GCDPriority.WindsReply; + break; + } + PushGCD(AID.WindsReply, BestLineTarget, prio); } diff --git a/BossMod/Autorotation/Standard/xan/Ranged/MCH.cs b/BossMod/Autorotation/Standard/xan/Ranged/MCH.cs index 9e5c6daa19..07ddd4b0dc 100644 --- a/BossMod/Autorotation/Standard/xan/Ranged/MCH.cs +++ b/BossMod/Autorotation/Standard/xan/Ranged/MCH.cs @@ -179,7 +179,7 @@ public override void Exec(StrategyValues strategy, Enemy? primaryTarget) private void OGCD(StrategyValues strategy, Enemy? primaryTarget) { - if (CountdownRemaining == null && !Player.InCombat && Player.DistanceToHitbox(primaryTarget) <= 25 && ReassembleLeft == 0 && !Overheated && AlwaysReassemble(NextGCD)) + if (CountdownRemaining == null && !Player.InCombat && Player.DistanceToHitbox(primaryTarget) <= 25 && ReassembleLeft == 0 && ShouldReassemble(strategy, primaryTarget)) PushGCD(AID.Reassemble, Player, priority: 50); if (!Player.InCombat || primaryTarget == null) diff --git a/BossMod/BossModule/AIHints.cs b/BossMod/BossModule/AIHints.cs index 674903e111..180828d3a2 100644 --- a/BossMod/BossModule/AIHints.cs +++ b/BossMod/BossModule/AIHints.cs @@ -23,6 +23,7 @@ public class Enemy(Actor actor, int priority, bool shouldBeTanked) public bool ShouldBeInterrupted; // if set and enemy is casting interruptible spell, some ranged/tank will try to interrupt public bool ShouldBeStunned; // if set, AI will stun if possible public bool StayAtLongRange; // if set, players with ranged attacks don't bother coming closer than max range (TODO: reconsider) + public bool Spikes; // if set, autoattacks will be prevented } public enum SpecialMode @@ -202,8 +203,13 @@ public void InitPathfindMap(Pathfinding.Map map) public int NumPriorityTargetsInAOECone(WPos origin, float radius, WDir direction, Angle halfAngle) => NumPriorityTargetsInAOE(a => TargetInAOECone(a.Actor, origin, radius, direction, halfAngle)); public int NumPriorityTargetsInAOERect(WPos origin, WDir direction, float lenFront, float halfWidth, float lenBack = 0) => NumPriorityTargetsInAOE(a => TargetInAOERect(a.Actor, origin, direction, lenFront, halfWidth, lenBack)); public bool TargetInAOECircle(Actor target, WPos origin, float radius) => target.Position.InCircle(origin, radius + target.HitboxRadius); - public bool TargetInAOECone(Actor target, WPos origin, float radius, WDir direction, Angle halfAngle) => target.Position.InCircleCone(origin, radius + target.HitboxRadius, direction, halfAngle); - public bool TargetInAOERect(Actor target, WPos origin, WDir direction, float lenFront, float halfWidth, float lenBack = 0) => target.Position.InRect(origin, direction, lenFront + target.HitboxRadius, lenBack, halfWidth); + public bool TargetInAOECone(Actor target, WPos origin, float radius, WDir direction, Angle halfAngle) => Intersect.CircleCone(target.Position, target.HitboxRadius, origin, radius, direction, halfAngle); + public bool TargetInAOERect(Actor target, WPos origin, WDir direction, float lenFront, float halfWidth, float lenBack = 0) + { + var rectCenterOffset = (lenFront - lenBack) / 2; + var rectCenter = origin + direction * rectCenterOffset; + return Intersect.CircleRect(target.Position, target.HitboxRadius, rectCenter, direction, halfWidth, (lenFront + lenBack) / 2); + } // goal zones // simple goal zone that returns 1 if target is in range, useful for single-target actions diff --git a/BossMod/BossModule/AIHintsBuilder.cs b/BossMod/BossModule/AIHintsBuilder.cs index 2960b3778c..94deae8d8b 100644 --- a/BossMod/BossModule/AIHintsBuilder.cs +++ b/BossMod/BossModule/AIHintsBuilder.cs @@ -63,8 +63,7 @@ public void Update(AIHints hints, int playerSlot, bool moveImminent) // fill list of potential targets from world state private void FillEnemies(AIHints hints, bool playerIsDefaultTank) { - var playerInFate = _ws.Client.ActiveFate.ID != 0 && _ws.Party.Player()?.Level <= Service.LuminaRow(_ws.Client.ActiveFate.ID)?.ClassJobLevelMax; - var allowedFateID = playerInFate ? _ws.Client.ActiveFate.ID : 0; + var allowedFateID = Utils.IsPlayerSyncedToFate(_ws) ? _ws.Client.ActiveFate.ID : 0; foreach (var actor in _ws.Actors.Where(a => a.IsTargetable && !a.IsAlly && !a.IsDead)) { var index = actor.CharacterSpawnIndex; @@ -85,7 +84,7 @@ private void FillEnemies(AIHints hints, bool playerIsDefaultTank) private void CalculateAutoHints(AIHints hints, Actor player) { - var inFate = _ws.Client.ActiveFate.ID != 0 && player.Level <= Service.LuminaRow(_ws.Client.ActiveFate.ID)?.ClassJobLevelMax; + var inFate = Utils.IsPlayerSyncedToFate(_ws); var center = inFate ? _ws.Client.ActiveFate.Center : player.PosRot.XYZ(); var (e, bitmap) = Obstacles.Find(center); var resolution = bitmap?.PixelSize ?? 0.5f; @@ -140,18 +139,18 @@ private void CalculateAutoHints(AIHints hints, Actor player) var finishAt = _ws.FutureTime(aoe.Caster.CastInfo.NPCRemainingTime); if (aoe.IsCharge) { - hints.AddForbiddenZone(ShapeDistance.Rect(aoe.Caster.Position, target, ((AOEShapeRect)aoe.Shape).HalfWidth), finishAt); + hints.AddForbiddenZone(ShapeDistance.Rect(aoe.Caster.Position, target, ((AOEShapeRect)aoe.Shape).HalfWidth), finishAt, aoe.Caster.InstanceID); } else if (aoe.Shape is AOEShapeCone cone) { // not sure how best to adjust cone shape distance to account for quantization error - we just pretend it is being cast from MaxError units "behind" the reported position and increase radius similarly var adjustedSourcePos = target + rot.ToDirection() * -MaxError; var adjustedRadius = cone.Radius + MaxError * 2; - hints.AddForbiddenZone(ShapeDistance.Cone(adjustedSourcePos, adjustedRadius, rot, cone.HalfAngle), finishAt); + hints.AddForbiddenZone(ShapeDistance.Cone(adjustedSourcePos, adjustedRadius, rot, cone.HalfAngle), finishAt, aoe.Caster.InstanceID); } else { - hints.AddForbiddenZone(aoe.Shape, target, rot, finishAt); + hints.AddForbiddenZone(aoe.Shape, target, rot, finishAt, aoe.Caster.InstanceID); } } } diff --git a/BossMod/BossModule/AIHintsVisualizer.cs b/BossMod/BossModule/AIHintsVisualizer.cs index 9d41d634a2..2a644dec3d 100644 --- a/BossMod/BossModule/AIHintsVisualizer.cs +++ b/BossMod/BossModule/AIHintsVisualizer.cs @@ -1,4 +1,5 @@ -using BossMod.Pathfinding; +using BossMod.Autorotation.xan.AI; +using BossMod.Pathfinding; using ImGuiNET; namespace BossMod; @@ -9,9 +10,12 @@ public class AIHintsVisualizer(AIHints hints, WorldState ws, Actor player, float private MapVisualizer? _pathfindVisualizer; private readonly NavigationDecision.Context _naviCtx = new(); private NavigationDecision _navi; + private readonly TrackPartyHealth _partyHealth = new(ws); public void Draw(UITree tree) { + _partyHealth.Update(hints); + foreach (var _1 in tree.Node("Potential targets", hints.PotentialTargets.Count == 0)) { tree.LeafNodes(hints.PotentialTargets, e => $"[{e.Priority}] {e.Actor} (str={e.AttackStrength:f2}), dist={(e.Actor.Position - player.Position).Length():f2}, tank={e.ShouldBeTanked}/{e.PreferProvoking}/{e.DesiredPosition}/{e.DesiredRotation}"); @@ -38,6 +42,13 @@ public void Draw(UITree tree) { tree.LeafNodes(hints.PredictedDamage, d => $"[{string.Join(", ", ws.Party.WithSlot().IncludedInMask(d.players).Select(ia => ia.Item2.Name))}], at {Math.Max(0, (d.activation - ws.CurrentTime).TotalSeconds):f3}"); } + foreach (var _1 in tree.Node("Party health")) + { + var ph = _partyHealth.PartyHealth; + ImGui.TextUnformatted($"Total: {ph.Count}"); + ImGui.TextUnformatted($"Average: {ph.Avg * 100:f2} / stddev {ph.StdDev * 100:f2}"); + ImGui.TextUnformatted($"Lowest HP ally: {ws.Party[ph.LowestHPSlot]}"); + } foreach (var _1 in tree.Node("Planned actions", hints.ActionsToExecute.Entries.Count == 0)) { tree.LeafNodes(hints.ActionsToExecute.Entries, e => $"{e.Action} @ {e.Target} (priority {e.Priority})"); diff --git a/BossMod/BossModule/ZoneModule.cs b/BossMod/BossModule/ZoneModule.cs index e468ae6ab5..981777bb50 100644 --- a/BossMod/BossModule/ZoneModule.cs +++ b/BossMod/BossModule/ZoneModule.cs @@ -23,6 +23,7 @@ public virtual void Update() { } public virtual void CalculateAIHints(int playerSlot, Actor player, AIHints hints) { } // note: this is called after framework automatically fills auto-detected hints public virtual bool WantDrawExtra() => false; // return true if it wants to draw something in a separate window public virtual void DrawExtra() { } + public virtual string WindowName() => ""; public void DrawGlobalHints() { diff --git a/BossMod/BossModule/ZoneModuleWindow.cs b/BossMod/BossModule/ZoneModuleWindow.cs index f696183827..a4b156b401 100644 --- a/BossMod/BossModule/ZoneModuleWindow.cs +++ b/BossMod/BossModule/ZoneModuleWindow.cs @@ -1,4 +1,6 @@ -namespace BossMod; +using Dalamud.Utility; + +namespace BossMod; public class ZoneModuleWindow : UIWindow { @@ -13,6 +15,13 @@ public class ZoneModuleWindow : UIWindow public override void PreOpenCheck() { IsOpen = _zmm.ActiveModule?.WantDrawExtra() ?? false; + if (IsOpen) + { + var title = _zmm.ActiveModule!.WindowName(); + if (title.IsNullOrEmpty()) + title = "Zone module###Zone module"; + WindowName = title; + } } public override void Draw() diff --git a/BossMod/Data/ClientState.cs b/BossMod/Data/ClientState.cs index e5e4e48d67..b33d14ba6d 100644 --- a/BossMod/Data/ClientState.cs +++ b/BossMod/Data/ClientState.cs @@ -58,6 +58,19 @@ public record struct DutyAction(ActionID Action, byte CurCharges, byte MaxCharge public Pet ActivePet; public ulong FocusTargetId; public Angle ForcedMovementDirection; // used for temporary misdirection and spinning states + public uint[] ContentKeyValueData = new uint[6]; // used for content-specific persistent player attributes, like bozja resistance rank + + public uint GetContentValue(uint key) => ContentKeyValueData[0] == key + ? ContentKeyValueData[1] + : ContentKeyValueData[2] == key + ? ContentKeyValueData[3] + : ContentKeyValueData[4] == key + ? ContentKeyValueData[5] + : 0; + + public uint ElementalLevel => GetContentValue(4); + public uint ElementalLevelSynced => GetContentValue(2); + public uint ResistanceRank => GetContentValue(5); public int ClassJobLevel(Class c) { @@ -355,4 +368,21 @@ protected override void Exec(WorldState ws) } public override void Write(ReplayRecorder.Output output) => output.EmitFourCC("CLFD"u8).Emit(Value); } + + public Event ContentKVDataChanged = new(); + public sealed record class OpContentKVDataChange(uint[] Value) : WorldState.Operation + { + public readonly uint[] Value = Value; + protected override void Exec(WorldState ws) + { + ws.Client.ContentKeyValueData = Value; + ws.Client.ContentKVDataChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("CLKV"u8); + foreach (var val in Value) + output.Emit(val); + } + } } diff --git a/BossMod/Framework/Utils.cs b/BossMod/Framework/Utils.cs index c8dbf26a7b..743d2ac837 100644 --- a/BossMod/Framework/Utils.cs +++ b/BossMod/Framework/Utils.cs @@ -50,6 +50,20 @@ public static string ObjectKindString(IGameObject obj) public static unsafe ulong SceneObjectFlags(FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object* o) => ReadField(o, 0x38); + public static bool IsPlayerSyncedToFate(WorldState world) + { + if (world.Client.ActiveFate.ID == 0) + return false; + + var fate = Service.LuminaRow(world.Client.ActiveFate.ID); + if (fate == null) + return false; + + return fate.Value.EurekaFate == 1 + ? world.Client.ElementalLevelSynced <= fate.Value.ClassJobLevelMax + : world.Party.Player()?.Level <= fate.Value.ClassJobLevelMax; + } + // lumina extensions public static int FindIndex(this Lumina.Excel.Collection collection, Func predicate) where T : struct { diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs index c7157bc230..b012615d74 100644 --- a/BossMod/Framework/WorldStateGameSync.cs +++ b/BossMod/Framework/WorldStateGameSync.cs @@ -667,6 +667,19 @@ private unsafe void UpdateClient() var forcedMovementDir = MovementOverride.ForcedMovementDirection->Radians(); if (_ws.Client.ForcedMovementDirection != forcedMovementDir) _ws.Execute(new ClientState.OpForcedMovementDirectionChange(forcedMovementDir)); + + var contentKeyValue = uiState->PlayerState.ContentKeyValueData; + var ckArray = new uint[] + { + contentKeyValue[0].Item1, + contentKeyValue[0].Item2, + contentKeyValue[1].Item1, + contentKeyValue[1].Item2, + contentKeyValue[2].Item1, + contentKeyValue[2].Item2 + }; + if (!MemoryExtensions.SequenceEqual(ckArray, _ws.Client.ContentKeyValueData)) + _ws.Execute(new ClientState.OpContentKVDataChange(ckArray)); } private unsafe void UpdateDeepDungeon() diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs index 143cc15c3b..21d9aaee1c 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs @@ -23,7 +23,7 @@ public override void OnActorModelStateChange(Actor actor, byte modelState, byte { Activation = WorldState.FutureTime(9.1f); AOEs.Add(new(_shape, actor.Position, actor.Rotation, Activation)); - DangerousSpots.Set((int)MathF.Round((Angle.FromDirection(actor.Position - Module.Center).Deg + 180) / 45) % 8); + DangerousSpots.Set((int)MathF.Round((-Angle.FromDirection(actor.Position - Module.Center).Deg + 180) / 45) % 8); } } @@ -172,7 +172,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (_spreadStack?.Stacks.Count > 0) spreadSpot &= 4; // stack on close spot - var direction = (~_seenDangerSpot).LowestSetBit() * 45.Degrees() + (spreadSpot >= 4 ? 0 : 180).Degrees(); + var direction = (4 - (~_seenDangerSpot).LowestSetBit()) % 4 * 45.Degrees() + (spreadSpot >= 4 ? 0 : 180).Degrees(); spreadSpot &= 3; direction += spreadSpot switch { diff --git a/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/EOFloorModule.cs b/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/EOFloorModule.cs index e069ca33ff..1a3da30811 100644 --- a/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/EOFloorModule.cs +++ b/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/EOFloorModule.cs @@ -170,7 +170,7 @@ protected override void OnCastFinished(Actor actor) // setting target to forbidden when it gains the spikes status is too late case AID.GelidCharge: case AID.SmolderingScales: - ForbiddenTargets.Add((actor, World.FutureTime(10))); + Spikes.Add((actor, World.FutureTime(10))); break; } } @@ -219,7 +219,7 @@ protected override void OnStatusLose(Actor actor, ActorStatus status) { case (uint)SID.IceSpikes: case (uint)SID.BlazeSpikes: - ForbiddenTargets.RemoveAll(t => t.Actor == actor); + Spikes.RemoveAll(t => t.Actor == actor); break; } } diff --git a/BossMod/Modules/Global/DeepDungeon/AutoClear.cs b/BossMod/Modules/Global/DeepDungeon/AutoClear.cs index a4d0de13e6..7b483879bb 100644 --- a/BossMod/Modules/Global/DeepDungeon/AutoClear.cs +++ b/BossMod/Modules/Global/DeepDungeon/AutoClear.cs @@ -55,7 +55,7 @@ public abstract class AutoClear : ZoneModule private readonly List Gazes = []; protected readonly List Interrupts = []; protected readonly List Stuns = []; - protected readonly List<(Actor Actor, DateTime Timeout)> ForbiddenTargets = []; + protected readonly List<(Actor Actor, DateTime Timeout)> Spikes = []; protected readonly List HintDisabled = []; private readonly List LOS = []; private readonly List IgnoreTraps = []; @@ -228,7 +228,7 @@ private void ClearState() Gazes.Clear(); Interrupts.Clear(); Stuns.Clear(); - ForbiddenTargets.Clear(); + Spikes.Clear(); HintDisabled.Clear(); LOS.Clear(); Walls.Clear(); @@ -293,6 +293,8 @@ private bool OpenSilver public override bool WantDrawExtra() => _config.EnableMinimap && !Palace.IsBossFloor; + public sealed override string WindowName() => "VBM DD minimap###Zone module"; + public override void DrawExtra() { var player = World.Party.Player(); @@ -445,6 +447,18 @@ public override void CalculateAIHints(int playerSlot, Actor player, AIHints hint revealedTraps.Add(ShapeDistance.Circle(a.Position, 2)); } + var fullClear = false; + if (_config.FullClear) + { + var unexplored = Array.FindIndex(Palace.Rooms, d => (byte)d > 0 && !d.HasFlag(RoomFlags.Revealed)); + if (unexplored > 0) + { + DesiredRoom = unexplored; + fullClear = true; + } + } + + if (_config.TrapHints && _trapsHidden) { var traps = _trapsCurrentZone.Where(t => t.InCircle(player.Position, 30) && !IgnoreTraps.Any(b => b.AlmostEqual(t, 1))).Select(t => ShapeDistance.Circle(t, 2)).ToList(); @@ -479,9 +493,10 @@ public override void CalculateAIHints(int playerSlot, Actor player, AIHints hint if (!player.InCombat && _config.AutoPassage && Palace.PassageActive) { - DesiredRoom = Array.FindIndex(Palace.Rooms, d => d.HasFlag(RoomFlags.Passage)); + if (DesiredRoom == 0) + DesiredRoom = Array.FindIndex(Palace.Rooms, d => d.HasFlag(RoomFlags.Passage)); - if (passage is Actor c) + if (passage is Actor c && !fullClear) { hints.GoalZones.Add(hints.GoalSingleTarget(c.Position, 2, 0.5f)); // give pathfinder a little help lmao @@ -621,10 +636,10 @@ private void DrawAOEs(int playerSlot, Actor player, AIHints hints) hints.AddForbiddenZone(new AOEShapeCircle(kb.Radius), kb.Source.Position, default, castFinish); }); - IterAndExpire(ForbiddenTargets, t => t.Timeout <= World.CurrentTime, t => + IterAndExpire(Spikes, t => t.Timeout <= World.CurrentTime, t => { if (hints.FindEnemy(t.Actor) is { } enemy) - enemy.Priority = AIHints.Enemy.PriorityForbidden; + enemy.Spikes = true; }); } @@ -670,14 +685,15 @@ private void HandleFloorPathfind(Actor player, AIHints hints) hints.GoalZones.Add(p => { var pp = player.Position; - return d switch + var improvement = d switch { Direction.North => pp.Z - p.Z, Direction.South => p.Z - pp.Z, Direction.East => p.X - pp.X, Direction.West => pp.X - p.X, _ => 0, - } * 0.001f; + }; + return improvement > 10 ? 10 : 0; }); } diff --git a/BossMod/Modules/Global/DeepDungeon/Config.cs b/BossMod/Modules/Global/DeepDungeon/Config.cs index 7d197f6e1b..e126593501 100644 --- a/BossMod/Modules/Global/DeepDungeon/Config.cs +++ b/BossMod/Modules/Global/DeepDungeon/Config.cs @@ -42,4 +42,7 @@ public enum ClearBehavior public bool SilverCoffer = true; [PropertyDisplay("Open bronze coffers")] public bool BronzeCoffer = true; + + [PropertyDisplay("Reveal all rooms before proceeding to next floor")] + public bool FullClear = false; } diff --git a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/PalaceFloorModule.cs b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/PalaceFloorModule.cs index 57b2dacefe..8aa2214722 100644 --- a/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/PalaceFloorModule.cs +++ b/BossMod/Modules/Heavensward/DeepDungeon/PalaceOfTheDead/PalaceFloorModule.cs @@ -65,8 +65,7 @@ protected override void OnStatusGain(Actor actor, ActorStatus status) { case SID.BlazeSpikes: case SID.IceSpikes: - if (Palace.Floor > 60) - ForbiddenTargets.Add((actor, World.FutureTime(10))); + Spikes.Add((actor, World.FutureTime(10))); break; } } @@ -77,7 +76,7 @@ protected override void OnStatusLose(Actor actor, ActorStatus status) { case SID.BlazeSpikes: case SID.IceSpikes: - ForbiddenTargets.RemoveAll(t => t.Actor == actor); + Spikes.RemoveAll(t => t.Actor == actor); break; } } diff --git a/BossMod/Modules/Stormblood/Foray/NM/Barong.cs b/BossMod/Modules/Stormblood/Foray/NM/Barong.cs new file mode 100644 index 0000000000..db1f05468f --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/Barong.cs @@ -0,0 +1,18 @@ +//namespace BossMod.Stormblood.Foray.NM.Barong; +//public enum OID : uint +//{ +// Boss = 0x2746, +// Helper = 0x233C, +//} + +//class BarongStates : StateMachineBuilder +//{ +// public BarongStates(BossModule module) : base(module) +// { +// TrivialPhase(); +// } +//} + +//[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7965)] +//public class Barong(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Stormblood/Foray/NM/Ceto.cs b/BossMod/Modules/Stormblood/Foray/NM/Ceto.cs new file mode 100644 index 0000000000..b9be6eea7d --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/Ceto.cs @@ -0,0 +1,53 @@ +namespace BossMod.Stormblood.Foray.NM.Ceto; + +public enum OID : uint +{ + Boss = 0x2765, // R5.000, x1 + FaithlessGuard = 0x2767, // R2.000, x0 (spawn during fight) + LifelessSlave1 = 0x2766, // R2.700, x1 + LifelessSlave2 = 0x2785, // R2.700, x1 + LifelessSlave3 = 0x2784, // R2.700, x1 +} + +public enum AID : uint +{ + SickleStrike = 15466, // Boss->player, 3.5s cast, single-target + PetrifactionBoss = 15469, // Boss->self, 450 circle + AbyssalReaper = 15468, // Boss->self, 4.0s cast, range 18 circle + PetrifactionAdds = 15475, // LifelessSlave1/LifelessSlave2/LifelessSlave3->self, 4.0s cast, range 50 circle + CircleOfFlames = 15472, // FaithlessGuard->location, 3.0s cast, range 5 circle + TailSlap = 15471, // FaithlessGuard->self, 3.0s cast, range 12 120-degree cone + Petrattraction = 15473, // FaithlessGuard->2783, 3.0s cast, single-target + CircleBlade = 15470, // FaithlessGuard->self, 3.0s cast, range 7 circle +} + +class SickleStrike(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.SickleStrike)); +class PetrifactionBoss(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.PetrifactionBoss), range: 50); +class PetrifactionAdds(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.PetrifactionAdds), range: 50); +class AbyssalReaper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AbyssalReaper), new AOEShapeCircle(18)); +class CircleOfFlames(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.CircleOfFlames), 5); +class TailSlap(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TailSlap), new AOEShapeCone(12, 60.Degrees())); +class Petrattraction(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.Petrattraction), 50, kind: Kind.TowardsOrigin); +class CircleBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CircleBlade), new AOEShapeCircle(7)); +class Adds(BossModule module) : Components.Adds(module, (uint)OID.FaithlessGuard); + +class CetoStates : StateMachineBuilder +{ + public CetoStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7955, Contributors = "xan")] +public class Ceto(WorldState ws, Actor primary) : BossModule(ws, primary, new(747.8959f, -878.8765f), new ArenaBoundsCircle(80, MapResolution: 1)); + diff --git a/BossMod/Modules/Stormblood/Foray/NM/Daphne.cs b/BossMod/Modules/Stormblood/Foray/NM/Daphne.cs new file mode 100644 index 0000000000..eac8a65344 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/Daphne.cs @@ -0,0 +1,42 @@ +namespace BossMod.Stormblood.Foray.NM.Daphne; + +public enum OID : uint +{ + Boss = 0x2744, // R6.875, x1 + Tentacle = 0x276E, // R7.200, x0 (spawn during fight) + Helper1 = 0x276A, // R0.500, x0 (spawn during fight) + Helper2 = 0x276B, // R0.500, x0 (spawn during fight) +} + +public enum AID : uint +{ + SpellwindCast = 15031, // Boss->self, 4.0s cast, single-target + SpellwindAOE = 15032, // Helper1->location, no cast, range 40 circle + Upburst = 15025, // Tentacle->self, 3.5s cast, range 8 circle + RoilingReach = 15029, // Boss->self, 4.5s cast, range 32 width 7 cross + Wallop = 15027, // Tentacle->self, 4.0s cast, range 50 width 7 rect + ChillingGlare = 15030, // Boss->self, 4.0s cast, range 40 circle +} + +class Spellwind(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.SpellwindCast)); +class Upburst(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Upburst), new AOEShapeCircle(8)); +class RoilingReach(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RoilingReach), new AOEShapeCross(32, 3.5f)); +class Wallop(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Wallop), new AOEShapeRect(50, 3.5f)); +class ChillingGlare(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.ChillingGlare)); + +class DaphneStates : StateMachineBuilder +{ + public DaphneStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7967, Contributors = "xan")] +public class Daphne(WorldState ws, Actor primary) : BossModule(ws, primary, new(207.8475f, -736.8179f), new ArenaBoundsCircle(80, MapResolution: 1)); + diff --git a/BossMod/Modules/Stormblood/Foray/NM/Frostmane.cs b/BossMod/Modules/Stormblood/Foray/NM/Frostmane.cs new file mode 100644 index 0000000000..3742dc2003 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/Frostmane.cs @@ -0,0 +1,19 @@ +//namespace BossMod.Stormblood.Foray.NM.Frostmane; + +//public enum OID : uint +//{ +// Boss = 0x2761, +// Helper = 0x233C, +//} + +//class FrostmaneStates : StateMachineBuilder +//{ +// public FrostmaneStates(BossModule module) : base(module) +// { +// TrivialPhase(); +// } +//} + +//[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 8072)] +//public class Frostmane(WorldState ws, Actor primary) : BossModule(ws, primary, new(-681.4338f, -243.3619f), new ArenaBoundsCircle(80, MapResolution: 1)); + diff --git a/BossMod/Modules/Stormblood/Foray/NM/KingGoldemar.cs b/BossMod/Modules/Stormblood/Foray/NM/KingGoldemar.cs new file mode 100644 index 0000000000..02108f2e9a --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/KingGoldemar.cs @@ -0,0 +1,18 @@ +//namespace BossMod.Stormblood.Foray.NM.KingGoldemar; +//public enum OID : uint +//{ +// Boss = 0x2745, +// Helper = 0x233C, +//} + +//class KingGoldemarStates : StateMachineBuilder +//{ +// public KingGoldemarStates(BossModule module) : base(module) +// { +// TrivialPhase(); +// } +//} + +//[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7966)] +//public class KingGoldemar(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Stormblood/Foray/NM/Leuke.cs b/BossMod/Modules/Stormblood/Foray/NM/Leuke.cs new file mode 100644 index 0000000000..35bbff52d8 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/Leuke.cs @@ -0,0 +1,18 @@ +//namespace BossMod.Stormblood.Foray.NM.Leuke; +//public enum OID : uint +//{ +// Boss = 0x273C, +// Helper = 0x233C, +//} + +//class LeukeStates : StateMachineBuilder +//{ +// public LeukeStates(BossModule module) : base(module) +// { +// TrivialPhase(); +// } +//} + +//[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 8068)] +//public class Leuke(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Stormblood/Foray/NM/Molech.cs b/BossMod/Modules/Stormblood/Foray/NM/Molech.cs new file mode 100644 index 0000000000..dd0293b6d7 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/Molech.cs @@ -0,0 +1,19 @@ +//namespace BossMod.Stormblood.Foray.NM.Molech; + +//public enum OID : uint +//{ +// Boss = 0x275D, +// Helper = 0x233C, +//} + +//class MolechStates : StateMachineBuilder +//{ +// public MolechStates(BossModule module) : base(module) +// { +// TrivialPhase(); +// } +//} + +//[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 8070)] +//public class Molech(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Stormblood/Foray/NM/Ovni.cs b/BossMod/Modules/Stormblood/Foray/NM/Ovni.cs new file mode 100644 index 0000000000..0b353e10de --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/Ovni.cs @@ -0,0 +1,78 @@ +namespace BossMod.Stormblood.Foray.NM.Ovni; + +public enum OID : uint +{ + Boss = 0x2685, // R16.000, x1 +} + +public enum AID : uint +{ + RockHard = 14786, // Boss->location, 3.0s cast, range 8 circle + TorrentialTorment = 14785, // Boss->self, 5.0s cast, range 40+R 45-degree cone + Fluorescence = 14789, // Boss->self, 3.0s cast, single-target + PullOfTheVoid = 14782, // Boss->self, 3.0s cast, range 30 circle + Megastorm = 14784, // Boss->self, 5.0s cast, range ?-40 donut + IonShower = 14787, // Boss->player, 5.0s cast, single-target + IonStorm = 14788, // Boss->players, no cast, range 20 circle + VitriolicBarrage = 14790, // Boss->self, 4.0s cast, range 25+R circle + ConcussiveOscillation = 14783, // Boss->self, 5.0s cast, range 24 circle +} + +public enum IconID : uint +{ + IonShower = 111, // player->self +} + +class PullOfTheVoid(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.PullOfTheVoid), 30, kind: Kind.TowardsOrigin, minDistanceBetweenHitboxes: true); +class Megastorm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Megastorm), new AOEShapeDonut(5, 40)); +class ConcussiveOscillation(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ConcussiveOscillation), new AOEShapeCircle(24)); +class VitriolicBarrage(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.VitriolicBarrage)); +class RockHard(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.RockHard), 8); +class TorrentialTorment(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TorrentialTorment), new AOEShapeCone(56, 22.5f.Degrees())); +class IonShower(BossModule module) : Components.GenericStackSpread(module, alwaysShowSpreads: true, raidwideOnResolve: false) +{ + private int _numCasts; + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if (iconID == (uint)IconID.IonShower && WorldState.Actors.Find(targetID) is { } target) + { + _numCasts = 0; + Spreads.Add(new(target, 20, WorldState.FutureTime(5))); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.IonStorm && ++_numCasts >= 3) + Spreads.Clear(); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Spreads.Any(s => s.Target == actor)) + // just gtfo from boss as far as possible + hints.GoalZones.Add(p => (p - Module.PrimaryActor.Position).LengthSq() > 1600 ? 100 : 0); + else + base.AddAIHints(slot, actor, assignment, hints); + } +} + +class OvniStates : StateMachineBuilder +{ + public OvniStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 8060, Contributors = "xan")] +public class Ovni(WorldState ws, Actor primary) : BossModule(ws, primary, new(266.1068f, -97.09414f), new ArenaBoundsCircle(80, MapResolution: 1)); + diff --git a/BossMod/Modules/Stormblood/Foray/NM/ProvenanceWatcher.cs b/BossMod/Modules/Stormblood/Foray/NM/ProvenanceWatcher.cs new file mode 100644 index 0000000000..e1732e80ee --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/NM/ProvenanceWatcher.cs @@ -0,0 +1,19 @@ +//namespace BossMod.Stormblood.Foray.NM.ProvenanceWatcher; + +//public enum OID : uint +//{ +// Boss = 0x2686, +// Helper = 0x233C, +//} + +//class ProvenanceWatcherStates : StateMachineBuilder +//{ +// public ProvenanceWatcherStates(BossModule module) : base(module) +// { +// TrivialPhase(); +// } +//} + +//[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 8061)] +//public class ProvenanceWatcher(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Pathfinding/ObstacleMaps/562.175.2.bmp b/BossMod/Pathfinding/ObstacleMaps/562.175.2.bmp index 17079651cb..1beb6798a4 100644 Binary files a/BossMod/Pathfinding/ObstacleMaps/562.175.2.bmp and b/BossMod/Pathfinding/ObstacleMaps/562.175.2.bmp differ diff --git a/BossMod/Pathfinding/ObstacleMaps/563.176.1.bmp b/BossMod/Pathfinding/ObstacleMaps/563.176.1.bmp index 9b5e685cfa..4657994c0f 100644 Binary files a/BossMod/Pathfinding/ObstacleMaps/563.176.1.bmp and b/BossMod/Pathfinding/ObstacleMaps/563.176.1.bmp differ diff --git a/BossMod/Pathfinding/ObstacleMaps/564.177.1.bmp b/BossMod/Pathfinding/ObstacleMaps/564.177.1.bmp index 59b311fa0a..92defcc032 100644 Binary files a/BossMod/Pathfinding/ObstacleMaps/564.177.1.bmp and b/BossMod/Pathfinding/ObstacleMaps/564.177.1.bmp differ diff --git a/BossMod/Pathfinding/ObstacleMaps/827.639.1.bmp b/BossMod/Pathfinding/ObstacleMaps/827.639.1.bmp new file mode 100644 index 0000000000..e23cc9ba23 Binary files /dev/null and b/BossMod/Pathfinding/ObstacleMaps/827.639.1.bmp differ diff --git a/BossMod/Pathfinding/ObstacleMaps/maplist.json b/BossMod/Pathfinding/ObstacleMaps/maplist.json index b5fb307176..1eafba39e2 100644 --- a/BossMod/Pathfinding/ObstacleMaps/maplist.json +++ b/BossMod/Pathfinding/ObstacleMaps/maplist.json @@ -3168,5 +3168,20 @@ "ViewHeight": 60, "Filename": "301.343.1.bmp" } + ], + "827.639": [ + { + "MinBoundsX": -950, + "MinBoundsY": 493, + "MinBoundsZ": -948, + "MaxBoundsX": 922.5, + "MaxBoundsY": 563, + "MaxBoundsZ": 16, + "OriginX": -950, + "OriginZ": -948, + "ViewWidth": 60, + "ViewHeight": 60, + "Filename": "827.639.1.bmp" + } ] } \ No newline at end of file diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index 8d839279ab..d58de0f42c 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -352,6 +352,7 @@ private ReplayParserLog(Input input, ReplayBuilder builder) [new("CPET"u8)] = ParseClientActivePet, [new("CLFT"u8)] = ParseClientFocusTarget, [new("CLFD"u8)] = ParseClientForcedMovementDirection, + [new("CLKV"u8)] = ParseClientContentKVData, [new("DDPG"u8)] = ParseDeepDungeonProgress, [new("DDMP"u8)] = ParseDeepDungeonMap, [new("DDPT"u8)] = ParseDeepDungeonParty, @@ -705,6 +706,14 @@ 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 ClientState.OpForcedMovementDirectionChange ParseClientForcedMovementDirection() => new(_input.ReadAngle()); + private ClientState.OpContentKVDataChange ParseClientContentKVData() => new([ + _input.ReadUInt(false), + _input.ReadUInt(false), + _input.ReadUInt(false), + _input.ReadUInt(false), + _input.ReadUInt(false), + _input.ReadUInt(false), + ]); 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()