diff --git a/BossMod/ActionQueue/ActionDefinition.cs b/BossMod/ActionQueue/ActionDefinition.cs index 6dbc849246..03af81e034 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) @@ -194,9 +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); + for (var i = PomanderID.Safety; i < PomanderID.Count; ++i) + RegisterDeepDungeon(new(ActionType.Pomander, (uint)i)); + for (var i = 1u; i <= 3; i++) + RegisterDeepDungeon(new(ActionType.Magicite, i)); } public void Dispose() @@ -340,7 +369,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); } @@ -395,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/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 61aabaa3ec..7b28280450 100644 --- a/BossMod/ActionQueue/ClassShared.cs +++ b/BossMod/ActionQueue/ClassShared.cs @@ -93,6 +93,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/Legacy/LegacyBRD.cs b/BossMod/Autorotation/Legacy/LegacyBRD.cs index 5e00fd82dd..7e0985424a 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 d18e76d841..8417cbb3da 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 = Math.Max(0.1f, _state.AnimationLockDelay); diff --git a/BossMod/Autorotation/Legacy/LegacyDRG.cs b/BossMod/Autorotation/Legacy/LegacyDRG.cs index e5a1bde9e5..9b441ca450 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 ce38f9fa93..f28fb9b438 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 5293bed934..7818c329fd 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 270ad4ff9a..a51a67e879 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 4f3e265938..d5934cef28 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/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..121ba710ed --- /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 readonly VersionedJSONSchema PlanSchema = BuildSchema(true); + public static readonly 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 07a1abcb94..cbafaadf73 100644 --- a/BossMod/Autorotation/PresetDatabase.cs +++ b/BossMod/Autorotation/PresetDatabase.cs @@ -39,10 +39,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) { @@ -74,13 +73,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/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index f0ae56acb3..c08e4e3f5a 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() => ""; @@ -119,6 +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(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/RotationModuleManager.cs b/BossMod/Autorotation/RotationModuleManager.cs index 9908d77090..db404af416 100644 --- a/BossMod/Autorotation/RotationModuleManager.cs +++ b/BossMod/Autorotation/RotationModuleManager.cs @@ -111,7 +111,7 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) { var m = ActiveModules[i]; 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); } } @@ -119,8 +119,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 }; @@ -132,6 +132,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; @@ -143,8 +184,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/Standard/StandardWAR.cs b/BossMod/Autorotation/Standard/StandardWAR.cs index 4fd027a534..674f8680d4 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") @@ -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/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 9633b265a5..ac3faec0ab 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/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 08ea89b172..3a3e92d8f8 100644 --- a/BossMod/Autorotation/Utility/ClassSMNUtility.cs +++ b/BossMod/Autorotation/Utility/ClassSMNUtility.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/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..5a54fcd8ba 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 @@ -130,27 +131,28 @@ 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) .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") .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 +163,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 +182,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 +217,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 +244,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 +251,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 +273,26 @@ 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 + + //Opener + Opener = 1000, //Opener } 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 +311,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 @@ -344,6 +353,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 @@ -353,7 +363,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; @@ -380,50 +391,26 @@ 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 int TargetsInRange() => Hints.NumPriorityTargetsInAOECircle(Player.Position, 25); //Returns the number of targets within 26-yalm radius around the player 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; + + //Are there enough targets in the general area? + if (TargetsInRange() < minimumTargetsForAOE) + { + return false; + } + float splashPriorityFunc(Actor actor) { var distanceToPlayer = actor.DistanceToHitbox(Player); - if (distanceToPlayer <= 24f) + if (distanceToPlayer < 26f) { var targetsInSplashRadius = 0; foreach (var enemy in Hints.PriorityTargets) @@ -447,18 +434,46 @@ float splashPriorityFunc(Actor actor) return false; } } + + private Actor? TargetChoice(StrategyValues.OptionRef strategy) => ResolveTargetOverride(strategy.Value); //Resolves the target choice based on the strategy + private Actor? FindBestAOETarget() + { + float AOEPriorityFunc(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 * 10 - actor.HPMP.CurHP * 0.01f; + } + return float.MinValue; + } + + var (BestAOETarget, bestPrio) = FindBetterTargetBy(null, 25f, AOEPriorityFunc); + return BestAOETarget; + } + private Actor? BestAOETarget => FindBestAOETarget(); // Find the best target for splash attack + //TODO: BestDOTTarget #endregion #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 - 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; @@ -492,7 +507,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 @@ -516,6 +534,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 +544,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) + if (forceST) BestST(TargetChoice(AOE) ?? primaryTarget); - if (AOEStrategy is AOEStrategy.ForceAOE) - BestAOE(TargetChoice(AOE) ?? BestAOETarget ?? primaryTarget); + if (forceAOE) + BestAOE(TargetChoice(AOE) ?? primaryTarget); } #endregion @@ -546,48 +566,32 @@ movingOption is CastingOption.Forbid && primaryTarget != null && isMoving) { - if (movementStrat is MovementStrategy.Allow) - { - //GCDs - if (!PlayerHasEffect(SID.Swiftcast, 10) || - !PlayerHasEffect(SID.Triplecast, 15)) - QueueGCD( - Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? BestPolyglot - : PlayerHasEffect(SID.Firestarter, 30) ? AID.Fire3 - : hasThunderhead ? BestThunder - : AID.Scathe, - Polyglots > 0 ? TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget - : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget - : hasThunderhead ? TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget - : primaryTarget, - GCDPriority.Moving1); - //OGCDs - if (ActionReady(AID.Swiftcast) && - !PlayerHasEffect(SID.Triplecast, 15)) - QueueOGCD(AID.Swiftcast, Player, GCDPriority.Moving2); - if (Unlocked(AID.Triplecast) && - CD(AID.Triplecast) <= 60 && - !PlayerHasEffect(SID.Triplecast, 15) && - !PlayerHasEffect(SID.Swiftcast, 10)) - QueueOGCD(AID.Triplecast, Player, GCDPriority.Moving3); - } - if (movementStrat is MovementStrategy.OnlyGCDs) + if (movementStrat is MovementStrategy.Allow + or MovementStrategy.AllowNoScathe + or MovementStrategy.OnlyGCDs) { - //GCDs - if (!PlayerHasEffect(SID.Swiftcast, 10) || - !PlayerHasEffect(SID.Triplecast, 15)) - QueueGCD( - Unlocked(TraitID.EnhancedPolyglot) && Polyglots > 0 ? BestPolyglot - : PlayerHasEffect(SID.Firestarter, 30) ? AID.Fire3 - : hasThunderhead ? BestThunder - : AID.Scathe, - Polyglots > 0 ? TargetChoice(polyglot) ?? BestAOETarget ?? primaryTarget - : PlayerHasEffect(SID.Firestarter, 30) ? TargetChoice(AOE) ?? primaryTarget - : hasThunderhead ? TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget - : primaryTarget, - GCDPriority.Moving1); + // 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 ?? BestAOETarget, + GCDPriority.Moving1); + + if (PlayerHasEffect(SID.Firestarter, 30)) + QueueGCD(AID.Fire3, + TargetChoice(AOE) ?? primaryTarget ?? BestAOETarget, + GCDPriority.Moving1); + + if (hasThunderhead) + QueueGCD(forceST ? BestThunderST : forceAOE ? BestThunderAOE : BestThunder, + TargetChoice(thunder) ?? primaryTarget ?? BestAOETarget, + GCDPriority.Moving1); + } } - if (movementStrat is MovementStrategy.OnlyOGCDs) + if (movementStrat is MovementStrategy.Allow + or MovementStrategy.AllowNoScathe + or MovementStrategy.OnlyOGCDs) { //OGCDs if (ActionReady(AID.Swiftcast) && @@ -597,64 +601,58 @@ movingOption is CastingOption.Forbid && !PlayerHasEffect(SID.Swiftcast, 10)) QueueOGCD(AID.Triplecast, Player, GCDPriority.Moving3); } - if (movementStrat is MovementStrategy.OnlyScathe) + if (movementStrat is MovementStrategy.Allow + or MovementStrategy.OnlyScathe) { - if (MP >= 800) - QueueGCD(AID.Scathe, primaryTarget, GCDPriority.Moving1); + if (Unlocked(AID.Scathe) && MP >= 800) + QueueGCD(AID.Scathe, TargetChoice(AOE) ?? primaryTarget ?? BestAOETarget, GCDPriority.Moving1); } } #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 (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) ?? BestAOETarget ?? primaryTarget, - ThunderLeft < 3 ? GCDPriority.NeedDOT : + TargetChoice(thunder), + ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); - if (AOEStrategy is AOEStrategy.ForceST) + if (forceST) QueueGCD(BestThunderST, TargetChoice(thunder) ?? primaryTarget, - ThunderLeft < 3 ? GCDPriority.NeedDOT : + ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); - if (AOEStrategy is AOEStrategy.ForceAOE) + if (forceAOE) QueueGCD(BestThunderAOE, - TargetChoice(thunder) ?? BestAOETarget ?? primaryTarget, - ThunderLeft < 3 ? GCDPriority.NeedDOT : + TargetChoice(thunder) ?? primaryTarget, + ThunderLeft <= 3 ? GCDPriority.NeedDOT : GCDPriority.DOT); } //Polyglots @@ -667,8 +665,8 @@ or PolyglotStrategy.AutoHold2 QueueGCD(BestPolyglot, TargetChoice(polyglot) ?? BestAOETarget ?? 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.XenoSpendAll or PolyglotStrategy.XenoHold1 or PolyglotStrategy.XenoHold2 @@ -676,13 +674,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, + polyglotStrat is PolyglotStrategy.ForceFoul ? GCDPriority.ForcedGCD + : Polyglots == MaxPolyglots && EnochianTimer <= 5000 ? GCDPriority.NeedPolyglot + : GCDPriority.Polyglot); } //LeyLines if (ShouldUseLeyLines(primaryTarget, llStrat)) @@ -725,11 +727,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, @@ -739,6 +743,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 @@ -796,7 +802,10 @@ 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; } - private void BestRotation(Actor? target) //Best rotation based on targets nearby + #endregion + + #region Rotation Helpers + private void BestRotation(Actor? target) { if (ShouldUseAOE) { @@ -807,697 +816,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) + if (Unlocked(AID.Blizzard3) && + MP < 9600 && + Player.InCombat) //if Blizzard III is unlocked 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 (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); //Queue Swiftcast->Blizzard III - else - QueueGCD(AID.Blizzard3, target, GCDPriority.NeedB3); //Queue Blizzard III - } + if (Unlocked(AID.Fire3) && + ((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 (InUmbralIce) //if Umbral Ice is active - { - //Step 1 - max stacks in UI - if (JustUsed(AID.Blizzard3, 5)) //if Blizzard III was just used + if (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 (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 (Player.Level is >= 60 and <= 71) { - 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 (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 (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 (Player.Level is >= 72 and <= 89) { - 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 (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 (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 (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-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) - { - //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 - { - if (NoStance) - { - if (Unlocked(AID.Blizzard2)) + if (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 (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 (InAstralFire) //if Astral Fire is active { - if (ActionReady(AID.Swiftcast)) - QueueGCD(AID.Swiftcast, target, GCDPriority.NeedB3); - else - QueueGCD(AID.HighBlizzard2, target, GCDPriority.NeedB3); + //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 (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 + //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.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)) - { - //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.HighBlizzard2) && - MP < 400) - QueueGCD(AID.HighBlizzard2, target, GCDPriority.Step1); - } } - private void AOELv100(Actor? target) //Level 100 AOE rotation + private void BestAOE(Actor? target) //AOE rotation based on level { - if (NoStance) + if (In25y(target)) { - if (Unlocked(AID.HighBlizzard2)) + if (NoStance) { - if (MP >= 10000) - QueueGCD(AID.HighBlizzard2, target, GCDPriority.NeedB3); - if (MP < 10000 && Player.InCombat) + if (Unlocked(AID.Blizzard2) && !Unlocked(AID.HighBlizzard2)) + { + if (MP >= 10000) + QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); + if (MP < 10000 && Player.InCombat) + QueueGCD(AID.Blizzard2, target, GCDPriority.NeedB3); + } + 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 - Flare - if (Unlocked(AID.Flare)) + if (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); - } - //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); + //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); } if (Player.Level is >= 35 and <= 39) { - AOELv35toLv39(target); + 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.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 (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) { - 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) { - 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) { - 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) { - 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); + } } } } @@ -1623,8 +1415,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, 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..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 @@ -348,7 +349,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 @@ -405,6 +406,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa 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 }; 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/AIBase.cs b/BossMod/Autorotation/xan/AI/AIBase.cs index f06e0c1b8b..d72e038e88 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,25 +8,12 @@ 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; - - internal bool IsCastReactable(Actor act) - { - var castInfo = act.CastInfo; - return !(castInfo == null || castInfo.TotalTime <= 1.5 || castInfo.EventHappened); - } + // 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 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 ce8721d1ca..01eb726d8f 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,23 +49,52 @@ 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); } - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + /// + /// 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, ref Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) { if (Player.MountId > 0) return; 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.Buddy && x.IsAlly), - RaiseTarget.Alliance => World.Party.WithoutSlot(true), - _ => World.Party.WithoutSlot(true, true) + RaiseTarget.Alliance => World.Party.WithoutSlot(true, 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,33 @@ private void AutoAST(StrategyValues strategy) Hints.ActionsToExecute.Push(ActionID.MakeSpell(BossMod.AST.AID.EarthlyStar), Player, ActionQueue.Priority.Medium, targetPos: Player.PosRot.XYZ()); } + private Vector3? ArenaCenter + { + get + { + 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 ??= ArenaCenter ?? 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 +305,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 +330,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 +385,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..13bff210a6 100644 --- a/BossMod/Autorotation/xan/AI/Melee.cs +++ b/BossMod/Autorotation/xan/AI/Melee.cs @@ -15,23 +15,23 @@ 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)) + 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..688d1f039e 100644 --- a/BossMod/Autorotation/xan/AI/Ranged.cs +++ b/BossMod/Autorotation/xan/AI/Ranged.cs @@ -14,18 +14,18 @@ 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) { - 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 257708693d..d83fd4a378 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") }; - public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + private TankActions JobActions => ActionsForJob(Player.Class); + + public override void Execute(StrategyValues strategy, ref 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 b3db3a5cf0..a8980798ad 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 27870f9a73..3e04aa4b18 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) - => Math.Max(cooldown, World.Client.AnimationLock) + actionLock + AnimationLockDelay <= GCD + GCDLength * extraGCDs + extraFixedDelay; + => Math.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) => Math.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 > 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) + public sealed override void Execute(StrategyValues strategy, ref 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,6 @@ 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; + public static float DistanceToHitbox(this Actor actor, Enemy? other) => actor.DistanceToHitbox(other?.Actor); } diff --git a/BossMod/Autorotation/xan/Casters/BLM.cs b/BossMod/Autorotation/xan/Casters/BLM.cs index f83c4dc7de..84890fa176 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 8952205faa..f2b37c7bc9 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 f8db415c80..77f842edd0 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 be3944943b..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).Length) - 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 616f215468..9787501e97 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 = Math.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 d618444631..c9d9b8d060 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 f2195516f6..8743cef44f 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 8e6ee0cdaa..9552466b57 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 50ea395473..d4b6a786d2 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 (Math.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 62ee972186..6c7db43cb4 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 81c1844f17..8298e39355 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..beb720ae0f 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,17 @@ public static RotationModuleDefinition Definition() public float Reign; public float SonicBreak; - public bool 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 +40,6 @@ 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)); NoMercy = StatusLeft(SID.NoMercy); NumAOETargets = NumMeleeAOETargets(strategy); @@ -51,7 +50,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 +126,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 +160,28 @@ 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 Continuation + { + get + { + 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 d99a6c8066..cae9e6f168 100644 --- a/BossMod/BossModule/AIHints.cs +++ b/BossMod/BossModule/AIHints.cs @@ -215,7 +215,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/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/BossModuleRegistry.cs b/BossMod/BossModule/BossModuleRegistry.cs index c5c44d6198..da2f2840f3 100644 --- a/BossMod/BossModule/BossModuleRegistry.cs +++ b/BossMod/BossModule/BossModuleRegistry.cs @@ -176,7 +176,7 @@ static BossModuleRegistry() continue; _modulesByType[t] = info; if (!RegisteredModules.TryAdd(info.PrimaryActorOID, info)) - Service.Log($"Two boss modules have same primary actor OID: {t.Name} and {RegisteredModules[info.PrimaryActorOID].ModuleType.Name}"); + Service.Log($"[ModuleRegistry] Two boss modules have same primary actor OID: {t.FullName} and {RegisteredModules[info.PrimaryActorOID].ModuleType.FullName}"); } } 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/RaidCooldowns.cs b/BossMod/BossModule/RaidCooldowns.cs index fb1c17f3cb..81bad21ac3 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 Math.Min(float.MaxValue, (float)(firstAvailable - _ws.CurrentTime).TotalSeconds); 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/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/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 new file mode 100644 index 0000000000..f38b124a0a --- /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 readonly 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.AsObject(), v == 0); + return newPayload; + }); + res.Converters.Add((j, _, _) => // v3: modified namespaces for old modules + { + 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 + { + 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.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.AsObject()) + 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.AsObject()) + { + 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.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.AsObject().TryRenameNode("BossMod.ActionManagerConfig", "BossMod.ActionTweaksConfig"); + j.AsObject().TryRenameNode("BossMod.AutorotationConfig", "BossMod.Autorotation.AutorotationConfig"); + ConvertV9Plans(j.AsObject(), 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 ccb87636fe..3a7b1f6fd0 100644 --- a/BossMod/Config/ConfigRoot.cs +++ b/BossMod/Config/ConfigRoot.cs @@ -1,16 +1,13 @@ 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(); - + private const int _version = 10; public readonly Dictionary _nodes = []; public List Nodes => [.. _nodes.Values]; @@ -37,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; @@ -189,347 +187,4 @@ public List ConsoleCommand(ReadOnlySpan args, 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/Config/ModuleViewer.cs b/BossMod/Config/ModuleViewer.cs index d69a5ceada..0531c5f506 100644 --- a/BossMod/Config/ModuleViewer.cs +++ b/BossMod/Config/ModuleViewer.cs @@ -50,6 +50,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)); @@ -58,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/ActionID.cs b/BossMod/Data/ActionID.cs index e989bb331f..3c6c48fab7 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/Actor.cs b/BossMod/Data/Actor.cs index a58fd4625f..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; } @@ -157,6 +159,7 @@ public int PendingMPDiffence 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) @@ -186,7 +189,8 @@ public int PendingMPDiffence 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/Data/DeepDungeonState.cs b/BossMod/Data/DeepDungeonState.cs new file mode 100644 index 0000000000..e0979358fc --- /dev/null +++ b/BossMod/Data/DeepDungeonState.cs @@ -0,0 +1,174 @@ +using RoomFlags = FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon.RoomFlags; + +namespace BossMod; + +public sealed class DeepDungeonState +{ + public enum DungeonType : byte + { + None = 0, + POTD = 1, + HOH = 2, + EO = 3 + } + + 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 readonly record struct Chest(byte Type, byte Room); + + 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 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 List CompareToInitial() + { + var ops = new List(6); + if (DungeonId != DungeonType.None) + { + 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(ref 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((byte)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[] Rooms) : WorldState.Operation + { + public readonly RoomFlags[] Rooms = Rooms; + + protected override void Exec(ref WorldState ws) + { + Array.Copy(Rooms, ws.DeepDungeon.Rooms, NumRooms); + ws.DeepDungeon.MapDataChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDMP"u8); + foreach (var r in Rooms) + output.Emit((byte)r, "X2"); + } + } + + public Event PartyStateChanged = new(); + public sealed record class OpPartyStateChange(PartyMember[] Value) : WorldState.Operation + { + public readonly PartyMember[] Value = Value; + + protected override void Exec(ref WorldState ws) + { + Array.Copy(Value, ws.DeepDungeon.Party, NumPartyMembers); + 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 PomandersChanged = new(); + public sealed record class OpPomandersChange(PomanderState[] Value) : WorldState.Operation + { + public readonly PomanderState[] Value = Value; + + protected override void Exec(ref WorldState ws) + { + Array.Copy(Value, ws.DeepDungeon.Pomanders, NumPomanderSlots); + ws.DeepDungeon.PomandersChanged.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(ref WorldState ws) + { + Array.Copy(Value, ws.DeepDungeon.Chests, NumChests); + 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(ref WorldState ws) + { + Array.Copy(Value, ws.DeepDungeon.Magicite, NumMagicites); + 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 9f82612a42..01dc1d1cfa 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; @@ -58,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)); @@ -72,6 +73,7 @@ public List CompareToInitial() ops.AddRange(party); ops.AddRange(client); ops.AddRange(network); + ops.AddRange(deepdungeon); return ops; } // implementation of operations 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" }, diff --git a/BossMod/Framework/IPCProvider.cs b/BossMod/Framework/IPCProvider.cs index d6609cd72d..61ea67f326 100644 --- a/BossMod/Framework/IPCProvider.cs +++ b/BossMod/Framework/IPCProvider.cs @@ -62,7 +62,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)) @@ -76,9 +76,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)); Register("AI.SetPreset", (string name) => ai.SetAIPreset(autorotation.Database.Presets.VisiblePresets.FirstOrDefault(x => x.Name == name))); @@ -115,6 +117,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/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/Utils.cs b/BossMod/Framework/Utils.cs index 80f7e241e2..376ac78d11 100644 --- a/BossMod/Framework/Utils.cs +++ b/BossMod/Framework/Utils.cs @@ -52,6 +52,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 61a0ffec2c..9eea81f29a 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; @@ -142,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 @@ -183,6 +184,7 @@ public unsafe void Update(TimeSpan prevFramePerf) UpdateActors(); UpdateParty(); UpdateClient(); + UpdateDeepDungeon(); } private unsafe void UpdateWaymarks() @@ -200,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) @@ -216,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; } @@ -227,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); } } @@ -236,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; @@ -330,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; @@ -350,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))); } @@ -372,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; } @@ -389,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) @@ -507,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); @@ -564,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; @@ -662,6 +669,60 @@ private unsafe void UpdateClient() _ws.Execute(new ClientState.OpFocusTargetChange(focusTargetId)); } + private unsafe void UpdateDeepDungeon() + { + var dd = EventFramework.Instance()->GetInstanceContentDeepDungeon(); + if (dd != null) + { + var currentId = (DeepDungeonState.DungeonType)dd->DeepDungeonId; + var fullUpdate = currentId != _ws.DeepDungeon.DungeonId; + + 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)); + + if (fullUpdate || !MemoryExtensions.SequenceEqual(_ws.DeepDungeon.Rooms.AsSpan(), dd->MapData)) + _ws.Execute(new DeepDungeonState.OpMapDataChange(dd->MapData.ToArray())); + + 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())); + + 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())); + + 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())); + + if (fullUpdate || !MemoryExtensions.SequenceEqual(_ws.DeepDungeon.Magicite.AsSpan(), dd->Magicite)) + _ws.Execute(new DeepDungeonState.OpMagiciteChange(dd->Magicite.ToArray())); + } + else if (_ws.DeepDungeon.DungeonId != DeepDungeonState.DungeonType.None) + { + // exiting deep dungeon, clean up all state + _ws.Execute(new DeepDungeonState.OpProgressChange(DeepDungeonState.DungeonType.None, default)); + } + // else: we were and still are outside deep dungeon, nothing to do + } + + 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) @@ -669,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); } @@ -681,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)); } @@ -694,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/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/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/Savage/M02SHoneyBLovely/AI/AIExperiment.cs b/BossMod/Modules/Dawntrail/Savage/M02SHoneyBLovely/AI/AIExperiment.cs index 9f587da5fa..0eb3f159b6 100644 --- a/BossMod/Modules/Dawntrail/Savage/M02SHoneyBLovely/AI/AIExperiment.cs +++ b/BossMod/Modules/Dawntrail/Savage/M02SHoneyBLovely/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 M02SHoneyBLovely module) return; diff --git a/BossMod/Modules/Dawntrail/Savage/M04SWickedThunder/AI/AIExperiment.cs b/BossMod/Modules/Dawntrail/Savage/M04SWickedThunder/AI/AIExperiment.cs index 8b7468bdf7..43312f3488 100644 --- a/BossMod/Modules/Dawntrail/Savage/M04SWickedThunder/AI/AIExperiment.cs +++ b/BossMod/Modules/Dawntrail/Savage/M04SWickedThunder/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 M04SWickedThunder module) return; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs index 49a1f12fa7..e6f91651bf 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs @@ -23,7 +23,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/Dawntrail/Ultimate/FRU/P2LightRampant.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs index 754812e168..e9e95e38de 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) { 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/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/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/Job/Dragoon/DragoonsFate.cs b/BossMod/Modules/Heavensward/Quest/Job/Dragoon/DragoonsFate.cs new file mode 100644 index 0000000000..623bbe27e3 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/Job/Dragoon/DragoonsFate.cs @@ -0,0 +1,93 @@ +namespace BossMod.Heavensward.Quest.Job.DragoonsFate; + +public enum OID : uint +{ + Boss = 0x10B9, // R7.0 + Icicle = 0x10BC, // R2.5 + Graoully = 0x10BA, // R7.0 +} + +public enum AID : uint +{ + 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 + ThinIce = 905 // Boss->player/10BB, extra=0x1/0x2/0x3 +} + +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) +{ + 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), Colors.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, arena.Center, arena) +{ + 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/MSQ/ASpectacleForTheAges.cs b/BossMod/Modules/Heavensward/Quest/MSQ/ASpectacleForTheAges.cs new file mode 100644 index 0000000000..1835e06965 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/MSQ/ASpectacleForTheAges.cs @@ -0,0 +1,34 @@ +namespace BossMod.Heavensward.Quest.MSQ.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.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); + +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/MSQ/CloseEncountersOfTheVIthKind.cs b/BossMod/Modules/Heavensward/Quest/MSQ/CloseEncountersOfTheVIthKind.cs new file mode 100644 index 0000000000..000030cef2 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/MSQ/CloseEncountersOfTheVIthKind.cs @@ -0,0 +1,61 @@ +namespace BossMod.Heavensward.Quest.MSQ.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.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, Colors.Danger, true); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + => _active ? Adds.Select(x => new AOEInstance(rect, 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/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/MSQ/Heliodrome.cs b/BossMod/Modules/Heavensward/Quest/MSQ/FlyFreeMyPretty.cs similarity index 66% rename from BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs rename to BossMod/Modules/Heavensward/Quest/MSQ/FlyFreeMyPretty.cs index dfda1784e4..4b7812c68f 100644 --- a/BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs +++ b/BossMod/Modules/Heavensward/Quest/MSQ/FlyFreeMyPretty.cs @@ -1,22 +1,22 @@ -namespace BossMod.Heavensward.Quest.MSQ.Heliodrome; +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 { 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 + 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 @@ -26,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())); @@ -38,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; + } } } @@ -48,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; } } @@ -58,8 +61,11 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { 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()); + 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)); } } } @@ -70,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() @@ -82,19 +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; })); } } -[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 222, NameID = 5576)] +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67894, NameID = 5576)] 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)]); - - protected override bool CheckPull() => Raid.Player()!.InCombat; } diff --git a/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs b/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs index fb1690f1a8..10af1ca419 100644 --- a/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs +++ b/BossMod/Modules/Heavensward/Quest/MSQ/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, // KnightOfDarkness->Boss/_Gen_FirstWard, extra=0x0 + Invincibility = 325 // KnightOfDarkness->Boss/_Gen_FirstWard, extra=0x0 } class Overpower(BossModule module) : Components.SimpleAOEs(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) @@ -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; @@ -71,7 +72,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } else { - e.Priority = -1; + e.Priority = AIHints.Enemy.PriorityUndesirable; } } @@ -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) @@ -119,7 +121,7 @@ public WarriorOfDarknessStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.Contributed, 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, default, arena) { private static readonly ArenaBoundsComplex arena = new([new Polygon(default, 19.5f, 36)]); 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/Extreme/Ex3Titan/Ex3TitanAI.cs b/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs index dcda2b676f..813540546e 100644 --- a/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs +++ b/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs @@ -17,7 +17,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/RealmReborn/Quest/MSQ/OperationArchon.cs b/BossMod/Modules/RealmReborn/Quest/MSQ/OperationArchon.cs new file mode 100644 index 0000000000..aacea470b6 --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/MSQ/OperationArchon.cs @@ -0,0 +1,65 @@ +namespace BossMod.RealmReborn.Quest.MSQ.OperationArchon; + +public enum OID : uint +{ + Boss = 0x38F5, // R1.500, x? + ImperialPilusPrior = 0x38F7, // R1.500, x0 (spawn during fight) + ImperialCenturion = 0x38F6, // R1.500, x0 (spawn during fight) + Helper = 0x233C +} + +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.AddsMulti(module, [(uint)OID.ImperialPilusPrior, (uint)OID.ImperialCenturion]); + +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(ShapeDistance.Cone(Module.PrimaryActor.Position, 100, Module.PrimaryActor.Rotation, a45), WorldState.FutureTime(10)); + } +} +class TartareanTomb(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TartareanTomb), 11); + +class RhitahtynSasArvinaStates : StateMachineBuilder +{ + public RhitahtynSasArvinaStates(BossModule module) : base(module) + { + TrivialPhase() + .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/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/MSQ/TheUltimateWeapon.cs b/BossMod/Modules/RealmReborn/Quest/MSQ/TheUltimateWeapon.cs new file mode 100644 index 0000000000..706436b8be --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/MSQ/TheUltimateWeapon.cs @@ -0,0 +1,125 @@ +namespace BossMod.RealmReborn.Quest.MSQ.TheUltimateWeapon; + +public enum OID : uint +{ + Boss = 0x3933, // R1.75 + SeaOfPitch = 0x1EB738, // R0.5 + Firesphere = 0x3934, // R1.0 +} + +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) + { + // 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; + if (Casters.Count != 0) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center, 5), Module.CastFinishAt(Casters[0].CastInfo)); + } +} + +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) + _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(circle, enemy.Position, default, dt); + } +} + +class Nightburn(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Nightburn)); +class AncientFire(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AncientFireIII)); + +class ArenaChange(BossModule module) : Components.GenericAOEs(module) +{ + private bool completed; + private static readonly AOEShapeDonut donut = new(15, 20); + 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.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) + { + Arena.Bounds = new ArenaBoundsCircle(15); + completed = true; + } + } +} + +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)); + +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() + .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/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/Job/Dancer/GambolingForGil.cs b/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/GambolingForGil.cs new file mode 100644 index 0000000000..424252596e --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/GambolingForGil.cs @@ -0,0 +1,87 @@ +namespace BossMod.Shadowbringers.Quest.Job.Dancer.GambolingForGil; + +public enum OID : uint +{ + Boss = 0x29D2, // R0.5 + Whirlwind = 0x29D5, // R1.0 +} + +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.SimpleAOEs(module, ActionID.MakeSpell(AID.WarDance), 5); +class CharmingChasse(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.CharmingChasse)); +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 +{ + public RanaaMihgoStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[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 ArenaBoundsComplex arena = new([new Ellipse(new(520.47f, 124.99f), 17.5f, 16, 50)]); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/SaveTheLastDanceForMe.cs b/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/SaveTheLastDanceForMe.cs new file mode 100644 index 0000000000..7f81b119ae --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Job/Dancer/SaveTheLastDanceForMe.cs @@ -0,0 +1,72 @@ +namespace BossMod.Shadowbringers.Quest.Job.Dancer.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.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() { 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(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.PersistentVoidzone(module, 8, m => m.Enemies(OID.ForebodingAura).Where(e => !e.IsDead)); + +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(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/MSQ/AFeastOfLies.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/AFeastOfLies.cs new file mode 100644 index 0000000000..1ad83452db --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/AFeastOfLies.cs @@ -0,0 +1,101 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.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 + + 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 + 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.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.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)); + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + 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/MSQ/ASleepDisturbed.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/ASleepDisturbed.cs index 73af4218ae..4e9b088c4b 100644 --- a/BossMod/Modules/Shadowbringers/Quest/MSQ/ASleepDisturbed.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/ASleepDisturbed.cs @@ -66,4 +66,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(19.5f)); +public class ASleepDisturbed(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsSquare(19.5f)) +{ + 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/MSQ/ComingClean.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/ComingClean.cs new file mode 100644 index 0000000000..4c5fcd0d3c --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/ComingClean.cs @@ -0,0 +1,135 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.MSQ.ComingClean; // quest name: Full Steam Ahead + +public enum OID : uint +{ + Boss = 0x295C, // R1.0 + Ranjit = 0x295D, // R1.0 + SerpentHead = 0x295F, // R1.0 + LightningVoidzone = 0x1E9685, // R0.5 + Helper = 0x233C, // R0.5 +} + +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 + 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, ??? + 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, ??? + 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 + 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, + DownForTheCount = 783 // 295E->player, extra=0xEC7 +} + +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 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); + +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() + .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, 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(OID.Ranjit)); + Arena.Actor(PrimaryActor); + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P1TelotekGamma.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P1TelotekGamma.cs new file mode 100644 index 0000000000..8f560c56ef --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P1TelotekGamma.cs @@ -0,0 +1,35 @@ +using BossMod.QuestBattle.Shadowbringers.MSQ; + +namespace BossMod.Shadowbringers.Quest.MSQ.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) : QuestBattle.RotationModule(module); +class AntiPersonnelMissile(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.AntiPersonnelMissile), 6); +class MRVMissile(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MRVMissile), 12, 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)); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P2LunarOdin.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P2LunarOdin.cs new file mode 100644 index 0000000000..223d7290c4 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P2LunarOdin.cs @@ -0,0 +1,97 @@ +using BossMod.QuestBattle; +using RID = BossMod.Roleplay.AID; + +namespace BossMod.Shadowbringers.Quest.MSQ.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(false, false).Select(p => p.Position).ToList(); + + Hints.GoalZones.Add(pos => partyPositions.Count(p => p.InCircle(pos, 16))); + + 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(false, false).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 AutoUrianger(BossModule module) : RotationModule(module); +class Fetters(BossModule module) : Components.Adds(module, (uint)OID.Fetters); + +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 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.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); + +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(); + } +} + +[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/MSQ/DeathUntoDawn/P3LunarRavana.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P3LunarRavana.cs new file mode 100644 index 0000000000..eb22c22965 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P3LunarRavana.cs @@ -0,0 +1,108 @@ +using BossMod.QuestBattle; +using RID = BossMod.Roleplay.AID; + +namespace BossMod.Shadowbringers.Quest.MSQ.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) : RotationModule(module); +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(ShapeDistance.Cone(Module.PrimaryActor.Position, 100, Module.PrimaryActor.Rotation, a45), WorldState.FutureTime(10)); + } +} +class Explosion(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), 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)); + + 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/MSQ/DeathUntoDawn/P4LunarIfrit.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P4LunarIfrit.cs new file mode 100644 index 0000000000..ae2405f1bb --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/DeathUntoDawn/P4LunarIfrit.cs @@ -0,0 +1,44 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.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 + 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 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.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.SimpleAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), 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/MSQ/FadedMemories/Ardbert.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Ardbert.cs new file mode 100644 index 0000000000..c32559efa3 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Ardbert.cs @@ -0,0 +1,108 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.FadedMemories; + +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) + { + 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(rect, spell.LocXZ, 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 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) + { + base.OnCastFinished(caster, spell); + if (NumCasts > 0) + MaxCasts = 2; + } +} +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.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 ((AID)spell.Action.ID == AID.GroundbreakerExaFirst) + { + 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 ((AID)spell.Action.ID is AID.GroundbreakerExaFirst or AID.GroundbreakerExaRest) + { + 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.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 +{ + 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/MSQ/FadedMemories/FadedMemories.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FadedMemories.cs new file mode 100644 index 0000000000..8944c958e7 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FadedMemories.cs @@ -0,0 +1,53 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.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/MSQ/FadedMemories/FlameGeneralAldynn.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FlameGeneralAldynn.cs new file mode 100644 index 0000000000..16c9ff7bfe --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/FlameGeneralAldynn.cs @@ -0,0 +1,19 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.FadedMemories; + +class FlamingTizona(BossModule module) : Components.SimpleAOEs(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)); +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/KingThordan.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/KingThordan.cs new file mode 100644 index 0000000000..45b8e8a317 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/KingThordan.cs @@ -0,0 +1,27 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.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)); + + 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/MSQ/FadedMemories/Nidhogg.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Nidhogg.cs new file mode 100644 index 0000000000..9a40b095ad --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Nidhogg.cs @@ -0,0 +1,17 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.FadedMemories; + +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(); + } +} + +[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/MSQ/FadedMemories/Zenos.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Zenos.cs new file mode 100644 index 0000000000..1d8f589817 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/FadedMemories/Zenos.cs @@ -0,0 +1,20 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.FadedMemories; + +class Swords(BossModule module) : Components.AddsMulti(module, [0x2F2A, 0x2F2B, 0x2F2C]); + +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 +{ + 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/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/MSQ/TheOracleOfLight.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/TheOracleOfLight.cs new file mode 100644 index 0000000000..9dd39210c6 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/TheOracleOfLight.cs @@ -0,0 +1,42 @@ +namespace BossMod.Shadowbringers.Quest.MSQ.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.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 +{ + 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/MSQ/VowsOfVitrueDeedsOfCruelty.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs index 7212b5d791..e991d9b645 100644 --- a/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs @@ -1,13 +1,15 @@ -namespace BossMod.Shadowbringers.Quest.MSQ.VowsOfVitrueDeedsOfCruelty; +using BossMod.QuestBattle; + +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 @@ -20,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 } @@ -73,9 +75,37 @@ class GarleanFire(BossModule module) : Components.SimpleAOEs(module, ActionID.Ma 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 VowsOfVirtueDeedsOfCrueltyStates : StateMachineBuilder + +class EstinienAI(WorldState ws) : UnmanagedRotation(ws, 3) { - public VowsOfVirtueDeedsOfCrueltyStates(BossModule module) : base(module) + 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() @@ -88,9 +118,27 @@ public VowsOfVirtueDeedsOfCrueltyStates(BossModule module) : base(module) .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(19.5f)); +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) + { + 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)); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/Role/ATearfulReunion.cs b/BossMod/Modules/Shadowbringers/Quest/Role/ATearfulReunion.cs new file mode 100644 index 0000000000..b6a5ac7e29 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/ATearfulReunion.cs @@ -0,0 +1,100 @@ +namespace BossMod.Shadowbringers.Quest.Role.ATearfulReunion; + +public enum OID : uint +{ + Boss = 0x29C5, + Hollow = 0x29C6, // R0.750-2.250, 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.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); +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.Count != 0 && WorldState.Actors.First(x => x.OID == 0x29C3) is Actor cerigg) + { + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(cerigg.Position, 6), 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.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, Colors.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); + Safezones.Clear(); + AddSafezone(NextExplosion, default); + } + + 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/Role/CourageBornOfFear.cs b/BossMod/Modules/Shadowbringers/Quest/Role/CourageBornOfFear.cs new file mode 100644 index 0000000000..83421c72f8 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/CourageBornOfFear.cs @@ -0,0 +1,105 @@ +namespace BossMod.Shadowbringers.Quest.Role.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 + BodkinVolley = 17189, // Andreia->29DF, 6.0s cast, range 5 circle +} + +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.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.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) +{ + 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)); + + 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.TargetID == actor.InstanceID ? 1 : 0; + } + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/Role/NyelbertsLament.cs b/BossMod/Modules/Shadowbringers/Quest/Role/NyelbertsLament.cs new file mode 100644 index 0000000000..0e85afc0d8 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/NyelbertsLament.cs @@ -0,0 +1,119 @@ +using BossMod.QuestBattle.Shadowbringers.RoleQuests; + +namespace BossMod.Shadowbringers.Quest.Role.NyelbertsLament; + +public enum OID : uint +{ + Boss = 0x2977, + + BovianBull = 0x2976, + LooseBoulder = 0x2978, // R2.4 + Helper = 0x233C +} + +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.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.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 (ActiveBaits.Any()) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center, 3), ActiveBaits.FirstOrDefault().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(), Colors.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) : QuestBattle.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)); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/Role/TheHardenedHeart.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheHardenedHeart.cs new file mode 100644 index 0000000000..586a95e049 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheHardenedHeart.cs @@ -0,0 +1,143 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.Role.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.SimpleAOEs(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; + + 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, Colors.Danger); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.DeadlyBite) + DwarfTether = null; + } +} + +class BrandenAI(BossModule module) : RotationModule(module); + +class RustingClaw(BossModule module) : Components.SimpleAOEs(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)); + + 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(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/Role/TheHuntersLegacy.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheHuntersLegacy.cs new file mode 100644 index 0000000000..b4d97cf589 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheHuntersLegacy.cs @@ -0,0 +1,95 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.Role.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); + +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) +{ + 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) : 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, Colors.SafeFromAOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (AuraCenter is Actor a) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(a.Position, 10), 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/Role/TheLostAndTheFound/Sophrosyne.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Sophrosyne.cs new file mode 100644 index 0000000000..4e3664b59b --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Sophrosyne.cs @@ -0,0 +1,29 @@ +namespace BossMod.Shadowbringers.Quest.Role.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)); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Yxtlilton.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Yxtlilton.cs new file mode 100644 index 0000000000..a43e1f15b9 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheLostAndTheFound/Yxtlilton.cs @@ -0,0 +1,97 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.Role.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(ShapeDistance.InvertedCircle(Arena.Center, 1.5f), 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(false, false); + + 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); + 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) : 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)); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/Role/TheSoulOfTemperance.cs b/BossMod/Modules/Shadowbringers/Quest/Role/TheSoulOfTemperance.cs new file mode 100644 index 0000000000..cf67e50ab8 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/TheSoulOfTemperance.cs @@ -0,0 +1,76 @@ +namespace BossMod.Shadowbringers.Quest.Role.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.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.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.SimpleAOEs(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)); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/Role/ToHaveLovedAndLost.cs b/BossMod/Modules/Shadowbringers/Quest/Role/ToHaveLovedAndLost.cs new file mode 100644 index 0000000000..1c47c0cd78 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/Role/ToHaveLovedAndLost.cs @@ -0,0 +1,85 @@ +namespace BossMod.Shadowbringers.Quest.Role.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.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 HashSet casts = [AID.Fracture, AID.Fracture1, AID.Fracture2, AID.Fracture3, AID.Fracture4, AID.Fracture5]; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + 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 (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.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.SimpleAOEs(module, ActionID.MakeSpell(AID.SanctifiedHolyII), 5); +class SanctifiedHolyIII(BossModule module) : Components.SimpleAOEs(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/TheSorrowOfWerlyt/SleepNowInSapphire/P1GuidanceSystem.cs b/BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P1GuidanceSystem.cs new file mode 100644 index 0000000000..abec4011ef --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheSorrowOfWerlyt/SleepNowInSapphire/P1GuidanceSystem.cs @@ -0,0 +1,38 @@ +using BossMod.QuestBattle.Shadowbringers.SideQuests; + +namespace BossMod.Shadowbringers.Quest.SorrowOfWerlyt.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.SimpleAOEs(module, ActionID.MakeSpell(AID.AerialBombardment), 12); + +class GWarrior(BossModule module) : QuestBattle.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)) +{ + 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/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/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/Job/DarkKnight/TheOrphansAndTheBrokenBlade.cs b/BossMod/Modules/Stormblood/Quest/Job/DarkKnight/TheOrphansAndTheBrokenBlade.cs new file mode 100644 index 0000000000..96a5b4ec54 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/DarkKnight/TheOrphansAndTheBrokenBlade.cs @@ -0,0 +1,57 @@ +namespace BossMod.Stormblood.Quest.Job.DarkKnight.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.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) + { + 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.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) + { + 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)); +} diff --git a/BossMod/Modules/Stormblood/Quest/Job/Dragoon/DragonSound.cs b/BossMod/Modules/Stormblood/Quest/Job/Dragoon/DragonSound.cs new file mode 100644 index 0000000000..e26f12d59c --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/Dragoon/DragonSound.cs @@ -0,0 +1,72 @@ +namespace BossMod.Stormblood.Quest.Job.Dragoon.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.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 +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/Job/Monk/ThePowerToProtect.cs b/BossMod/Modules/Stormblood/Quest/Job/Monk/ThePowerToProtect.cs new file mode 100644 index 0000000000..4045d102e9 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/Monk/ThePowerToProtect.cs @@ -0,0 +1,77 @@ +namespace BossMod.Stormblood.Quest.Job.Monk.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.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.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 +{ + 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)); +} + diff --git a/BossMod/Modules/Stormblood/Quest/Job/Paladin/RaisingTheSword.cs b/BossMod/Modules/Stormblood/Quest/Job/Paladin/RaisingTheSword.cs new file mode 100644 index 0000000000..74ff8dcf10 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/Paladin/RaisingTheSword.cs @@ -0,0 +1,75 @@ +namespace BossMod.Stormblood.Quest.Job.Paladin.RaisingTheSword; + +public enum OID : uint +{ + Boss = 0x1B51, + AldisSwordOfNald = 0x18D6, // R0.5 + TaintedWindSprite = 0x1B52, // R1.0 + Helper = 0x233C +} + +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.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; + 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.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 +{ + 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/Job/Samurai/BloodOnTheDeck.cs b/BossMod/Modules/Stormblood/Quest/Job/Samurai/BloodOnTheDeck.cs new file mode 100644 index 0000000000..2be0e90c0b --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/Samurai/BloodOnTheDeck.cs @@ -0,0 +1,43 @@ +namespace BossMod.Stormblood.Quest.Job.BloodOnTheDeck; + +public enum OID : uint +{ + Boss = 0x1BED, + ShamShinobi = 0x1BE8, // R0.5 + AdjunctOstyrgreinHelper = 0x1BEB, // R0.5 + AdjunctOstyrgrein = 0x1BEA, // R0.5 + Vanara = 0x1BE9, // R3.0 + Helper = 0x233C +} + +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.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 +{ + 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)); +} + diff --git a/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheBattleOnBekko.cs b/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheBattleOnBekko.cs new file mode 100644 index 0000000000..d202409d0d --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheBattleOnBekko.cs @@ -0,0 +1,79 @@ +namespace BossMod.Stormblood.Quest.Job.Samurai.TheBattleOnBekko; + +public enum OID : uint +{ + Boss = 0x1BF8, + UgetsuSlayerOfAThousandSouls = 0x1BF9, // R0.5 + Voidzone = 0x1E8EA9, // R1.0 + Helper = 0x233C +} + +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 + 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 +} + +class KuruiGekko(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.KuruiGekko1)); +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.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 ((AID)spell.Action.ID == AID.Ugetsuzan1) + AddSequence(spell.LocXZ, Module.CastFinishAt(spell), spell.Rotation); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + var order = (AID)spell.Action.ID switch + { + AID.Ugetsuzan1 => 0, + AID.Ugetsuzan2 => 1, + AID.Ugetsuzan3 => 2, + AID.Ugetsuzan4 => 3, + _ => -1 + }; + AdvanceSequence(order, spell.LocXZ, WorldState.FutureTime(2.5f), spell.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/Job/Samurai/TheFaceOfTrueEvil.cs b/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheFaceOfTrueEvil.cs new file mode 100644 index 0000000000..13abdf0986 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/Samurai/TheFaceOfTrueEvil.cs @@ -0,0 +1,76 @@ +namespace BossMod.Stormblood.Quest.Job.Samurai.TheFaceOfTrueEvil; + +public enum OID : uint +{ + Boss = 0x1BEE, + Musosai = 0x1BF0, // R1.0 + ViolentWind = 0x1BF1, // R1.0 + Helper2 = 0x1BEF, + Helper = 0x233C +} + +public enum AID : uint +{ + HissatsuTo = 8415, // 1BEF->self, 3.0s cast, range 44+R width 4 rect + HissatsuKyuten = 8412, // Boss->self, 3.0s cast, range 5+R 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 + 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)); + +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; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Activation == null) + yield break; + + 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) + { + 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 ((AID)spell.Action.ID == AID.Arashi) + 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/Job/Scholar/OurUnsungHeroes.cs b/BossMod/Modules/Stormblood/Quest/Job/Scholar/OurUnsungHeroes.cs new file mode 100644 index 0000000000..044d7750a3 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/Scholar/OurUnsungHeroes.cs @@ -0,0 +1,60 @@ +namespace BossMod.Stormblood.Quest.Jobs.Scholar.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.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.SimpleAOEs(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) + { + 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/Job/Summoner/AnArtForTheLiving.cs b/BossMod/Modules/Stormblood/Quest/Job/Summoner/AnArtForTheLiving.cs new file mode 100644 index 0000000000..4a8821290f --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Job/Summoner/AnArtForTheLiving.cs @@ -0,0 +1,118 @@ +namespace BossMod.Stormblood.Quest.Job.Summoner.AnArtForTheLiving; + +public enum OID : uint +{ + Boss = 0x1CBA, + ExplosiveIndicator = 0x1CD7, // R0.5 + AetherochemicalExplosive = 0x1CD5, // R1.0 + Helper = 0x233C +} + +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 120-degree cone +} + +public enum SID : uint +{ + Invincibility = 325 +} + +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.SimpleAOEs(module, ActionID.MakeSpell(AID.NerveGas), new AOEShapeCone(35, 60.Degrees())); + +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) +{ + 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) + { + 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; + } + } +} + +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/MSQ/ARequiemForHeroes/Enums.cs b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/Enums.cs new file mode 100644 index 0000000000..234c77dc13 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/Enums.cs @@ -0,0 +1,30 @@ +namespace BossMod.Stormblood.Quest.MSQ.ARequiemForHeroes; + +public enum OID : uint +{ + BossP1 = 0x268A, + BossP2 = 0x268C, + + AmeNoHabakiri = 0x2692, // R3.0 + TheStorm = 0x2760, // R3.0 + TheSwell = 0x275F, // R3.0 + DarkAether = 0x2694, // R1.2 + Helper = 0x233C +} + +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/MSQ/ARequiemForHeroes/P1.cs b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P1.cs new file mode 100644 index 0000000000..49d6ec1de6 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P1.cs @@ -0,0 +1,51 @@ +using BossMod.QuestBattle; + +namespace BossMod.Stormblood.Quest.MSQ.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) : 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/MSQ/ARequiemForHeroes/P2.cs b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P2.cs new file mode 100644 index 0000000000..1637b906d4 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/ARequiemForHeroes/P2.cs @@ -0,0 +1,80 @@ +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 = 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) + { + 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); + } + } +} + +class LightlessSpark2(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.LightlessSparkAdds), new AOEShapeCone(40, 45.Degrees())); + +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.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) + { + if (Casters.Count > 0) + hints.AddForbiddenZone(new AOEShapeDonut(8, 50), Arena.Center); + } +} + +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) +{ + 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/MSQ/BestServedWithColdSteel.cs b/BossMod/Modules/Stormblood/Quest/MSQ/BestServedWithColdSteel.cs new file mode 100644 index 0000000000..38cceb0b45 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/BestServedWithColdSteel.cs @@ -0,0 +1,157 @@ +namespace BossMod.Stormblood.Quest.MSQ.BestServedWithColdSteel; + +public enum OID : uint +{ + Boss = 0x1A52, // R2.1 + 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.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.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)) +{ + 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(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, Colors.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)); + } + + 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]; + 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/MSQ/EmissaryOfTheDawn.cs b/BossMod/Modules/Stormblood/Quest/MSQ/EmissaryOfTheDawn.cs new file mode 100644 index 0000000000..eb328c91cb --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/EmissaryOfTheDawn.cs @@ -0,0 +1,38 @@ +using BossMod.QuestBattle.Stormblood.MSQ; + +namespace BossMod.Stormblood.Quest.MSQ.EmissaryOfTheDawn; + +public enum OID : uint +{ + Boss = 0x234B, + Helper = 0x233C +} + +class AlphinaudAI(BossModule module) : QuestBattle.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)); +} + diff --git a/BossMod/Modules/Stormblood/Quest/MSQ/HisForgottenHome.cs b/BossMod/Modules/Stormblood/Quest/MSQ/HisForgottenHome.cs new file mode 100644 index 0000000000..662a89a309 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/HisForgottenHome.cs @@ -0,0 +1,74 @@ +namespace BossMod.Stormblood.Quest.MSQ.HisForgottenHome; + +public enum OID : uint +{ + Boss = 0x213A, + 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 = 1087, // Boss->location, 3.0s cast, range 5 circle +} + +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) +{ + 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)); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // attack anyone targeting isse + 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/MSQ/HopeOnTheWaves.cs b/BossMod/Modules/Stormblood/Quest/MSQ/HopeOnTheWaves.cs new file mode 100644 index 0000000000..abc1ced2b3 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/HopeOnTheWaves.cs @@ -0,0 +1,81 @@ +namespace BossMod.Stormblood.Quest.MSQ.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.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.SimpleAOEs(module, ActionID.MakeSpell(AID.SelfDetonate1), 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; + + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + 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, arena.Center, arena) +{ + 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)); + } +} diff --git a/BossMod/Modules/Stormblood/Quest/MSQ/Naadam.cs b/BossMod/Modules/Stormblood/Quest/MSQ/Naadam.cs new file mode 100644 index 0000000000..d1343640a8 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/Naadam.cs @@ -0,0 +1,144 @@ +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, +} + +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.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.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 +{ + 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, Colors.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) + { + 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) + 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(); + + 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) + 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)); +} + diff --git a/BossMod/Modules/Stormblood/Quest/MSQ/ReturnOfTheBull.cs b/BossMod/Modules/Stormblood/Quest/MSQ/ReturnOfTheBull.cs new file mode 100644 index 0000000000..942b10b5c4 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/ReturnOfTheBull.cs @@ -0,0 +1,106 @@ +namespace BossMod.Stormblood.Quest.MSQ.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.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) +{ + 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, Colors.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, Colors.Other9); + } + + 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) + { + 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/MSQ/RhalgrsBeacon.cs b/BossMod/Modules/Stormblood/Quest/MSQ/RhalgrsBeacon.cs new file mode 100644 index 0000000000..76d933b75f --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/RhalgrsBeacon.cs @@ -0,0 +1,123 @@ +namespace BossMod.Stormblood.Quest.MSQ.RhalgrsBeacon; + +public enum OID : uint +{ + Boss = 0x1A88, + 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 +{ + 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.SimpleAOEs(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), Colors.Object, true); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var t in Termini) + yield return new(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/MSQ/TheMeasureOfHisReach.cs b/BossMod/Modules/Stormblood/Quest/MSQ/TheMeasureOfHisReach.cs new file mode 100644 index 0000000000..03feeb2ea9 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/TheMeasureOfHisReach.cs @@ -0,0 +1,45 @@ +namespace BossMod.Stormblood.Quest.MSQ.TheMeasureOfHisReach; + +public enum OID : uint +{ + Boss = 0x1C48, + Whitefang = 0x1C5A, + Helper = 0x233C +} + +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.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(false, false).Mask(), c.Activation)); + } +} +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 +{ + 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/MSQ/TheResonant.cs b/BossMod/Modules/Stormblood/Quest/MSQ/TheResonant.cs new file mode 100644 index 0000000000..70bcec2396 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/TheResonant.cs @@ -0,0 +1,87 @@ +namespace BossMod.Stormblood.Quest.MSQ.TheResonant; + +public enum OID : uint +{ + Boss = 0x1B7D, + 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 +{ + 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.SimpleAOEs(module, ActionID.MakeSpell(AID.Skullbreaker1), 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.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) + { + 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; + 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/MSQ/TheTimeBetweenTheSeconds.cs b/BossMod/Modules/Stormblood/Quest/MSQ/TheTimeBetweenTheSeconds.cs new file mode 100644 index 0000000000..b20e437f2c --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/TheTimeBetweenTheSeconds.cs @@ -0,0 +1,90 @@ +namespace BossMod.Stormblood.Quest.MSQ.TheTimeBetweenTheSeconds; + +public enum OID : uint +{ + Boss = 0x1A36, + 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 +{ + 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.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)) +{ + 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.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)); + Arena.Actors(Module.Enemies(OID.DarkReflection)); + } +} + +class ZenosYaeGalvusStates : StateMachineBuilder +{ + public ZenosYaeGalvusStates(BossModule module) : base(module) + { + SimplePhase(0, id => BuildState(id, ""), "P1") + .Raw.Update = () => !Module.PrimaryActor.IsTargetable; + 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) + { + 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/MSQ/TheWillOfTheMoon.cs b/BossMod/Modules/Stormblood/Quest/MSQ/TheWillOfTheMoon.cs new file mode 100644 index 0000000000..03135e86f7 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/MSQ/TheWillOfTheMoon.cs @@ -0,0 +1,171 @@ +using BossMod.QuestBattle; +using RPID = BossMod.Roleplay.AID; + +namespace BossMod.Stormblood.Quest.MSQ.TheWillOfTheMoon; + +public enum OID : uint +{ + Boss = 0x24A0, + Magnai = 0x24A1, + KhunShavar = 0x252F, // R1.82 + Hien = 0x24A3, + Daidukul = 0x24A2, // R0.5 + TheScaleOfTheFather = 0x2532, // R1.0 + Helper = 0x233C +} + +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.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.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 = Module.Enemies(OID.TheScaleOfTheFather).MinBy(actor.DistanceToHitbox); + else + base.AddAIHints(slot, actor, assignment, hints); + } +} + +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) + base.AddAIHints(slot, actor, assignment, hints); + } +} + +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); + +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) : RotationModule(module); + +class P1Hints(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + 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; + + // 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) + { + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + 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).Count != 0; + 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)); + } +} diff --git a/BossMod/Modules/Stormblood/Quest/TheFourLords/TortoiseInTime.cs b/BossMod/Modules/Stormblood/Quest/TheFourLords/TortoiseInTime.cs new file mode 100644 index 0000000000..9c39d123bf --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheFourLords/TortoiseInTime.cs @@ -0,0 +1,119 @@ +namespace BossMod.Stormblood.Quest.FourLords.TortoiseInTime; + +public enum OID : uint +{ + Boss = 0x2339, + Soroban = 0x2351, // R0.5 + MonkeyMagick = 0x23C2, // R1.0 + Font = 0x233B, // R4.0 + Helper = 0x233C +} + +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.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) +{ + private readonly List Balls = []; + private Actor? Tataru; + + 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, Colors.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.SimpleAOEs(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, Colors.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)); + diff --git a/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs b/BossMod/Modules/Stormblood/TreasureHunt/TheHiddenCanalsOfUznair/Airavata.cs index 29863cc883..d4d5adcded 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 @@ -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/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) { diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index 047084961f..4cb13c21c9 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -417,6 +417,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)] = ParseDeepDungeonPomanders, + [new("DDCT"u8)] = ParseDeepDungeonChests, + [new("DDMG"u8)] = ParseDeepDungeonMagicite, [new("IPCI"u8)] = ParseNetworkIDScramble, [new("IPCS"u8)] = ParseNetworkServerIPC, }; @@ -796,6 +802,45 @@ 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((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]; + 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() + { + var pt = new DeepDungeonState.PartyMember[DeepDungeonState.NumPartyMembers]; + for (var i = 0; i < pt.Length; i++) + pt[i] = new(_input.ReadActorID(), _input.ReadByte(false)); + return new(pt); + } + private DeepDungeonState.OpPomandersChange ParseDeepDungeonPomanders() + { + var it = new DeepDungeonState.PomanderState[DeepDungeonState.NumPomanderSlots]; + for (var i = 0; i < it.Length; i++) + it[i] = new(_input.ReadByte(false), _input.ReadByte(true)); + return new(it); + } + private DeepDungeonState.OpChestsChange ParseDeepDungeonChests() + { + var ct = new DeepDungeonState.Chest[DeepDungeonState.NumChests]; + for (var i = 0; i < ct.Length; i++) + 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)); 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())); diff --git a/BossMod/Replay/ReplayRecorder.cs b/BossMod/Replay/ReplayRecorder.cs index bca45e6240..e4ae313e70 100644 --- a/BossMod/Replay/ReplayRecorder.cs +++ b/BossMod/Replay/ReplayRecorder.cs @@ -267,7 +267,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) { diff --git a/BossMod/Replay/Visualization/EventList.cs b/BossMod/Replay/Visualization/EventList.cs index 7d8ca3d7e5..9556404d23 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}"))) { 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/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..e00145cec3 --- /dev/null +++ b/BossMod/Util/VersionedJSONSchema.cs @@ -0,0 +1,65 @@ +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 JsonNode ConvertDelegate(JsonNode 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 + 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); + + // 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