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..729e120b02 --- /dev/null +++ b/BossMod/Autorotation/PlanPresetConverter.cs @@ -0,0 +1,102 @@ +using System.Text.Json.Nodes; + +namespace BossMod.Autorotation; + +// TODO: waiting for .net9 to complete implementation, since it adds proper API for renaming keys, it's a pita to maintain order otherwise +public static class PlanPresetConverter +{ + // note: we always apply renames _after_ changes - this allows converting old modules to new ones without affecting plans/presets that already use new ones + private record class TrackChanges(Dictionary OptionRenames); + private record class ModuleChanges(Dictionary TrackChanges, Dictionary TrackRenames); + private record class ModuleConverter(Dictionary ModuleChanges, Dictionary ModuleRenames); + + public static VersionedJSONSchema PlanSchema = BuildSchema(true); + public static VersionedJSONSchema PresetSchema = BuildSchema(false); + + private static VersionedJSONSchema BuildSchema(bool plan) + { + var res = new VersionedJSONSchema(); + //AddModuleConverter(res, plan, BuildModuleConverterV1()); // v1: StandardWAR -> VeynVAR rename + return res; + } + + //private static void AddModuleConverter(VersionedJSONSchema schema, bool plan, ModuleConverter cvt) + //{ + // schema.Converters.Add((j, _, _) => + // { + // if (plan) + // { + // var modules = j!["Modules"]!.AsObject(); + // foreach (var (moduleName, moduleData) in modules) + // { + // if (cvt.ModuleChanges.TryGetValue(moduleName, out var moduleChanges)) + // { + // var tracks = moduleData!.AsObject(); + // foreach (var (trackName, trackData) in tracks) + // { + // if (moduleChanges.TrackChanges.TryGetValue(trackName, out var trackChanges)) + // { + // foreach (var entry in trackData!.AsArray()) + // { + // var optionName = entry!["Option"]!.GetValue(); + // if (trackChanges.OptionRenames.TryGetValue(optionName, out var optionNewName)) + // entry["Option"] = optionNewName; + // } + // } + // } + // ApplyRenames(tracks, moduleChanges.TrackRenames); + // } + // } + // ApplyRenames(modules, cvt.ModuleRenames); + // } + // else + // { + // foreach (var preset in j.AsArray()) + // { + // var modules = preset!["Modules"]!.AsObject(); + // foreach (var (moduleName, moduleData) in modules) + // { + // if (cvt.ModuleChanges.TryGetValue(moduleName, out var moduleChanges)) + // { + // foreach (var entry in moduleData!.AsArray()) + // { + // var trackName = entry!["Track"]!.GetValue(); + // if (moduleChanges.TrackChanges.TryGetValue(trackName, out var trackChanges)) + // { + // var optionName = entry!["Option"]!.GetValue(); + // if (trackChanges.OptionRenames.TryGetValue(optionName, out var optionNewName)) + // entry["Option"] = optionNewName; + // } + + // if (moduleChanges.TrackRenames.TryGetValue(trackName, out var trackNewName)) + // entry["Track"] = trackNewName; + // } + // } + // } + // ApplyRenames(modules, cvt.ModuleRenames); + // } + // } + // return j; + // }); + //} + + //private static void ApplyRenames(JsonObject j, Dictionary renames) + //{ + // if (renames.Count == 0) + // return; + + // for (int i = 0; i < j.Count; ++i) + // { + // // TODO: implement... + // } + //} + + //private static ModuleConverter BuildModuleConverterV1() + //{ + // Dictionary moduleRenames = new() + // { + // ["BossMod.Autorotation.StandardWAR"] = "BossMod.Autorotation.VeynWAR", + // }; + // return new([], moduleRenames); + //} +} diff --git a/BossMod/Autorotation/PresetDatabase.cs b/BossMod/Autorotation/PresetDatabase.cs index 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..190ec3867e 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/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/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/ZoneModuleRegistry.cs b/BossMod/BossModule/ZoneModuleRegistry.cs index 6e0cbee503..021185cd98 100644 --- a/BossMod/BossModule/ZoneModuleRegistry.cs +++ b/BossMod/BossModule/ZoneModuleRegistry.cs @@ -24,12 +24,12 @@ static ZoneModuleRegistry() var attr = t.GetCustomAttribute(); if (attr == null) { - Service.Log($"Zone module {t} has no ZoneModuleInfo attribute, skipping"); + Service.Log($"[ZoneModuleRegistry] Zone module {t} has no ZoneModuleInfo attribute, skipping"); continue; } if (_modulesByCFC.TryGetValue(attr.CFCID, out var existingModule)) { - Service.Log($"Two zone modules have same CFCID: {t.Name} and {existingModule.ModuleType.Name}"); + Service.Log($"[ZoneModuleRegistry] Two zone modules have same CFCID: {t.FullName} and {existingModule.ModuleType.FullName}"); continue; } _modulesByCFC[attr.CFCID] = new Info(t, attr, New.ConstructorDerived(t)); diff --git a/BossMod/Config/ConfigConverter.cs b/BossMod/Config/ConfigConverter.cs new file mode 100644 index 0000000000..9503ea4516 --- /dev/null +++ b/BossMod/Config/ConfigConverter.cs @@ -0,0 +1,302 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace BossMod; + +public static class ConfigConverter +{ + public static VersionedJSONSchema Schema = BuildSchema(); + + private static VersionedJSONSchema BuildSchema() + { + var res = new VersionedJSONSchema(); + res.Converters.Add((j, _, _) => j); // v1: moved BossModuleConfig children to special encounter config node; use type names as keys - do nothing, next converter takes care of it + res.Converters.Add((j, v, _) => // v2: flat structure (config root contains all nodes) + { + JsonObject newPayload = []; + ConvertV1GatherChildren(newPayload, j.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..cd3f25b989 100644 --- a/BossMod/Config/ConfigRoot.cs +++ b/BossMod/Config/ConfigRoot.cs @@ -1,14 +1,10 @@ using System.IO; using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; namespace BossMod; public class ConfigRoot { - private const int _version = 10; - public Event Modified = new(); public readonly Dictionary _nodes = []; @@ -37,9 +33,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 +186,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..a92791f2b4 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)); 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..12ec801f4d 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -157,6 +157,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 +187,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..622c3ee9f2 --- /dev/null +++ b/BossMod/Data/DeepDungeonState.cs @@ -0,0 +1,172 @@ +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 IEnumerable CompareToInitial() + { + if (DungeonId != DungeonType.None) + { + yield return new OpProgressChange(DungeonId, Progress); + yield return new OpMapDataChange(Rooms); + yield return new OpPartyStateChange(Party); + yield return new OpPomandersChange(Pomanders); + yield return new OpChestsChange(Chests); + yield return new OpMagiciteChange(Magicite); + } + } + + public Event ProgressChanged = new(); + public sealed record class OpProgressChange(DungeonType DungeonId, DungeonProgress Value) : WorldState.Operation + { + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.DungeonId = DungeonId; + ws.DeepDungeon.Progress = Value; + ws.DeepDungeon.ProgressChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDPG"u8) + .Emit((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(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(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(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(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(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..7271ee6870 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; @@ -73,6 +74,8 @@ public List CompareToInitial() ops.AddRange(client); ops.AddRange(network); return ops; + foreach (var o in DeepDungeon.CompareToInitial()) + yield return o; } // implementation of operations public Event FrameStarted = new(); 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/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..970c34d1ba 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; @@ -183,6 +184,7 @@ public unsafe void Update(TimeSpan prevFramePerf) UpdateActors(); UpdateParty(); UpdateClient(); + UpdateDeepDungeon(); } private unsafe void UpdateWaymarks() @@ -662,6 +664,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) 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/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs new file mode 100644 index 0000000000..51163d5057 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/AncelRockfist.cs @@ -0,0 +1,59 @@ +namespace BossMod.Endwalker.Quest.LifeEphemeralPathEternal; + +class ElectrogeneticForce(BossModule module) : Components.CastTowers(module, ActionID.MakeSpell(AID.ElectrogeneticForce), 6); +class RawRockbreaker(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeCircle(10), new AOEShapeDonut(10, 20)]) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.RawRockbreaker) + AddSequence(caster.Position, Module.CastFinishAt(spell)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var idx = (AID)spell.Action.ID switch + { + AID.RawRockbreaker1 => 0, + AID.RawRockbreaker2 => 1, + _ => -1 + }; + AdvanceSequence(idx, caster.Position, WorldState.FutureTime(2)); + } + + public override void Update() + { + if (!Module.PrimaryActor.IsTargetable) + Sequences.Clear(); + } +} +class ChiBlast(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ChiBlast1)); +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCircle(6)); +class ArmOfTheScholar(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArmOfTheScholar), new AOEShapeCircle(5)); + +class ClassicalFire(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ClassicalFire), 6); +class ClassicalThunder(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.ClassicalThunder), 6); +class ClassicalBlizzard(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ClassicalBlizzard), 6); +class ClassicalStone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ClassicalStone), new AOEShapeCircle(15)); + +class AncelRockfistStates : StateMachineBuilder +{ + public AncelRockfistStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69608, NameID = 10732)] +public class AncelRockfist(WorldState ws, Actor primary) : BossModule(ws, primary, new(224.8f, -855.8f), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs new file mode 100644 index 0000000000..d9879ffa03 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Enums.cs @@ -0,0 +1,74 @@ +namespace BossMod.Endwalker.Quest.LifeEphemeralPathEternal; + +public enum OID : uint +{ + Boss = 0x35C5, + BossP2 = 0x35C6, + Helper = 0x233C, + MahaudFlamehand = 0x35C4, // R0.500, x1 + Lalah = 0x35C2, + Loifa = 0x35C3, + Mahaud = 0x361C, + Ancel = 0x361D, + EnhancedNoulith = 0x3859, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + ChiBlast = 26838, // Boss->self, 5.0s cast, single-target + ChiBlast1 = 26839, // Helper->self, 5.0s cast, range 100 circle + ChiBomb = 26835, // Boss->self, 5.0s cast, single-target + Explosion = 26837, // 35C7->self, 5.0s cast, range 6 circle + ArmOfTheScholar = 26836, // Boss->self, 5.0s cast, range 5 circle + RawRockbreaker = 26832, // Boss->self, 5.0s cast, single-target + RawRockbreaker1 = 26833, // Helper->self, 4.0s cast, range 10 circle + RawRockbreaker2 = 26834, // Helper->self, 4.0s cast, range 10-20 donut + DemifireII = 26842, // MahaudFlamehand->Lalah, 8.0s cast, single-target + Demiburst = 26843, // MahaudFlamehand->self, 7.0s cast, single-target + ElectrogeneticForce = 26844, // Helper->self, 8.0s cast, range 6 circle + ElectrogeneticBlast = 26845, // Helper->self, 1.0s cast, range 80 circle + DemifireIII = 26841, // MahaudFlamehand->Lalah, 3.0s cast, single-target + FourElements = 26846, // MahaudFlamehand->self, 8.0s cast, single-target + ClassicalFire = 26847, // Helper->Lalah, 8.0s cast, range 6 circle + ClassicalThunder = 26848, // Helper->player/Loifa/Lalah, 5.0s cast, range 6 circle + ClassicalBlizzard = 26849, // Helper->location, 5.0s cast, range 6 circle + ClassicalStone = 26850, // Helper->self, 9.0s cast, range 50 circle + + Nouliths = 26851, // BossP2->self, 5.0s cast, single-target + AetherstreamTank = 26852, // 35C8->Lalah, no cast, range 50 width 4 rect + AetherstreamPlayer = 26853, // 35C8->players/Loifa, no cast, range 50 width 4 rect + Tracheostomy = 26854, // BossP2->self, 5.0s cast, range 10-20 donut + RightScalpel = 26855, // BossP2->self, 5.0s cast, range 15 210-degree cone + LeftScalpel = 26856, // BossP2->self, 5.0s cast, range 15 210-degree cone + Laparotomy = 26857, // BossP2->self, 5.0s cast, range 15 120-degree cone + Amputation = 26858, // BossP2->self, 7.0s cast, range 20 120-degree cone + Hypothermia = 26861, // BossP2->self, 5.0s cast, range 50 circle + Cryonics = 26860, // Helper->player, 8.0s cast, range 6 circle + Cryonics1 = 26859, // BossP2->self, 8.0s cast, single-target + Craniotomy = 28386, // BossP2->self, 8.0s cast, range 40 circle + RightLeftScalpel = 26862, // BossP2->self, 7.0s cast, range 15 210-degree cone + RightLeftScalpel1 = 26863, // BossP2->self, 3.0s cast, range 15 210-degree cone + LeftRightScalpel = 26864, // BossP2->self, 7.0s cast, range 15 210-degree cone + LeftRightScalpel1 = 26865, // BossP2->self, 3.0s cast, range 15 210-degree cone + Frigotherapy = 26866, // BossP2->self, 5.0s cast, single-target + Frigotherapy1 = 26867, // Helper->players/Mahaud/Loifa, 7.0s cast, range 5 circle +} + +public enum IconID : uint +{ + Tankbuster = 230, // Lalah + Noulith = 244, // player/Loifa +} + +public enum TetherID : uint +{ + Noulith = 17, // StrengthenedNoulith->Lalah/player/Loifa + Craniotomy = 174, // EnhancedNoulith->Lalah/Loifa/player/Mahaud/Ancel +} + +public enum SID : uint +{ + Craniotomy = 2968, // none->player/Lalah/Mahaud/Ancel/Loifa, extra=0x0 + DownForTheCount = 1953, // none->player/Lalah/Mahaud/Ancel/Loifa, extra=0xEC7 + +} diff --git a/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs new file mode 100644 index 0000000000..5e12572506 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/LifeEphemeralPathEternal/Guildivain.cs @@ -0,0 +1,91 @@ +namespace BossMod.Endwalker.Quest.LifeEphemeralPathEternal; + +class AetherstreamTether(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeRect(50, 2), (uint)TetherID.Noulith) +{ + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.AetherstreamPlayer or AID.AetherstreamTank) + CurrentBaits.RemoveAll(x => x.Target.InstanceID == spell.MainTargetID); + } +} + +class Tracheostomy : Components.SelfTargetedAOEs +{ + public Tracheostomy(BossModule module) : base(module, ActionID.MakeSpell(AID.Tracheostomy), new AOEShapeDonut(10, 20)) + { + WorldState.Actors.EventStateChanged.Subscribe((act) => + { + if (act.OID == 0x1EA1A1 && act.EventState == 7) + Arena.Bounds = new ArenaBoundsCircle(20); + }); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (spell.Action == WatchedAction) + Arena.Bounds = new ArenaBoundsCircle(10); + } +} + +class RightScalpel(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightScalpel), new AOEShapeCone(15, 105.Degrees())); +class LeftScalpel(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LeftScalpel), new AOEShapeCone(15, 105.Degrees())); +class Laparotomy(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Laparotomy), new AOEShapeCone(15, 60.Degrees())); +class Amputation(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Amputation), new AOEShapeCone(20, 60.Degrees())); + +class Hypothermia(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Hypothermia)); +class Cryonics(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.Cryonics), 6); +class Craniotomy(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Craniotomy)); +class RightLeftScalpel1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightLeftScalpel), new AOEShapeCone(15, 105.Degrees())); +class RightLeftScalpel2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightLeftScalpel1), new AOEShapeCone(15, 105.Degrees())); +class LeftRightScalpel1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LeftRightScalpel), new AOEShapeCone(15, 105.Degrees())); +class LeftRightScalpel2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LeftRightScalpel1), new AOEShapeCone(15, 105.Degrees())); + +class EnhancedNoulith(BossModule module) : Components.Adds(module, (uint)OID.EnhancedNoulith) +{ + private readonly List<(Actor, Actor)> Tethers = []; + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.Craniotomy && WorldState.Actors.Find(tether.Target) is Actor target) + Tethers.Add((source, target)); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if (status.ID == (uint)SID.Craniotomy) + Tethers.RemoveAll(t => t.Item2 == actor); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + foreach (var t in Tethers) + Arena.AddLine(t.Item1.Position, t.Item2.Position, ArenaColor.Danger); + } +} +class Frigotherapy(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Frigotherapy1), 5); + +class GuildivainOfTheTaintedEdgeStates : StateMachineBuilder +{ + public GuildivainOfTheTaintedEdgeStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69608, NameID = 10733, PrimaryActorOID = (uint)OID.BossP2)] +public class GuildivainOfTheTaintedEdge(WorldState ws, Actor primary) : BossModule(ws, primary, new(224.8f, -855.8f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Endwalker/Quest/SagesFocus.cs b/BossMod/Modules/Endwalker/Quest/SagesFocus.cs new file mode 100644 index 0000000000..72a5e90d80 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/SagesFocus.cs @@ -0,0 +1,65 @@ +namespace BossMod.Endwalker.Quest.SagesFocus; + +public enum OID : uint +{ + Boss = 0x3587, + Helper = 0x233C, + Mahaud = 0x3586, + Loifa = 0x3588, +} + +public enum AID : uint +{ + TripleThreat = 26535, // Boss->3589, 8.0s cast, single-target + ChiBomb = 26536, // Boss->self, 5.0s cast, single-target + Explosion = 26537, // 358D->self, 5.0s cast, range 6 circle + ArmOfTheScholar = 26543, // Boss->self, 5.0s cast, range 5 circle + Nouliths = 26538, // 3588->self, 5.0s cast, single-target + Noubelea = 26541, // 3588->self, 5.0s cast, single-target + Noubelea1 = 26542, // 358E->self, 5.0s cast, range 50 width 4 rect + DemiblizzardIII = 26545, // 3586->self, 5.0s cast, single-target + DemiblizzardIII1 = 26546, // Helper->self, 5.0s cast, range -40 donut + Demigravity = 26539, // 3586->location, 5.0s cast, range 6 circle + Demigravity1 = 26550, // Helper->location, 5.0s cast, range 6 circle + DemifireIII = 26547, // 3586->self, 5.0s cast, single-target + DemifireIII1 = 26548, // Helper->self, 5.6s cast, range 40 circle + DemifireII = 26552, // Mahaud->self, 7.0s cast, single-target + DemifireII1 = 26553, // Helper->player/3589, 5.0s cast, range 5 circle + DemifireII2 = 26554, // Helper->location, 5.0s cast, range 14 circle +} + +class DemifireSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.DemifireII1), 5); +class DemifireII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.DemifireII2), 14); +class DemifireIII(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DemifireIII1)); +class Noubelea(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Noubelea1), new AOEShapeRect(50, 2)); +class Demigravity(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Demigravity), 6); +class Demigravity1(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Demigravity1), 6); +class Demiblizzard(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DemiblizzardIII1), new AOEShapeDonut(10, 40)); +class TripleThreat(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.TripleThreat)); +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCircle(6)); +class ArmOfTheScholar(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArmOfTheScholar), new AOEShapeCircle(5)); + +class AncelRockfistStates : StateMachineBuilder +{ + public AncelRockfistStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69604, NameID = 10732)] +public class AncelRockfist(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, -82.17f), new ArenaBoundsCircle(18.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Endwalker/Quest/TheKillingArt.cs b/BossMod/Modules/Endwalker/Quest/TheKillingArt.cs new file mode 100644 index 0000000000..7c93da35e1 --- /dev/null +++ b/BossMod/Modules/Endwalker/Quest/TheKillingArt.cs @@ -0,0 +1,87 @@ +namespace BossMod.Endwalker.Quest.TheKillingArt; + +public enum OID : uint +{ + Boss = 0x3664, // R1.500, x1 + Helper = 0x233C, // R0.500, x10, Helper type + VoidHecteyes = 0x3666, // R1.200, x0 (spawn during fight) + VoidPersona = 0x3667, // R1.200, x0 (spawn during fight) + Voidzone = 0x1E963D +} + +public enum AID : uint +{ + MeatySlice = 27590, // Boss->self, 3.4+0.6s cast, single-target + MeatySlice1 = 27591, // Helper->self, 4.0s cast, range 50 width 12 rect + Cleaver = 27594, // Boss->self, 3.5+0.5s cast, single-target + Cleaver1 = 27595, // Helper->self, 4.0s cast, range 40 120-degree cone + FlankCleaver = 27596, // Boss->self, 3.5+0.5s cast, single-target + FlankCleaver1 = 27597, // Helper->self, 4.0s cast, range 40 120-degree cone + Explosion = 27606, // VoidHecteyes->self, 20.0s cast, range 60 circle + Explosion1 = 27607, // VoidPersona->self, 20.0s cast, range 50 circle + FocusInferi = 27592, // Boss->self, 2.9+0.6s cast, single-target + FocusInferi1 = 27593, // Helper->location, 3.5s cast, range 6 circle + CarnemLevare = 27598, // Boss->self, 4.0s cast, single-target + CarnemLevare1 = 27599, // Helper->self, 4.0s cast, range 40 width 8 cross + CarnemLevare2 = 27602, // Helper->self, 3.5s cast, range -17 donut + CarnemLevare3 = 27600, // Helper->self, 3.5s cast, range -7 donut + CarnemLevare4 = 27603, // Helper->self, 3.5s cast, range -22 donut + CarnemLevare5 = 27601, // Helper->self, 3.5s cast, range -12 donut + VoidMortar = 27604, // Boss->self, 4.0+1.0s cast, single-target + VoidMortar1 = 27605, // Helper->self, 5.0s cast, range 13 circle +} + +class VoidMortar(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VoidMortar1), new AOEShapeCircle(13)); +class FocusInferi(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 6, ActionID.MakeSpell(AID.FocusInferi1), m => m.Enemies(OID.Voidzone).Where(x => x.EventState != 7), 0); +class CarnemLevareCross(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CarnemLevare1), new AOEShapeCross(40, 4)); +class CarnemLevareDonut(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor, AOEShape)> Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Casters.Take(4).Select(c => new AOEInstance(c.Item2, c.Item1.Position, c.Item1.CastInfo!.Rotation, Module.CastFinishAt(c.Item1.CastInfo))); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + AOEShape? sh = (AID)spell.Action.ID switch + { + AID.CarnemLevare2 => new AOEShapeDonutSector(12, 17, 90.Degrees()), + AID.CarnemLevare3 => new AOEShapeDonutSector(2, 7, 90.Degrees()), + AID.CarnemLevare4 => new AOEShapeDonutSector(17, 22, 90.Degrees()), + AID.CarnemLevare5 => new AOEShapeDonutSector(7, 12, 90.Degrees()), + _ => null + }; + + if (sh != null) + Casters.Add((caster, sh)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.CarnemLevare2 or AID.CarnemLevare3 or AID.CarnemLevare4 or AID.CarnemLevare5) + Casters.RemoveAll(x => x.Item1 == caster); + } +} +class MeatySlice(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MeatySlice1), new AOEShapeRect(50, 6)); +class Cleaver(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Cleaver1), new AOEShapeCone(40, 60.Degrees())); +class FlankCleaver(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FlankCleaver1), new AOEShapeCone(40, 60.Degrees())); +class Adds(BossModule module) : Components.AddsMulti(module, [(uint)OID.VoidHecteyes, (uint)OID.VoidPersona], 1); + +class OrcusStates : StateMachineBuilder +{ + public OrcusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69614, NameID = 10581)] +public class Orcus(WorldState ws, Actor primary) : BossModule(ws, primary, new(-69.7f, -388.5f), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs b/BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs new file mode 100644 index 0000000000..3d1a98e541 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/ASpectacleForTheAges.cs @@ -0,0 +1,34 @@ +namespace BossMod.Heavensward.Quest.ASpectacleForTheAges; + +public enum OID : uint +{ + Boss = 0x154E, + Tizona = 0x1552 +} + +public enum AID : uint +{ + FlamingTizona = 5763, // D25->location, 3.0s cast, range 6 circle + TheCurse = 5765, // D25->self, 3.0s cast, range 7+R ?-degree cone +} + +class FlamingTizona(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FlamingTizona), 6); +class TheCurse(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TheCurse), new AOEShapeDonutSector(2, 7, 90.Degrees())); + +class Demoralize(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(0x1E9FA8).Where(e => e.EventState != 7)); +class Tizona(BossModule module) : Components.Adds(module, (uint)OID.Tizona, 5); + +class FlameGeneralAldynnStates : StateMachineBuilder +{ + public FlameGeneralAldynnStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67775, NameID = 4739)] +public class FlameGeneralAldynn(WorldState ws, Actor primary) : BossModule(ws, primary, new(-35.75f, -205.5f), new ArenaBoundsCircle(15)); diff --git a/BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs b/BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs new file mode 100644 index 0000000000..b8941f2050 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/CloseEncountersOfTheVIthKind.cs @@ -0,0 +1,62 @@ +namespace BossMod.Heavensward.Quest.CloseEncountersOfTheVIthKind; + +public enum OID : uint +{ + Boss = 0xF1C, // R0.550, x? + Puddle = 0x1E88F5, // R0.500, x? + TerminusEst = 0xF5D, // R1.000, x? +} + +public enum AID : uint +{ + HandOfTheEmpire = 4000, // Boss->location, 2.0s cast, range 2 circle + TerminusEstBoss = 4005, // Boss->self, 3.0s cast, range 50 circle + TerminusEstAOE = 3825, // TerminusEst->self, no cast, range 40+R width 4 rect +} + +class RegulaVanHydrusStates : StateMachineBuilder +{ + public RegulaVanHydrusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +class HandOfTheEmpire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HandOfTheEmpire), 2); + +class Voidzone(BossModule module) : Components.PersistentVoidzone(module, 8, m => m.Enemies(OID.Puddle)); + +class TerminusEst(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.TerminusEstAOE)) +{ + private bool _active; + + private IEnumerable Adds => Module.Enemies(OID.TerminusEst).Where(x => !x.IsDead); + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Adds, ArenaColor.Danger, true); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + => _active ? Adds.Select(x => new AOEInstance(new AOEShapeRect(40, 2), x.Position, x.Rotation)) : []; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TerminusEstBoss) + _active = true; + } + + public override void OnActorDestroyed(Actor actor) + { + if ((OID)actor.OID == OID.TerminusEst) + _active = false; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67203, NameID = 3818)] +public class RegulaVanHydrus(WorldState ws, Actor primary) : BossModule(ws, primary, new(252.75f, 553), new ArenaBoundsCircle(19.5f)); + diff --git a/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs b/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs new file mode 100644 index 0000000000..c81f39bf27 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/DivineIntervention.cs @@ -0,0 +1,63 @@ +namespace BossMod.Heavensward.Quest.DivineIntervention; + +public enum OID : uint +{ + Boss = 0x1010, + Helper = 0x233C, + IshgardianSteelChain = 0x102C, // R1.000, x1 + SerPaulecrainColdfire = 0x1011, // R0.500, x1 + ThunderPicket = 0xEC4, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + LightningBolt = 3993, // EC4->E0F, 2.0s cast, width 4 rect charge + IronTempest = 1003, // Boss->self, 3.5s cast, range 5+R circle + Overpower = 720, // Boss->self, 2.5s cast, range 6+R 90-degree cone + RingOfFrost = 1316, // 1011->self, 3.0s cast, range 6+R circle + Rive = 1135, // Boss->self, 2.5s cast, range 30+R width 2 rect + Heartstopper = 866, // 1011->self, 2.5s cast, range 3+R width 3 rect +} + +class LightningBolt(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.LightningBolt), 2); +class IronTempest(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IronTempest), new AOEShapeCircle(5.5f)); +class Overpower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6.5f, 45.Degrees())); +class RingOfFrost(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RingOfFrost), new AOEShapeCircle(6.5f)); +class Rive(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Rive), new AOEShapeRect(30.5f, 1)); +class Heartstopper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Heartstopper), new AOEShapeRect(3.5f, 1.5f)); +class Chain(BossModule module) : Components.Adds(module, (uint)OID.IshgardianSteelChain, 1); + +class SerGrinnauxTheBullStates : StateMachineBuilder +{ + public SerGrinnauxTheBullStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.PrimaryActor.IsDeadOrDestroyed && module.Enemies(OID.SerPaulecrainColdfire).All(x => x.IsDeadOrDestroyed); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67133, NameID = 3850)] +public class SerGrinnauxTheBull(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 2), FunnyBounds) +{ + public static ArenaBoundsCustom NewBounds() + { + var arc = CurveApprox.CircleArc(new(3.6f, 0), 11.5f, 0.Degrees(), 180.Degrees(), 0.01f); + var arc2 = CurveApprox.CircleArc(new(-3.6f, 0), 11.5f, 180.Degrees(), 360.Degrees(), 0.01f); + + return new(16, new(arc.Concat(arc2).Select(a => a.ToWDir()))); + } + + public static readonly ArenaBoundsCustom FunnyBounds = NewBounds(); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Heavensward/Quest/DragoonsFate.cs b/BossMod/Modules/Heavensward/Quest/DragoonsFate.cs new file mode 100644 index 0000000000..6ffdd0b21f --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/DragoonsFate.cs @@ -0,0 +1,97 @@ +namespace BossMod.Heavensward.Quest.DragoonsFate; + +public enum OID : uint +{ + Boss = 0x10B9, // R7.000, x1 + Icicle = 0x10BC, // R2.500, x0 (spawn during fight) + Graoully = 0x10BA, // R7.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + PillarImpact = 3095, // 10BC->self, 3.0s cast, range 4+R circle + PillarPierce = 4259, // 10BC->self, 2.0s cast, range 80+R width 4 rect + Cauterize = 4260, // 10BA->self, 3.0s cast, range 48+R width 20 rect + SheetOfIce = 4261, // Boss->location, 2.5s cast, range 5 circle +} + +public enum SID : uint +{ + Prey = 904, // none->player/10BB, extra=0x0 + SlipperyPrey = 475, // none->player/10BB, extra=0x0 + ThinIce = 905, // Boss->player/10BB, extra=0x1/0x2/0x3 + DeepFreeze = 3479, // Boss->10BB/player, extra=0x1 +} + +class SheetOfIce(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SheetOfIce), 5); +class PillarImpact(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PillarImpact), new AOEShapeCircle(6.5f)); +class PillarPierce(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PillarPierce), new AOEShapeRect(82.5f, 2)); +class Cauterize(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Cauterize), new AOEShapeRect(55, 10)); + +class Prey(BossModule module) : BossComponent(module) +{ + private static readonly AOEShape Cleave = new AOEShapeCone(27, 65.Degrees()); + private int IceStacks(Actor actor) => actor.FindStatus(SID.ThinIce) is ActorStatus st ? st.Extra & 0xFF : 0; + + private Actor? PreyCur; + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if (status.ID == (uint)SID.Prey) + PreyCur = actor; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (PreyCur is not Actor prey) + return; + + var partner = WorldState.Party[slot == 0 ? PartyState.MaxAllianceSize : slot]!; + + // force debuff swap + if (IceStacks(prey) == 3) + hints.GoalZones.Add(p => p.InCircle(partner.Position, 2) ? 1 : 0); + else + { + // prevent premature swap, even though it doesn't really matter, because the debuff generally falls off with plenty of time left + hints.AddForbiddenZone(ShapeDistance.Circle(partner.Position, 5), WorldState.FutureTime(1)); + + if (Module.PrimaryActor.IsTargetable) + hints.AddForbiddenZone(Cleave.Distance(Module.PrimaryActor.Position, Module.PrimaryActor.AngleTo(partner)), WorldState.FutureTime(1)); + } + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + // sometimes partner loses prey status *after* we get it + if (status.ID == (uint)SID.Prey && actor == PreyCur) + PreyCur = null; + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (PreyCur is Actor p && Module.PrimaryActor is var primary && primary.IsTargetable) + Cleave.Outline(Arena, primary.Position, primary.AngleTo(p), ArenaColor.Danger); + } +} + +class GraoullyStates : StateMachineBuilder +{ + public GraoullyStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67231, NameID = 4190)] +public class Graoully(WorldState ws, Actor primary) : BossModule(ws, primary, BCenter, BBounds) +{ + public static readonly WPos BCenter = new(-515.285f, -304.69f); + private static readonly WPos[] Corners = [new(-483.91f, -299.22f), new(-519.70f, -272.85f), new(-546.66f, -309.50f), new(-510.38f, -336.53f)]; + public static readonly ArenaBoundsCustom BBounds = new(32, new(Corners.Select(c => c - BCenter))); +} diff --git a/BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs b/BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs new file mode 100644 index 0000000000..965839ee10 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs @@ -0,0 +1,125 @@ +<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs +namespace BossMod.Heavensward.Quest.MSQ.Heliodrome; +======== +namespace BossMod.Heavensward.Quest.FlyFreeMyPretty; +>>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs + +public enum OID : uint +{ + Boss = 0x195E, + Helper = 0x233C, + GrynewahtP2 = 0x195F, // R0.500, x0 (spawn during fight) + ImperialColossus = 0x1966, // R3.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + AugmentedUprising = 7608, // Boss->self, 3.0s cast, range 8+R 120-degree cone + AugmentedSuffering = 7607, // Boss->self, 3.5s cast, range 6+R circle + Heartstopper = 866, // ImperialEques->self, 2.5s cast, range 3+R width 3 rect + Overpower = 720, // ImperialLaquearius->self, 2.1s cast, range 6+R 90-degree cone + GrandSword = 7615, // ImperialColossus->self, 3.0s cast, range 18+R 120-degree cone + MagitekRay = 7617, // ImperialColossus->location, 3.0s cast, range 6 circle + GrandStrike = 7616, // ImperialColossus->self, 2.5s cast, range 45+R width 4 rect + ShrapnelShell = 7614, // GrynewahtP2->location, 2.5s cast, range 6 circle + MagitekMissiles = 7612, // GrynewahtP2->location, 5.0s cast, range 15 circle + +} + +class MagitekMissiles(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekMissiles), 15); +class ShrapnelShell(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.ShrapnelShell), 6); +class Firebomb(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(0x1E86DF).Where(e => e.EventState != 7)); + +class Uprising(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); +class Suffering(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), 6.5f); +class Heartstopper(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Heartstopper), new AOEShapeRect(3.5f, 1.5f)); +class Overpower(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6, 45.Degrees())); +class GrandSword(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GrandSword), new AOEShapeCone(21, 60.Degrees())); +class MagitekRay(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekRay), 6); +class GrandStrike(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GrandStrike), new AOEShapeRect(48, 2)); + +class Adds(BossModule module) : Components.AddsMulti(module, [0x1960, 0x1961, 0x1962, 0x1963, 0x1964, 0x1965, 0x1966]) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = (OID)e.Actor.OID == OID.ImperialColossus ? 5 : e.Actor.TargetID == actor.InstanceID ? 1 : 0; + } +} + +class Bounds(BossModule module) : BossComponent(module) +{ + public override void OnEventDirectorUpdate(uint updateID, uint param1, uint param2, uint param3, uint param4) + { + if (updateID == 0x10000002) + Arena.Bounds = new ArenaBoundsCircle(20); + } +} + +class ReaperAI(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.MountId == 103 && WorldState.Actors.Find(actor.TargetID) is var target && target != null) + { + if ((OID)target.OID == OID.ImperialColossus) + hints.ActionsToExecute.Push(ActionID.MakeSpell(Roleplay.AID.DiffractiveMagitekCannon), target, ActionQueue.Priority.High, targetPos: target.PosRot.XYZ()); + hints.ActionsToExecute.Push(ActionID.MakeSpell(Roleplay.AID.MagitekCannon), target, ActionQueue.Priority.High, targetPos: target.PosRot.XYZ()); + + hints.GoalZones.Add(hints.GoalSingleTarget(target, 25)); + } + } +} + +class GrynewahtStates : StateMachineBuilder +{ + public GrynewahtStates(BossModule module) : base(module) + { + State build(uint id) => SimpleState(id, 10000, "Enrage") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + + SimplePhase(1, id => build(id).ActivateOnEnter(), "P1") + .Raw.Update = () => module.Enemies(OID.GrynewahtP2).Count != 0; + DeathPhase(0x100, id => build(id).ActivateOnEnter().OnEnter(() => + { + module.Arena.Bounds = Grynewaht.CircleBounds; + })); + } +} + +<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 222, NameID = 5576)] +public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, default, hexBounds) +======== +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67894, NameID = 5576)] +public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), HexBounds) +>>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs +{ + private static readonly ArenaBoundsComplex hexBounds = new([new Polygon(default, 10.675f, 6, 30.Degrees())]); + public static readonly ArenaBoundsComplex CircleBounds = new([new Polygon(default, 20, 20)]); + +<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs + protected override bool CheckPull() => Raid.Player()!.InCombat; +======== + private static ArenaBoundsCustom BuildHexBounds() + { + var hexSideLen = 20 / MathF.Sqrt(3); + + // slight adjustment to account for player hitbox radius, otherwise dodges can get very sketchy + hexSideLen -= 1.5f; + + List verts = [new(hexSideLen, 0), hexSideLen * 30.Degrees().ToDirection(), -hexSideLen * 150.Degrees().ToDirection(), new(-hexSideLen, 0), hexSideLen * -30.Degrees().ToDirection(), hexSideLen * 150.Degrees().ToDirection()]; + return new(hexSideLen, new(verts)); + } +>>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs +} diff --git a/BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs b/BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs index dfda1784e4..965839ee10 100644 --- a/BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs +++ b/BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs @@ -1,4 +1,8 @@ +<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs namespace BossMod.Heavensward.Quest.MSQ.Heliodrome; +======== +namespace BossMod.Heavensward.Quest.FlyFreeMyPretty; +>>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs public enum OID : uint { @@ -12,11 +16,11 @@ 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 @@ -58,8 +62,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)); } } } @@ -90,11 +97,29 @@ State build(uint id) => SimpleState(id, 10000, "Enrage") } } +<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs [ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 222, NameID = 5576)] public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, default, hexBounds) +======== +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67894, NameID = 5576)] +public class Grynewaht(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), HexBounds) +>>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs { private static readonly ArenaBoundsComplex hexBounds = new([new Polygon(default, 10.675f, 6, 30.Degrees())]); public static readonly ArenaBoundsComplex CircleBounds = new([new Polygon(default, 20, 20)]); +<<<<<<<< HEAD:BossMod/Modules/Heavensward/Quest/MSQ/Heliodrome.cs protected override bool CheckPull() => Raid.Player()!.InCombat; +======== + private static ArenaBoundsCustom BuildHexBounds() + { + var hexSideLen = 20 / MathF.Sqrt(3); + + // slight adjustment to account for player hitbox radius, otherwise dodges can get very sketchy + hexSideLen -= 1.5f; + + List verts = [new(hexSideLen, 0), hexSideLen * 30.Degrees().ToDirection(), -hexSideLen * 150.Degrees().ToDirection(), new(-hexSideLen, 0), hexSideLen * -30.Degrees().ToDirection(), hexSideLen * 150.Degrees().ToDirection()]; + return new(hexSideLen, new(verts)); + } +>>>>>>>> merge:BossMod/Modules/Heavensward/Quest/FlyFreeMyPretty.cs } diff --git a/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs b/BossMod/Modules/Heavensward/Quest/MSQ/OneLifeOneWorld.cs index fb1690f1a8..344ee0a806 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) @@ -71,7 +71,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } else { - e.Priority = -1; + e.Priority = AIHints.Enemy.PriorityUndesirable; } } @@ -119,7 +119,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/TheFateOfStars.cs b/BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs new file mode 100644 index 0000000000..e9f3b69463 --- /dev/null +++ b/BossMod/Modules/Heavensward/Quest/TheFateOfStars.cs @@ -0,0 +1,50 @@ +namespace BossMod.Heavensward.Quest.TheFateOfStars; + +public enum OID : uint +{ + Boss = 0x161E, + Helper = 0x233C, + MagitekTurretI = 0x161F, // R0.600, x0 (spawn during fight) + MagitekTurretII = 0x1620, // R0.600, x0 (spawn during fight) + TerminusEst = 0x1621, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + MagitekSlug = 6026, // Boss->self, 2.5s cast, range 60+R width 4 rect + AetherochemicalGrenado = 6031, // 1620->location, 3.0s cast, range 8 circle + SelfDetonate = 6032, // 161F/1620->self, 5.0s cast, range 40+R circle + MagitekSpread = 6027, // Boss->self, 3.0s cast, range 20+R 240-degree cone +} + +class MagitekSlug(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekSlug), new AOEShapeRect(60, 2)); +class AetherochemicalGrenado(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AetherochemicalGrenado), 8); +class SelfDetonate(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SelfDetonate), "Kill turret before detonation!", true) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PriorityTargets) + if (h.Actor.CastInfo?.Action == WatchedAction) + h.Priority = 5; + } +} +class MagitekSpread(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekSpread), new AOEShapeCone(20.55f, 120.Degrees())); + +class RegulaVanHydrusStates : StateMachineBuilder +{ + public RegulaVanHydrusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67824, NameID = 3818)] +public class RegulaVanHydrus(WorldState ws, Actor primary) : BossModule(ws, primary, new(230, 79), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/RealmReborn/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/OperationArchon.cs b/BossMod/Modules/RealmReborn/Quest/OperationArchon.cs new file mode 100644 index 0000000000..2b69db0e91 --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/OperationArchon.cs @@ -0,0 +1,66 @@ +namespace BossMod.RealmReborn.Quest.OperationArchon; + +public enum OID : uint +{ + Boss = 0x38F5, // R1.500, x? + Helper = 0x233C, // R0.500, x?, Helper type + ImperialPilusPrior = 0x38F7, // R1.500, x0 (spawn during fight) + ImperialCenturion = 0x38F6, // R1.500, x0 (spawn during fight) +} + +public enum SID : uint +{ + DirectionalParry = 680 +} + +public enum AID : uint +{ + TartareanShockwave = 28871, // 38F5->self, 3.0s cast, range 7 circle + GalesOfTartarus = 28870, // 38F5->self, 3.0s cast, range 30 width 5 rect + MagitekMissiles = 28865, // 233C->location, 4.0s cast, range 7 circle + TartareanTomb = 28869, // 233C->self, 8.0s cast, range 11 circle + DrillShot = 28874, // Boss->self, 3.0s cast, range 30 width 5 rect + TartareanShockwave1 = 28877, // Boss->self, 6.0s cast, range 14 circle + GalesOfTartarus1 = 28876, // Boss->self, 6.0s cast, range 30 width 30 rect +} + +class Adds(BossModule module) : Components.Adds(module, (uint)OID.ImperialCenturion); +class Adds1(BossModule module) : Components.Adds(module, (uint)OID.ImperialPilusPrior); + +class MagitekMissiles(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekMissiles), 7); +class DrillShot(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DrillShot), new AOEShapeRect(30, 2.5f)); +class TartareanShockwave(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TartareanShockwave), new AOEShapeCircle(7)); +class BigTartareanShockwave(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TartareanShockwave1), new AOEShapeCircle(14)); +class GalesOfTartarus(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GalesOfTartarus), new AOEShapeRect(30, 2.5f)); +class BigGalesOfTartarus(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GalesOfTartarus1), new AOEShapeRect(30, 15)); +class DirectionalParry(BossModule module) : Components.DirectionalParry(module, (uint)OID.Boss) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Module.PrimaryActor.FindStatus(SID.DirectionalParry) != null) + hints.AddForbiddenZone(new AOEShapeCone(100, 45.Degrees()), Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, WorldState.FutureTime(10)); + } +} +class TartareanTomb(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TartareanTomb), new AOEShapeCircle(11)); + +class RhitahtynSasArvinaStates : StateMachineBuilder +{ + public RhitahtynSasArvinaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70057, NameID = 2160)] +public class RhitahtynSasArvina(WorldState ws, Actor primary) : BossModule(ws, primary, new(-689, -815), new ArenaBoundsCircle(14.5f)); diff --git a/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs b/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs new file mode 100644 index 0000000000..ec19d7d21a --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/TheStepsOfFaith.cs @@ -0,0 +1,263 @@ +namespace BossMod.RealmReborn.Quest.TheStepsOfFaith; + +public enum OID : uint +{ + Boss = 0x3A5F, // R30.000, x1 +} + +public enum AID : uint +{ + FlameBreathCast = 30185, // Vishap->self, 5.0s cast, range 1 width 2 rect + FlameBreathChannel = 30884, // Vishap->self, no cast, range 40 width 20 rect + Cauterize = 30878, // Boss->self, 30.5+4.5s cast, single-target + Touchdown = 26408, // Vishap->self, 6.0s cast, range 80 circle + Fireball = 30875, // Vishap->players/3A71/3A6F/3A6C/3A69/3A68/3A62/3A61/3A60/3A72/3A70/3A6B/3A6A/3A64/3A63, 6.0s cast, range 6 circle + BodySlam = 26401, // Vishap->self, 6.0s cast, range 80 width 44 rect + Flamisphere = 30883, // Vishap->location, 8.0s cast, range 10 circle + FlameBreath2Cast = 26411, // Boss->self, 3.8+1.2s cast, range 60 width 20 rect + RipperClaw = 31262, // 3ABD->self, 3.7s cast, range 9 ?-degree cone + EarthshakerAOE = 30880, // Boss->self, 4.5s cast, range 31 circle + Earthshaker = 30887, // Vishap->self, 6.5s cast, range 80 30-degree cone + EarthrisingAOE = 26410, // Boss->self, 4.5s cast, range 31 circle + EarthrisingCast = 30888, // Vishap->self, 7.0s cast, range 8 circle + EarthrisingRepeat = 26412, // Vishap->self, no cast, range 8 circle + SidewiseSlice = 30879, // Boss->self, 8.0s cast, range 50 120-degree cone + ScorchingBreath = 29785, // Boss->self, 15.0+5.0s cast, single-target + +} + +class RipperClaw(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RipperClaw), new AOEShapeCone(9, 45.Degrees())); + +class EarthShakerAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EarthshakerAOE), new AOEShapeCircle(31)); +class Earthshaker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Earthshaker), new AOEShapeCone(80, 15.Degrees()), maxCasts: 2); + +class EarthrisingAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EarthrisingAOE), new AOEShapeCircle(31)); +class Earthrising(BossModule module) : Components.Exaflare(module, 8) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.EarthrisingCast) + { + Lines.Add(new() { Next = caster.Position, Advance = new(0, -7.5f), NextExplosion = Module.CastFinishAt(spell), TimeToMove = 1, ExplosionsLeft = 5, MaxShownExplosions = 2 }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.EarthrisingRepeat or AID.EarthrisingCast) + { + foreach (var l in Lines.Where(l => l.Next.AlmostEqual(caster.Position, 1))) + AdvanceLine(l, caster.Position); + ++NumCasts; + } + } +} + +class SidewiseSlice(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SidewiseSlice), new AOEShapeCone(50, 60.Degrees())); + +class FireballSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Fireball), 6); + +class Flamisphere(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Flamisphere), new AOEShapeCircle(10)); + +class BodySlam(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.BodySlam), 20, kind: Kind.DirForward, stopAtWall: true); + +class FlameBreath(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.FlameBreathChannel)) +{ + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.FlameBreathCast) + _aoe = new(new AOEShapeRect(500, 10), Module.PrimaryActor.Position, 180.Degrees(), Module.CastFinishAt(spell).AddSeconds(1)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (NumCasts >= 35) + { + _aoe = null; + NumCasts = 0; + } + } +} + +class FlameBreath2(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.FlameBreathChannel)) +{ + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.FlameBreath2Cast) + { + NumCasts = 0; + + _aoe = new(new AOEShapeRect(60, 10), caster.Position, spell.Rotation, Module.CastFinishAt(spell)); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (NumCasts >= 14) + { + _aoe = null; + } + } +} + +class Cauterize(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.Cauterize)) +{ + private Actor? Source; + + private static readonly AOEShapeRect MoveIt = new(40, 22, 38); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Source == null) + yield break; + + if (Arena.Center.Z > 218) + yield return new AOEInstance(MoveIt, Arena.Center); + else + yield return new AOEInstance(new AOEShapeRect(160, 22), Source.Position, 180.Degrees(), Module.CastFinishAt(Source.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + Source = Module.PrimaryActor; + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + Source = null; + } +} + +class Touchdown(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.Touchdown), 10, stopAtWall: true); + +class ScorchingBreath(BossModule module) : Components.GenericAOEs(module) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.ScorchingBreath) + NumCasts++; + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (NumCasts > 0) + yield return new AOEInstance(new AOEShapeRect(100, 10, 100), Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, Module.CastFinishAt(Module.PrimaryActor.CastInfo)); + } +} + +class ScrollingBounds(BossModule module) : BossComponent(module) +{ + public const float HalfHeight = 40; + public const float HalfWidth = 22; + + public static readonly ArenaBoundsRect Bounds = new(HalfWidth, HalfHeight); + + private int Phase = 1; + private (float Min, float Max) ZBounds = (120, 300); + + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 3 && state == 0x20001) + { + ZBounds = (120, 200); + Phase = 2; + } + + if (index == 0 && state == 0x800040) + { + ZBounds = (-40, 200); + Phase = 3; + } + + if (index == 4 && state == 0x20001) + { + ZBounds = (-40, 40); + Phase = 4; + } + + if (index == 1 && state == 0x800040) + { + ZBounds = (-200, 40); + Phase = 5; + } + + if (index == 6 && state == 0x20001) + { + ZBounds = (-200, -120); + Phase = 6; + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // force player to walk south to aggro vishap (status 1268 = In Event, not actionable) + if (Phase == 1 && !actor.InCombat && actor.FindStatus(1268) == null) + hints.AddForbiddenZone(new AOEShapeRect(38, 22, 40), Arena.Center); + + // subsequent state transitions don't trigger until player moves into the area + if (Phase == 3 && actor.Position.Z > 25) + hints.AddForbiddenZone(new AOEShapeRect(40, 22, 38), Arena.Center); + + if (Phase == 5 && actor.Position.Z > -135) + hints.AddForbiddenZone(new AOEShapeRect(40, 22, 38), Arena.Center); + } + + public override void Update() + { + base.Update(); + if (WorldState.Party.Player() is not Actor p) + return; + + Arena.Center = new(0, Math.Clamp(p.Position.Z, ZBounds.Min + HalfHeight, ZBounds.Max - HalfHeight)); + } +} + +class VishapStates : StateMachineBuilder +{ + public VishapStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70127, NameID = 3330)] +public class Vishap(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 245), ScrollingBounds.Bounds) +{ + // vishap doesn't start targetable + protected override bool CheckPull() => PrimaryActor.InCombat; + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + Arena.Actor(PrimaryActor, ArenaColor.Enemy, true); + } +} + diff --git a/BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs b/BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs new file mode 100644 index 0000000000..ef6bf5515e --- /dev/null +++ b/BossMod/Modules/RealmReborn/Quest/TheUltimateWeapon.cs @@ -0,0 +1,140 @@ +namespace BossMod.RealmReborn.Quest.TheUltimateWeapon; + +public enum OID : uint +{ + Boss = 0x3933, // R1.750, x? + SeaOfPitch = 0x1EB738, // R0.500, x?, EventObj type + Firesphere = 0x3934, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + AncientFireIII = 29327, // Boss->self, 4.0s cast, range 40 circle + DarkThunder = 29329, // Lahabrea->self, 4.0s cast, range 1 circle + EndOfDays = 29331, // Boss->self, 4.0s cast, range 60 width 8 rect + EndOfDaysAdds = 29762, // PhantomLahabrea->self, 4.0s cast, range 60 width 8 rect + Nightburn = 29340, // Boss->player, 4.0s cast, single-target + FiresphereSummon = 29332, // Boss->self, 4.0s cast, single-target + Burst = 29333, // Firesphere->self, 3.0s cast, range 8 circle + AncientEruption = 29335, // Lahabrea->self, 4.0s cast, range 6 circle + FluidFlare = 29760, // Lahabrea->self, 4.0s cast, range 40 60-degree cone + AncientCross = 29756, // Lahabrea->self, 4.0s cast, range 6 circle + BurstFlare = 29758, // Lahabrea->self, 5.0s cast, range 60 circle + GripOfNight = 29337, // Boss->self, 6.0s cast, range 40 150-degree cone +} + +class BurstFlare(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.BurstFlare), 10) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + + // don't add any hints if Burst hasn't gone off yet, it tends to spook AI mode into running into deathwall + if (Module.Enemies(OID.Firesphere).Any(x => x.CastInfo?.RemainingTime > 0)) + return; + + foreach (var c in Casters) + hints.AddForbiddenZone(new AOEShapeDonut(5, 100), Arena.Center, default, Module.CastFinishAt(c.CastInfo)); + } +} + +class GripOfNight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GripOfNight), new AOEShapeCone(40, 75.Degrees())); + +class AncientCross(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AncientCross), new AOEShapeCircle(6), maxCasts: 8); + +class AncientEruption(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AncientEruption), new AOEShapeCircle(6)); + +class FluidFlare(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FluidFlare), new AOEShapeCone(40, 30.Degrees())); + +class FireSphere(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.Burst)) +{ + private DateTime? _predictedCast; + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.FiresphereSummon) + _predictedCast = WorldState.CurrentTime.AddSeconds(12); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Burst) + _predictedCast = Module.CastFinishAt(spell); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (_predictedCast is DateTime dt && dt > WorldState.CurrentTime) + foreach (var enemy in Module.Enemies(OID.Firesphere)) + yield return new AOEInstance(new AOEShapeCircle(8), enemy.Position, default, dt); + } +} + +class Nightburn(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Nightburn), "WoLbuster"); + +class AncientFire(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AncientFireIII), hint: "Raidwide + spawn deathwall"); + +class DeathWall(BossModule module) : BossComponent(module) +{ + private bool _active; + private bool _completed; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.AncientFireIII && !_completed) + _active = true; + } + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (_active) + new AOEShapeDonut(15, 100).Draw(Arena, Arena.Center, default, ArenaColor.AOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_active) + hints.AddForbiddenZone(new AOEShapeDonut(15, 100), Arena.Center); + } + + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 0 && state == 0x20001) + { + Module.Arena.Bounds = new ArenaBoundsCircle(15); + _completed = true; + _active = false; + } + } +} + +class DarkThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkThunder), new AOEShapeCircle(1)); + +class SeaOfPitch(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.SeaOfPitch).Where(x => x.EventState != 7)); + +class EndOfDays(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EndOfDays), new AOEShapeRect(60, 4)); +class EndOfDaysAdds(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EndOfDaysAdds), new AOEShapeRect(60, 4)); + +class LahabreaStates : StateMachineBuilder +{ + public LahabreaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70058, NameID = 2143)] +public class Lahabrea(WorldState ws, Actor primary) : BossModule(ws, primary, new(-704, 480), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs b/BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs new file mode 100644 index 0000000000..f605961e83 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/AFeastOfLies.cs @@ -0,0 +1,90 @@ +namespace BossMod.Shadowbringers.Quest.AFeastOfLies; + +public enum OID : uint +{ + Boss = 0x295A, + Helper = 0x233C, +} + +public enum AID : uint +{ + UnceremoniousBeheading = 16274, // Boss->self, 4.0s cast, range 10 circle + KatunCycle = 16275, // Boss->self, 4.0s cast, range 5-40 donut + MercilessRight = 16278, // Boss->self, 4.0s cast, single-target + MercilessRight1 = 16283, // 29FB->self, 3.8s cast, range 40 120-degree cone + MercilessRight2 = 16284, // 29FE->self, 4.2s cast, range 40 120-degree cone + Evisceration = 16277, // Boss->self, 4.5s cast, range 40 120-degree cone + HotPursuit = 16291, // Boss->self, 2.5s cast, single-target + HotPursuit1 = 16285, // 29E6->location, 3.0s cast, range 5 circle + NexusOfThunder = 16280, // Boss->self, 2.5s cast, single-target + NexusOfThunder1 = 16276, // 29E6->self, 4.3s cast, range 45 width 5 rect + LivingFlame = 16294, // Boss->self, 3.0s cast, single-target + Spiritcall = 16292, // Boss->self, 3.0s cast, range 40 circle + Burn = 16290, // 29C2->self, 4.5s cast, range 8 circle + RisingThunder = 16293, // Boss->self, 3.0s cast, single-target + Electrocution = 16286, // 295B->self, 10.0s cast, range 6 circle + ShatteredSky = 17191, // Boss->self, 4.0s cast, single-target + ShatteredSky1 = 16282, // 29E6->self, 0.5s cast, range 40 circle + NexusOfThunder2 = 16296, // 29E6->self, 6.3s cast, range 45 width 5 rect + MercilessLeft = 16279, // Boss->self, 4.0s cast, single-target + MercilessLeft1 = 16298, // 29FC->self, 3.8s cast, range 40 120-degree cone + MercilessLeft2 = 16297, // 29FD->self, 4.2s cast, range 40 120-degree cone +} + +class UnceremoniousBeheading(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.UnceremoniousBeheading), new AOEShapeCircle(10)); +class KatunCycle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KatunCycle), new AOEShapeDonut(5, 40)); +class MercilessRight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessRight1), new AOEShapeCone(40, 60.Degrees())); +class MercilessRight1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessRight2), new AOEShapeCone(40, 60.Degrees())); +class MercilessLeft(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessLeft1), new AOEShapeCone(40, 60.Degrees())); +class MercilessLeft1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessLeft2), new AOEShapeCone(40, 60.Degrees())); +class Evisceration(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Evisceration), new AOEShapeCone(40, 60.Degrees())); +class HotPursuit(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); +class NexusOfThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder1), new AOEShapeRect(45, 2.5f)); +class NexusOfThunder1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder2), new AOEShapeRect(45, 2.5f)); +class Burn(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Burn), new AOEShapeCircle(8), maxCasts: 5); +class Spiritcall(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.Spiritcall), 20, stopAtWall: true); + +class Electrocution(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Electrocution), new AOEShapeCircle(6)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Casters.Count == 12) + { + var enemy = hints.PotentialTargets.Where(x => x.Actor.OID == 0x295B).MinBy(e => actor.DistanceToHitbox(e.Actor)); + foreach (var e in hints.PotentialTargets) + e.Priority = e == enemy ? 1 : 0; + } + else + { + base.AddAIHints(slot, actor, assignment, hints); + } + } +} + +class SerpentHead(BossModule module) : Components.Adds(module, 0x29E8, 1); + +class RanjitStates : StateMachineBuilder +{ + public RanjitStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69167, NameID = 8374)] +public class Ranjit(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 18), new ArenaBoundsCircle(15)); diff --git a/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs b/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs new file mode 100644 index 0000000000..41bf920c9b --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/ATearfulReunion.cs @@ -0,0 +1,99 @@ +namespace BossMod.Shadowbringers.Quest.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.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardIV), new AOEShapeDonut(5, 20)); +class SanctifiedBlizzardII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardII), new AOEShapeCircle(5)); +class SanctifiedFireIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedFireIII), 6); +class SanctifiedBlizzardIII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedBlizzardIII), new AOEShapeCone(40.5f, 22.5f.Degrees())); +class Hollow(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.Hollow)); +class HollowTether(BossModule module) : Components.Chains(module, 1, chainLength: 5); +class SanctifiedFireIV(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SanctifiedFireIV1), 10); +class SanctifiedFlare(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.SanctifiedFlare), 6, 1) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + if (ActiveStacks.Any() && WorldState.Actors.First(x => x.OID == 0x29C3) is Actor cerigg) + { + hints.AddForbiddenZone(new AOEShapeDonut(6, 100), cerigg.Position, default, ActiveStacks.First().Activation); + } + } +} + +class LightningGlobe(BossModule module) : Components.GenericLineOfSightAOE(module, default, 100, false) +{ + private readonly List Balls = []; + private IEnumerable<(WPos Center, float Radius)> Hollows => Module.Enemies(OID.Hollow).Select(h => (h.Position, h.HitboxRadius)); + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == 6) + Balls.Add(source); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var b in Balls) + Arena.AddLine(pc.Position, b.Position, ArenaColor.Danger); + } + + public override void Update() + { + var player = Raid.Player(); + if (player == null) + return; + + Balls.RemoveAll(b => b.IsDead); + + var closestBall = Balls.OrderBy(player.DistanceToHitbox).FirstOrDefault(); + Modify(closestBall?.Position, Hollows); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Origin != null + && actor.Position.InCircle(Origin.Value, MaxRange) + && !Visibility.Any(v => !actor.Position.InCircle(Origin.Value, v.Distance) && actor.Position.InCone(Origin.Value, v.Dir, v.HalfWidth))) + { + hints.Add("Pull lightning orb into black hole!"); + } + } +} + +class PhronesisStates : StateMachineBuilder +{ + public PhronesisStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69164, NameID = 8931)] +public class Phronesis(WorldState ws, Actor primary) : BossModule(ws, primary, new(-256, -284), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs b/BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs new file mode 100644 index 0000000000..e6976f2ef1 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/CourageBornOfFear.cs @@ -0,0 +1,102 @@ +namespace BossMod.Shadowbringers.Quest.CourageBornOfFear; + +public enum OID : uint +{ + Boss = 0x29E1, // r=0.5 + Helper = 0x233C, + Andreia = 0x29E0, + Knight = 0x29E4, +} + +public enum AID : uint +{ + Overcome = 17088, // Boss->self, 3.0s cast, range 8+R 120-degree cone + SanctifiedFireII1 = 17188, // 29E3->29DF, no cast, range 5 circle + MythrilCyclone1 = 17087, // 29DD->self, 4.0s cast, range 50 circle + SanctifiedMeltdown = 17323, // 29DD->player/29DF, 5.0s cast, range 6 circle + MythrilCyclone2 = 17207, // 29DD->self, 8.0s cast, range 8-20 donut + UncloudedAscension1 = 17335, // 2AD1->self, 5.0s cast, range 10 circle + ThePathOfLight = 17230, // 2A3F->self, 5.5s cast, range 15 circle + InquisitorsBlade = 17095, // 29E4->self, 5.0s cast, range 40 180-degree cone + RainOfLight = 17082, // 29DD->location, 3.0s cast, range 4 circle + ArrowOfFortitude = 17211, // Andreia->self, 4.0s cast, range 30 width 8 rect + BodkinVolley1 = 17189, // Andreia->29DF, 6.0s cast, range 5 circle +} + +class ArrowOfFortitude(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArrowOfFortitude), new AOEShapeRect(30, 4)); +class BodkinVolley(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.BodkinVolley1), 5, minStackSize: 1); +class RainOfLight(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.RainOfLight), 4); +class ThePathOfLight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ThePathOfLight), new AOEShapeCircle(15)); +class InquisitorsBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.InquisitorsBlade), new AOEShapeCone(40, 90.Degrees())); +class MythrilCycloneKB(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.MythrilCyclone1), 18, stopAtWall: true); +class MythrilCycloneDonut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MythrilCyclone2), new AOEShapeDonut(8, 20)); +class SanctifiedMeltdown(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SanctifiedMeltdown), 6); +class UncloudedAscension(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.UncloudedAscension1), new AOEShapeCircle(10)); +class Overcome(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overcome), new AOEShapeCone(8.5f, 60.Degrees())); + +class SanctifiedFireII(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(5), 23, centerAtTarget: true) +{ + private DateTime Timeout = DateTime.MaxValue; + + public override void Update() + { + // for some reason, the magus can just forget to cast the two followups, leaving lue-reeq to run around like a moron + if (WorldState.CurrentTime > Timeout && CurrentBaits.Count > 0) + Reset(); + } + + private void Reset() + { + CurrentBaits.Clear(); + NumCasts = 0; + Timeout = DateTime.MaxValue; + } + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + base.OnEventIcon(actor, iconID, targetID); + if (iconID == IID) + Timeout = WorldState.FutureTime(10); + } + + public override void OnActorCreated(Actor actor) + { + if (actor.OID == 0x29E5 && ++NumCasts >= 3) + Reset(); + } +} + +class FireVoidzone(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 5, ActionID.MakeSpell(AID.SanctifiedFireII1), m => m.Enemies(0x29E5).Where(e => e.EventState != 7), 0.25f); + +class ImmaculateWarriorStates : StateMachineBuilder +{ + public ImmaculateWarriorStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.Enemies(OID.Andreia).All(x => x.IsDeadOrDestroyed); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68814, NameID = 8782)] +public class ImmaculateWarrior(WorldState ws, Actor primary) : BossModule(ws, primary, new(-247, 688.5f), new ArenaBoundsCircle(19.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.TargetID == actor.InstanceID ? 1 : 0; + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs new file mode 100644 index 0000000000..b36a861858 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P1TelotekGamma.cs @@ -0,0 +1,35 @@ +using BossMod.QuestBattle.Shadowbringers.MSQ; + +namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P1; + +public enum AID : uint +{ + AntiPersonnelMissile = 24845, // 233C->player/321D, 5.0s cast, range 6 circle + MRVMissile = 24843, // 233C->location, 8.0s cast, range 12 circle +} + +enum OID : uint +{ + Boss = 0x3376 +} + +class AlisaieAI(BossModule module) : QuestBattle.RotationModule(module); +class AntiPersonnelMissile(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.AntiPersonnelMissile), 6); +class MRVMissile(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MRVMissile), 12, maxCasts: 6); + +public class TelotekGammaStates : StateMachineBuilder +{ + public TelotekGammaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69602, NameID = 10189)] +public class TelotekGamma(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, -180), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs new file mode 100644 index 0000000000..ebc0e28f31 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P2LunarOdin.cs @@ -0,0 +1,110 @@ +using BossMod.QuestBattle; +using RID = BossMod.Roleplay.AID; + +namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P2; + +public enum OID : uint +{ + Boss = 0x3200, + Fetters = 0x3218 +} + +public enum AID : uint +{ + LunarGungnir = 24025, // LunarOdin->31EC, 12.0s cast, range 6 circle + LunarGungnir1 = 24026, // LunarOdin->2E2E, 25.0s cast, range 6 circle + GungnirAOE = 24698, // 233C->self, 10.0s cast, range 10 circle + Gagnrath = 24030, // 321C->self, 3.0s cast, range 50 width 4 rect + GungnirSpread = 24029, // 321C->self, no cast, range 10 circle + LeftZantetsuken = 24034, // LunarOdin->self, 4.0s cast, range 70 width 39 rect + RightZantetsuken = 24032, // LunarOdin->self, 4.0s cast, range 70 width 39 rect +} + +class UriangerAI(WorldState ws) : UnmanagedRotation(ws, 25) +{ + public const ushort StatusParam = 158; + + private float HeliosLeft(Actor p) => p.IsTargetable ? StatusDetails(p, 836, Player.InstanceID).Left : float.MaxValue; + + protected override void Exec(Actor? primaryTarget) + { + var partyPositions = World.Party.WithoutSlot().Select(p => p.Position).ToList(); + + Hints.GoalZones.Add(pos => partyPositions.Count(p => p.InCircle(pos, 16))); + + if (World.Party.WithoutSlot().All(p => HeliosLeft(p) < 1 && p.Position.InCircle(Player.Position, 15.5f + p.HitboxRadius))) + UseAction(RID.AspectedHelios, Player); + + if (World.Party.WithoutSlot().FirstOrDefault(p => p.HPMP.CurHP < p.HPMP.MaxHP * 0.4f) is Actor low) + UseAction(RID.Benefic, low); + + UseAction(RID.MaleficIII, primaryTarget); + + if (Player.FindStatus(Roleplay.SID.DestinyDrawn) != null) + { + if (ComboAction == RID.DestinyDrawn) + UseAction(RID.LordOfCrowns, primaryTarget, -100); + + if (ComboAction == RID.DestinysSleeve) + UseAction(RID.TheScroll, Player, -100); + } + else + { + UseAction(RID.DestinyDrawn, Player, -100); + UseAction(RID.DestinysSleeve, Player, -100); + } + + UseAction(RID.FixedSign, Player, -150); + } +} + +class Fetters(BossModule module) : Components.Adds(module, (uint)OID.Fetters); +class AutoUri(BossModule module) : RotationModule(module); +class GunmetalSoul(BossModule module) : Components.GenericAOEs(module) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Module.Enemies(0x1EB1D5).Where(e => e.EventState != 7).Select(e => new AOEInstance(new AOEShapeDonut(4, 100), e.Position)); +} +class LunarGungnir(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.LunarGungnir), 6); +class LunarGungnir2(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.LunarGungnir1), 6); +class Gungnir(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GungnirAOE), new AOEShapeCircle(10)); +class Gagnrath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Gagnrath), new AOEShapeRect(50, 2)); +class GungnirSpread(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(10), 189, ActionID.MakeSpell(AID.GungnirSpread), 5.3f, centerAtTarget: true); + +class Zantetsuken(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Casters.Select(c => new AOEInstance(new AOEShapeRect(70, 19.5f), actor.CastInfo!.LocXZ, actor.CastInfo!.Rotation, Module.CastFinishAt(actor.CastInfo))); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.RightZantetsuken or AID.LeftZantetsuken) + Casters.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.RightZantetsuken or AID.LeftZantetsuken) + Casters.Remove(caster); + } +} + +public class LunarOdinStates : StateMachineBuilder +{ + public LunarOdinStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69602, NameID = 10034)] +public class LunarOdin(WorldState ws, Actor primary) : BossModule(ws, primary, new(146.5f, 84.5f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs new file mode 100644 index 0000000000..f6bdd8e466 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P3LunarRavana.cs @@ -0,0 +1,109 @@ +using BossMod.QuestBattle; +using RID = BossMod.Roleplay.AID; + +namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P3; + +public enum OID : uint +{ + Boss = 0x3201, + Helper = 0x233C, + MoonGana = 0x3219, + SpiritGana = 0x321A, + RavanasWill = 0x321B, +} + +public enum AID : uint +{ + Explosion = 24046, // 3204->self, 5.0s cast, range 80 width 10 cross +} + +public enum SID : uint +{ + Invincibility = 325, +} + +class GrahaAI(WorldState ws) : UnmanagedRotation(ws, 25) +{ + private IEnumerable Adds => World.Actors.Where(x => (OID)x.OID is OID.MoonGana or OID.SpiritGana or OID.RavanasWill && x.IsTargetable && !x.IsDead); + + // Ravana's Wills just move to boss, whereas butterflies are only a threat once they start casting + private bool ShouldBreak(Actor a) => StatusDetails(a, Roleplay.SID.Break, Player.InstanceID).Left == 0 && ((OID)a.OID == OID.RavanasWill || a.CastInfo != null); + + protected override void Exec(Actor? primaryTarget) + { + var adds = Adds.ToList(); + + if (adds.Any(ShouldBreak)) + { + Hints.GoalZones.Add(p => adds.Count(a => a.Position.InCircle(p, 20))); + if (adds.Any(a => ShouldBreak(a) && a.Position.InCircle(Player.Position, 20))) + UseAction(RID.Break, Player); + } + + if (MP >= 1000 && Player.HPMP.CurHP * 3 < Player.HPMP.MaxHP) + UseAction(RID.CureII, Player); + + if (MP < 800) + UseAction(RID.AllaganBlizzardIV, primaryTarget); + + if (primaryTarget?.OID == 0x3201) + { + var thunder = StatusDetails(primaryTarget, Roleplay.SID.ThunderIV, Player.InstanceID); + if (thunder.Left < 3) + UseAction(RID.ThunderIV, primaryTarget); + } + + switch (ComboAction) + { + case RID.FireIV: + UseAction(RID.FireIV2, primaryTarget); + break; + case RID.FireIV2: + UseAction(RID.FireIV3, primaryTarget); + break; + case RID.FireIV3: + UseAction(RID.Foul, primaryTarget); + break; + default: + UseAction(RID.FireIV, primaryTarget); + break; + } + } +} + +class AutoGraha(BossModule module) : RotationModule(module); +class DirectionalParry(BossModule module) : Components.DirectionalParry(module, 0x3201) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Module.PrimaryActor.FindStatus(680) != null) + { + hints.AddForbiddenZone(new AOEShapeCone(100, 45.Degrees()), Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, WorldState.FutureTime(10)); + } + } +} +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), maxCasts: 2); + +class LunarRavanaStates : StateMachineBuilder +{ + public LunarRavanaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69602, NameID = 10037)] +public class LunarRavana(WorldState ws, Actor primary) : BossModule(ws, primary, new(-144, 83), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.CalculateModuleAIHints(slot, actor, assignment, hints); + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs new file mode 100644 index 0000000000..6201f5dbce --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/DeathUntoDawn/P4LunarIfrit.cs @@ -0,0 +1,44 @@ +namespace BossMod.Shadowbringers.Quest.DeathUntoDawn.P4; + +public enum OID : uint +{ + Boss = 0x3202, + Helper = 0x233C, + InfernalNail = 0x3205, +} + +public enum AID : uint +{ + RadiantPlume1 = 24057, // Helper->self, 7.0s cast, range 8 circle + Hellfire = 24058, // Boss->self, 36.0s cast, range 40 circle + Hellfire1 = 24059, // Boss->self, 28.0s cast, range 40 circle + CrimsonCyclone = 24054, // 3203->self, 4.5s cast, range 49 width 18 rect + Explosion = 24046, // 3204->self, 5.0s cast, range 80 width 10 cross + AgonyOfTheDamned1 = 24062, // Helper->self, 0.7s cast, range 40 circle +} + +class Hellfire(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Hellfire)); +class Hellfire1(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Hellfire1)); +class AgonyOfTheDamned(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AgonyOfTheDamned1)); +class RadiantPlume(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RadiantPlume1), new AOEShapeCircle(8)); +class CrimsonCyclone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CrimsonCyclone), new AOEShapeRect(49, 9), maxCasts: 3); +class InfernalNail(BossModule module) : Components.Adds(module, (uint)OID.InfernalNail, 5); +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(80, 5), maxCasts: 2); + +class LunarIfritStates : StateMachineBuilder +{ + public LunarIfritStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69602, NameID = 10041)] +public class LunarIfrit(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs new file mode 100644 index 0000000000..d70c8c34b0 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Ardbert.cs @@ -0,0 +1,113 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class Overcome(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overcome), new AOEShapeCone(8, 60.Degrees()), 2); +class Skydrive(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Skydrive), new AOEShapeCircle(5)); + +class SkyHighDrive(BossModule module) : Components.GenericRotatingAOE(module) +{ + Angle angle; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.SkyHighDriveCCW: + angle = -20.Degrees(); + return; + case AID.SkyHighDriveCW: + angle = 20.Degrees(); + return; + case AID.SkyHighDriveFirst: + if (angle != default) + { + Sequences.Add(new(new AOEShapeRect(40, 4), caster.Position, spell.Rotation, angle, Module.CastFinishAt(spell, 0.5f), 0.6f, 10, 4)); + } + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.SkyHighDriveFirst or AID.SkyHighDriveRest) + { + AdvanceSequence(caster.Position, caster.Rotation, WorldState.CurrentTime); + if (Sequences.Count == 0) + angle = default; + } + } +} + +class AvalancheAxe(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe1), new AOEShapeCircle(10)); +class AvalancheAxe2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe2), new AOEShapeCircle(10)); +class AvalancheAxe3(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AvalanceAxe3), new AOEShapeCircle(10)); +class OvercomeAllOdds(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.OvercomeAllOdds), new AOEShapeCone(60, 15.Degrees()), 1) +{ + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + base.OnCastFinished(caster, spell); + if (NumCasts > 0) + MaxCasts = 2; + } +} +class Soulflash(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Soulflash1), new AOEShapeCircle(4)); +class EtesianAxe(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.EtesianAxe), 15, kind: Kind.DirForward); +class Soulflash2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Soulflash2), new AOEShapeCircle(8)); + +class GroundbreakerExaflares(BossModule module) : Components.Exaflare(module, new AOEShapeCircle(6)) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.GroundbreakerExaFirst) + { + Lines.Add(new Line + { + Next = caster.Position, + Advance = caster.Rotation.ToDirection() * 6, + Rotation = default, + NextExplosion = Module.CastFinishAt(spell), + TimeToMove = 1, + ExplosionsLeft = 8, + MaxShownExplosions = 3 + }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID is (uint)AID.GroundbreakerExaFirst or (uint)AID.GroundbreakerExaRest) + { + var line = Lines.FirstOrDefault(x => x.Next.AlmostEqual(caster.Position, 1)); + if (line != null) + AdvanceLine(line, caster.Position); + } + } +} + +class GroundbreakerCone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundbreakerCone), new AOEShapeCone(40, 45.Degrees())); +class GroundbreakerDonut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundbreakerDonut), new AOEShapeDonut(5, 20)); +class GroundbreakerCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundbreakerCircle), new AOEShapeCircle(15)); + +class ArdbertStates : StateMachineBuilder +{ + public ArdbertStates(BossModule module) : base(module) + { + TrivialPhase(0) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 8258, PrimaryActorOID = (uint)OID.Ardbert)] +public class Ardbert(WorldState ws, Actor primary) : BossModule(ws, primary, new(-392, 780), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs new file mode 100644 index 0000000000..f7e49aae1f --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FadedMemories.cs @@ -0,0 +1,53 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +public enum OID : uint +{ + KingThordan = 0x2F1D, + FlameGeneralAldynn = 0x2F1E, + Nidhogg = 0x2F21, + Zenos = 0x2F28, + Ardbert = 0x2F2E, + Helper = 0x233C, +} + +public enum AID : uint +{ + // raubahn + FlamingTizona = 21094, // player->location, 4.0s cast, range 6 circle + + // thordan + TheDragonsGaze = 21090, // 2F1D->self, 4.0s cast, range 80 circle + + // nidhogg + HighJump = 21299, // player->self, 4.0s cast, range 8 circle + Geirskogul = 21098, // 2F22/2F21->self, 4.0s cast, range 62 width 8 rect + + // zenos + EntropicFlame = 21117, // Helper->self, 5.0s cast, range 50 width 8 rect + VeinSplitter = 21118, // 2F29->self, 5.0s cast, range 10 circle + + // ardbert + Overcome = 21126, // Ardbert->self, 2.5s cast, range 8 120-degree cone + Skydrive = 21127, // Ardbert->self, 2.5s cast, range 5 circle + SkyHighDriveCCW = 21138, // Ardbert->self, 4.5s cast, single-target + SkyHighDriveCW = 21139, // Ardbert->self, 4.5s cast, single-target + SkyHighDriveFirst = 21140, // 233C->self, 5.0s cast, range 40 width 8 rect + SkyHighDriveRest = 21141, // 233C->self, no cast, range 40 width 8 rect + AvalanceAxe1 = 21145, // 233C->self, 4.0s cast, range 10 circle + AvalanceAxe2 = 21144, // 233C->self, 7.0s cast, range 10 circle + AvalanceAxe3 = 21143, // 233C->self, 10.0s cast, range 10 circle + OvercomeAllOdds = 21130, // 233C->self, 2.5s cast, range 60 30-degree cone + Soulflash1 = 21136, // 233C->self, 4.0s cast, range 4 circle + EtesianAxe = 21147, // 233C->self, 6.5s cast, range 80 circle + Soulflash2 = 21137, // 233C->self, 4.0s cast, range 8 circle + GroundbreakerExaFirst = 21563, // 233C->self, 5.0s cast, range 6 circle + GroundbreakerExaRest = 21151, // 233C->self, no cast, range 6 circle + GroundbreakerCone = 21153, // 233C->self, 6.0s cast, range 40 90-degree cone + GroundbreakerDonut = 21157, // 233C->self, 6.0s cast, range 5-20 donut + GroundbreakerCircle = 21155, // 233C->self, 6.0s cast, range 15 circle +} + +public enum SID : uint +{ + Invincibility = 671 +} diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs new file mode 100644 index 0000000000..54d236eee2 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/FlameGeneralAldynn.cs @@ -0,0 +1,18 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class FlamingTizona(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FlamingTizona), 6); + +class FlameGeneralAldynnStates : StateMachineBuilder +{ + public FlameGeneralAldynnStates(BossModule module) : base(module) + { + TrivialPhase().ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 4739, PrimaryActorOID = (uint)OID.FlameGeneralAldynn)] +public class FlameGeneralAldynn(WorldState ws, Actor primary) : BossModule(ws, primary, new(-143, 357), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs new file mode 100644 index 0000000000..4eb731e55c --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/KingThordan.cs @@ -0,0 +1,23 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class DragonsGaze(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.TheDragonsGaze)); + +class KingThordanStates : StateMachineBuilder +{ + public KingThordanStates(BossModule module) : base(module) + { + TrivialPhase().ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 3632, PrimaryActorOID = (uint)OID.KingThordan)] +public class KingThordan(WorldState ws, Actor primary) : BossModule(ws, primary, new(-247, 321), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs new file mode 100644 index 0000000000..a7cc3c74c6 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Nidhogg.cs @@ -0,0 +1,15 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class HighJump(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HighJump), new AOEShapeCircle(8)); +class Geirskogul(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Geirskogul), new AOEShapeRect(62, 4)); + +class NidhoggStates : StateMachineBuilder +{ + public NidhoggStates(BossModule module) : base(module) + { + TrivialPhase().ActivateOnEnter().ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 3458, PrimaryActorOID = (uint)OID.Nidhogg)] +public class Nidhogg(WorldState ws, Actor primary) : BossModule(ws, primary, new(-242, 436.5f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs new file mode 100644 index 0000000000..848cdcc2a3 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FadedMemories/Zenos.cs @@ -0,0 +1,21 @@ +namespace BossMod.Shadowbringers.Quest.FadedMemories; + +class Swords(BossModule module) : Components.AddsMulti(module, [0x2F2A, 0x2F2B, 0x2F2C]); + +class EntropicFlame(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EntropicFlame), new AOEShapeRect(50, 4)); +class VeinSplitter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), new AOEShapeCircle(10)); + +class ZenosYaeGalvusStates : StateMachineBuilder +{ + public ZenosYaeGalvusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69311, NameID = 6039, PrimaryActorOID = (uint)OID.Zenos)] +public class ZenosYaeGalvus(WorldState ws, Actor primary) : BossModule(ws, primary, new(-321.03f, 617.73f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs b/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs new file mode 100644 index 0000000000..6e464d9242 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/FullSteamAhead.cs @@ -0,0 +1,126 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.FullSteamAhead; + +public enum OID : uint +{ + Boss = 0x295D, + LightningVoidzone = 0x1E9685 +} + +public enum AID : uint +{ + ShatteredSky = 16405, // Boss->self, 5.0s cast, single-target + ShatteredSky1 = 16429, // 233C->self, 6.0s cast, range 45 circle + HotPursuit = 16406, // Boss->self, 3.0s cast, single-target + HotPursuit1 = 16430, // 233C->location, 3.0s cast, range 5 circle + NexusOfThunder = 16404, // Boss->self, 3.0s cast, single-target + NexusOfThunder1 = 16427, // 233C->self, 7.0s cast, range 60+R width 5 rect + Wrath = 16425, // 295E->self, no cast, range 100 circle + CoiledLevin = 16424, // 295E->self, 3.0s cast, single-target + CoiledLevin1 = 16428, // 233C->self, 7.0s cast, range 6 circle + UnbridledWrath = 16426, // 295E->self, no cast, range 100 circle + HiddenCurrent = 16403, // Boss->location, no cast, ??? + VeilOfGukumatz = 16423, // 2998->self, no cast, single-target + VeilOfGukumatz1 = 16422, // 295D->self, no cast, single-target + VeilOfGukumatz2 = 16402, // Boss->self, no cast, single-target + UnceremoniousBeheading = 16412, // 295D->self, 3.5s cast, range 10 circle + HiddenCurrent1 = 16411, // 295D->location, no cast, ??? + MercilessLeft = 16415, // 295D->self, 4.0s cast, single-target + MercilessLeft1 = 33202, // 233C->self, 4.0s cast, range 40 120-degree cone + MercilessRight = 16431, // 233C->self, 4.0s cast, range 40 120-degree cone + KatunCycle = 16413, // 295D->self, 5.5s cast, range 5-40 donut + HotPursuit2 = 16410, // 295D->self, 3.0s cast, single-target + AgelessSerpent = 16417, // 295D->self, no cast, single-target + SerpentRising = 16433, // 295F->self, no cast, single-target + Evisceration = 16419, // 295D->self, 2.0s cast, range 40 120-degree cone + Spiritcall = 16420, // 295D->self, no cast, range 100 circle + SnakingFlame = 16432, // 295F->player, 40.0s cast, width 4 rect charge +} + +public enum SID : uint +{ + Smackdown = 2068, +} + +class KatunCycle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KatunCycle), new AOEShapeDonut(5, 40)); +class MercilessLeft(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessLeft1), new AOEShapeCone(40, 60.Degrees())); +class MercilessRight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MercilessRight), new AOEShapeCone(40, 60.Degrees())); +class UnceremoniousBeheading(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.UnceremoniousBeheading), new AOEShapeCircle(10)); +class Evisceration(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Evisceration), new AOEShapeCone(40, 60.Degrees())); + +class HotPursuit(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); +class NexusOfThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder1), new AOEShapeRect(60, 2.5f)); +class CoiledLevin(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CoiledLevin1), new AOEShapeCircle(6)); +class LightningVoidzone(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.LightningVoidzone).Where(x => x.EventState != 7)); + +class ThancredAI(BossModule module) : RotationModule(module); + +class AutoThancred(WorldState ws) : UnmanagedRotation(ws, 3) +{ + protected override void Exec(Actor? primaryTarget) + { + if (World.Client.DutyActions[0].CurCharges > 0) + { + UseAction(World.Client.DutyActions[0].Action, primaryTarget); + return; + } + + if (primaryTarget == null) + return; + + var distance = Player.DistanceToHitbox(primaryTarget); + + if (distance <= 3) + { + UseAction(Roleplay.AID.Smackdown, Player, -100); + + if (Player.FindStatus(SID.Smackdown) != null) + UseAction(Roleplay.AID.RoughDivide, primaryTarget, -100); + } + + if (Player.HPMP.CurHP * 2 < Player.HPMP.MaxHP) + UseAction(Roleplay.AID.SoothingPotion, Player, -100); + + switch (ComboAction) + { + case Roleplay.AID.BrutalShell: + UseAction(Roleplay.AID.SolidBarrel, primaryTarget); + break; + case Roleplay.AID.KeenEdge: + UseAction(Roleplay.AID.BrutalShell, primaryTarget); + break; + default: + UseAction(Roleplay.AID.KeenEdge, primaryTarget); + break; + } + } +} + +class RanjitStates : StateMachineBuilder +{ + public RanjitStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69155, NameID = 8374)] +public class Ranjit(WorldState ws, Actor primary) : BossModule(ws, primary, new(-203, 395), new ArenaBoundsCircle(19.5f)) +{ + protected override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Enemies(0x295C), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs b/BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs new file mode 100644 index 0000000000..29fcdbbe6e --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/GambolingForGil.cs @@ -0,0 +1,88 @@ +namespace BossMod.Shadowbringers.Quest.GambolingForGil; + +public enum OID : uint +{ + Boss = 0x29D2, // R0.500, x1 + Whirlwind = 0x29D5, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + WarDance = 17197, // Boss->self, 3.0s cast, range 5 circle + CharmingChasse = 17198, // Boss->self, 3.0s cast, range 40 circle + HannishFire1 = 17204, // 29D6->location, 3.3s cast, range 6 circle + Foxshot = 17289, // Boss->player, 6.0s cast, width 4 rect charge + HannishWaters = 17214, // 2A0B->self, 5.0s cast, range 40+R 30-degree cone + RanaasFinish = 15646, // Boss->self, 6.0s cast, range 15 circle +} + +class Foxshot(BossModule module) : Components.BaitAwayChargeCast(module, ActionID.MakeSpell(AID.Foxshot), 2); +class FoxshotKB(BossModule module) : Components.Knockback(module, stopAtWall: true) +{ + private readonly List Casters = []; + private Whirlwind? ww; + + public override IEnumerable Sources(int slot, Actor actor) => Casters.Select(c => new Source(c.Position, 25, Module.CastFinishAt(c.CastInfo))); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + ww ??= Module.FindComponent(); + + if (Casters.FirstOrDefault() is not Actor source) + return; + + var sources = ww?.Sources(Module).Select(p => p.Position).ToList() ?? []; + if (sources.Count == 0) + return; + + hints.AddForbiddenZone(p => + { + foreach (var s in sources) + if (Intersect.RayCircle(source.Position, source.DirectionTo(p), s, 6) < 1000) + return -1; + + return 1; + }, Module.CastFinishAt(source.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Foxshot) + Casters.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Foxshot) + Casters.Remove(caster); + } +} +class Whirlwind(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.Whirlwind).Where(x => !x.IsDead)); +class WarDance(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WarDance), new AOEShapeCircle(5)); +class CharmingChasse(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.CharmingChasse)); +class HannishFire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HannishFire1), 6); +class HannishWaters(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HannishWaters), new AOEShapeCone(40, 15.Degrees())); +class RanaasFinish(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RanaasFinish), new AOEShapeCircle(15)); + +class RanaaMihgoStates : StateMachineBuilder +{ + public RanaaMihgoStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 670, NameID = 8489)] +public class RanaaMihgo(WorldState ws, Actor primary) : BossModule(ws, primary, new(520.47f, 124.99f), WeirdBounds) +{ + public static readonly ArenaBoundsCustom WeirdBounds = new(17.5f, new(CurveApprox.Ellipse(17.5f, 16f, 0.01f))); +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/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/VowsOfVitrueDeedsOfCruelty.cs b/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs index 7212b5d791..180476c5a6 100644 --- a/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs +++ b/BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs @@ -1,4 +1,10 @@ +<<<<<<<< HEAD:BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs namespace BossMod.Shadowbringers.Quest.MSQ.VowsOfVitrueDeedsOfCruelty; +======== +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.VowsOfVirtueDeedsOfCruelty; +>>>>>>>> merge:BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs public enum OID : uint { @@ -73,9 +79,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 +122,28 @@ 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)] +<<<<<<<< HEAD:BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs public class VowsOfVirtueDeedsOfCruelty(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(19.5f)); +======== +public class ArchUltima(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(20)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = (OID)h.Actor.OID switch + { + OID.MagitekBit => 2, + OID.LembusPraetorianus => 1, + _ => 0 + }; + } + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} +>>>>>>>> merge:BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs diff --git a/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs b/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs new file mode 100644 index 0000000000..f8bbffa5d0 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/NyelbertsLament.cs @@ -0,0 +1,116 @@ +using BossMod.QuestBattle.Shadowbringers.RoleQuests; + +namespace BossMod.Shadowbringers.Quest.NyelbertsLament; + +public enum OID : uint +{ + Boss = 0x2977, + Helper = 0x233C, + BovianBull = 0x2976, + LooseBoulder = 0x2978, // R2.400, x0 (spawn during fight) +} + +public enum AID : uint +{ + FallingRock = 16595, // Helper->location, 3.0s cast, range 4 circle + ZoomTargetSelect = 16599, // Helper->player, no cast, single-target + ZoomIn = 16598, // Helper->self, no cast, range 42 width 8 rect + TwoThousandMinaSlash = 16601, // Bovian->self/player, 5.0s cast, range 40 ?-degree cone +} + +public enum SID : uint +{ + WingedShield = 1900 +} + +class TwoThousandMinaSlash : Components.GenericLineOfSightAOE +{ + private readonly List _casters = []; + + public TwoThousandMinaSlash(BossModule module) : base(module, ActionID.MakeSpell(AID.TwoThousandMinaSlash), 40, false) + { + Refresh(); + } + + public Actor? ActiveCaster => _casters.MinBy(c => c.CastInfo!.RemainingTime); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + { + _casters.Add(caster); + Refresh(); + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + { + _casters.Remove(caster); + Refresh(); + } + } + + private void Refresh() + { + var blockers = Module.Enemies(OID.LooseBoulder); + + Modify(ActiveCaster?.CastInfo?.LocXZ, blockers.Select(b => (b.Position, b.HitboxRadius)), Module.CastFinishAt(ActiveCaster?.CastInfo)); + } +} + +class FallingRock(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FallingRock), 4); +class ZoomIn(BossModule module) : Components.SimpleLineStack(module, 4, 42, ActionID.MakeSpell(AID.ZoomTargetSelect), ActionID.MakeSpell(AID.ZoomIn), 5.1f) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Source != null) + hints.AddForbiddenZone(new AOEShapeDonut(3, 100), Arena.Center, default, Activation); + } +} + +class PassageOfArms(BossModule module) : BossComponent(module) +{ + private ActorCastInfo? EnrageCast => Module.PrimaryActor.CastInfo is { Action.ID: 16604 } castInfo ? castInfo : null; + private Actor? Paladin => WorldState.Actors.FirstOrDefault(x => x.FindStatus(SID.WingedShield) != null); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (EnrageCast != null && Paladin != null) + hints.AddForbiddenZone(ShapeDistance.InvertedCone(Paladin.Position, 8, Paladin.Rotation + 180.Degrees(), 60.Degrees()), Module.CastFinishAt(EnrageCast)); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (EnrageCast != null && Paladin != null) + Arena.ZoneCone(Paladin.Position, 0, 8, Paladin.Rotation + 180.Degrees(), 60.Degrees(), ArenaColor.SafeFromAOE); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (EnrageCast != null && Paladin != null && !actor.Position.InCircleCone(Paladin.Position, 8, Paladin.Rotation + 180.Degrees(), 60.Degrees())) + hints.Add("Hide behind tank!"); + } +} + +class NyelbertAI(BossModule module) : 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), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs b/BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs new file mode 100644 index 0000000000..b69b7bc9cd --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/SaveTheLastDanceForMe.cs @@ -0,0 +1,84 @@ +namespace BossMod.Shadowbringers.Quest.SaveTheLastDanceForMe; + +public enum OID : uint +{ + Boss = 0x2AC7, // R2.400, x1 + ShadowySpume = 0x2AC8, // R0.800, x0 (spawn during fight) + ForebodingAura = 0x2ACB, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + Dread = 17476, // Boss->location, 3.0s cast, range 5 circle + Anguish = 17487, // ->2ACD, 5.5s cast, range 6 circle + WhelmingLossFirst = 17480, // AethericShadow->self, 5.0s cast, range 5 circle + WhelmingLossRest = 17481, // AethericShadow1->self, no cast, range 5 circle + BitterLove = 15650, // 2AC9->self, 3.0s cast, range 12 120-degree cone +} + +class Dread(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Dread), 5); +class BitterLove(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BitterLove), new AOEShapeCone(12, 60.Degrees())); +class WhelmingLoss(BossModule module) : Components.Exaflare(module, new AOEShapeCircle(5)) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.WhelmingLossFirst) + Lines.Add(new Line + { + Next = caster.Position, + Advance = caster.Rotation.ToDirection() * 5, + NextExplosion = Module.CastFinishAt(spell), + TimeToMove = 1, + ExplosionsLeft = 7, + MaxShownExplosions = 3 + }); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID is (uint)AID.WhelmingLossFirst or (uint)AID.WhelmingLossRest) + { + var index = Lines.FindIndex(l => l.Next.AlmostEqual(caster.Position, 1)); + if (index == -1) + { + ReportError($"Failed to find entry for {caster.InstanceID:X}"); + return; + } + + AdvanceLine(Lines[index], caster.Position); + } + } +} +class Adds(BossModule module) : Components.Adds(module, (uint)OID.ShadowySpume); +class Anguish(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.Anguish), 6); +class ForebodingAura(BossModule module) : Components.GenericAOEs(module) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Module.Enemies(OID.ForebodingAura).Where(e => !e.IsDead).Select(e => new AOEInstance(new AOEShapeCircle(8), e.Position)); +} + +class AethericShadowStates : StateMachineBuilder +{ + public AethericShadowStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68790, NameID = 8493)] +public class AethericShadow(WorldState ws, Actor primary) : BossModule(ws, primary, new(73.6f, -743.6f), new ArenaBoundsCircle(20)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.FindStatus(DNC.SID.ClosedPosition) == null && Raid.WithoutSlot().Exclude(actor).FirstOrDefault() is Actor partner) + { + hints.ActionsToExecute.Push(ActionID.MakeSpell(DNC.AID.ClosedPosition), partner, ActionQueue.Priority.VeryHigh); + } + } +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs new file mode 100644 index 0000000000..9395f1677e --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P1GuidanceSystem.cs @@ -0,0 +1,38 @@ +using BossMod.QuestBattle.Shadowbringers.SideQuests; + +namespace BossMod.Shadowbringers.Quest.SleepNowInSapphire.P1GuidanceSystem; + +public enum OID : uint +{ + Boss = 0x2DFF, + Helper = 0x233C, +} + +public enum AID : uint +{ + AerialBombardment = 21492, // 233C->location, 2.5s cast, range 12 circle +} + +class AerialBombardment(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AerialBombardment), 12); + +class GWarrior(BossModule module) : 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, 1)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.FindStatus(Roleplay.SID.PyreticBooster) == null) + hints.ActionsToExecute.Push(ActionID.MakeSpell(Roleplay.AID.PyreticBooster), actor, ActionQueue.Priority.Medium); + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs new file mode 100644 index 0000000000..a0ec6bca18 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/SleepNowInSapphire/P2SapphireWeapon.cs @@ -0,0 +1,86 @@ +using BossMod.Shadowbringers.Quest.SleepNowInSapphire.P1GuidanceSystem; + +namespace BossMod.Shadowbringers.Quest.SleepNowInSapphire.P2SapphireWeapon; + +public enum OID : uint +{ + Boss = 0x2DFA, + Helper = 0x233C, +} + +public enum AID : uint +{ + TailSwing = 20326, // Boss->self, 4.0s cast, range 46 circle + OptimizedJudgment = 20325, // Boss->self, 4.0s cast, range -60 donut + MagitekSpread = 20336, // RegulasImage->self, 5.0s cast, range 43 ?-degree cone + SideraysRight = 20329, // Helper->self, 8.0s cast, range 128 ?-degree cone + SideraysLeft = 21021, // Helper->self, 8.0s cast, range 128 ?-degree cone + SapphireRay = 20327, // Boss->self, 8.0s cast, range 120 width 40 rect + MagitekRay = 20332, // 2DFC->self, 3.0s cast, range 100 width 6 rect + ServantRoar = 20339, // 2DFD->self, 2.5s cast, range 100 width 8 rect +} + +public enum SID : uint +{ + Invincibility = 775, // none->Boss, extra=0x0 +} + +class MagitekRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRay), new AOEShapeRect(100, 3)); +class ServantRoar(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ServantRoar), new AOEShapeRect(100, 4)); +class TailSwing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TailSwing), new AOEShapeCircle(46)); +class OptimizedJudgment(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.OptimizedJudgment), new AOEShapeDonut(21, 60)); +class MagitekSpread(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekSpread), new AOEShapeCone(43, 120.Degrees())); +class SapphireRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SapphireRay), new AOEShapeRect(120, 20)); +class Siderays(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor, WPos)> Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Casters.Select(c => new AOEInstance(new AOEShapeCone(128, 45.Degrees()), c.Item2, c.Item1.CastInfo!.Rotation, Module.CastFinishAt(c.Item1.CastInfo))); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.SideraysLeft: + Casters.Add((caster, caster.Position + caster.Rotation.ToDirection().OrthoL() * 15)); + break; + case AID.SideraysRight: + Casters.Add((caster, caster.Position + caster.Rotation.ToDirection().OrthoR() * 15)); + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + Casters.RemoveAll(c => c.Item1 == caster); + } +} + +class TheSapphireWeaponStates : StateMachineBuilder +{ + public TheSapphireWeaponStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69431, NameID = 9458)] +public class TheSapphireWeapon(WorldState ws, Actor primary) : BossModule(ws, primary, new(-15, 610), new ArenaBoundsSquare(60, 1)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs b/BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs new file mode 100644 index 0000000000..dfd2b3530e --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/SteelAgainstSteel.cs @@ -0,0 +1,128 @@ +namespace BossMod.Shadowbringers.Quest.SteelAgainstSteel; + +public enum OID : uint +{ + Boss = 0x2A45, + Helper = 0x233C, + Fustuarium = 0x2AD8, // R0.500, x1 (spawn during fight) + CullingBlade = 0x2AD3, // R0.500, x0 (spawn during fight) + IndustrialForce = 0x2BCE, // R0.500, x0 (spawn during fight) + TerminusEst = 0x2A46, // R1.000, x0 (spawn during fight) + CaptiveBolt = 0x2AD7, // R0.500, x0 (spawn during fight) +} + +public enum AID : uint +{ + CullingBlade1 = 17553, // CullingBlade->self, 3.5s cast, range 60 30-degree cone + TheOrder = 17568, // Boss->self, 4.0s cast, single-target + TerminusEst1 = 17567, // TerminusEst->self, no cast, range 40+R width 4 rect + CaptiveBolt = 17561, // CaptiveBolt->self, 7.0s cast, range 50+R width 10 rect + AetherochemicalGrenado = 17575, // 2A47->location, 4.0s cast, range 8 circle + Exsanguination = 17565, // 2AD6->self, 5.0s cast, range -17 donut + Exsanguination1 = 17564, // 2AD5->self, 5.0s cast, range -12 donut + Exsanguination2 = 17563, // 2AD4->self, 5.0s cast, range -7 donut + DiffractiveLaser = 17574, // 2A48->self, 3.0s cast, range 45+R width 4 rect + SnakeShot = 17569, // Boss->self, 4.0s cast, range 20 240-degree cone + ScaldingTank1 = 17558, // Fustuarium->2A4A, 6.0s cast, range 6 circle + ToTheSlaughter = 17559, // Boss->self, 4.0s cast, range 40 180-degree cone +} + +class ScaldingTank(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ScaldingTank1), 6); +class ToTheSlaughter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ToTheSlaughter), new AOEShapeCone(40, 90.Degrees())); +class Exsanguination(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor Actor, float Inner)> Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Casters.Select(c => new AOEInstance(new AOEShapeDonutSector(c.Inner, c.Inner + 5, 90.Degrees()), c.Actor.CastInfo!.LocXZ, c.Actor.Rotation, Module.CastFinishAt(c.Actor.CastInfo))); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + var radius = (AID)spell.Action.ID switch + { + AID.Exsanguination => 12, + AID.Exsanguination1 => 7, + AID.Exsanguination2 => 2, + _ => 0 + }; + + if (radius > 0) + Casters.Add((caster, radius)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.Exsanguination or AID.Exsanguination1 or AID.Exsanguination2) + Casters.RemoveAll(c => c.Actor == caster); + } +} +class CaptiveBolt(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CaptiveBolt), new AOEShapeRect(50, 5), maxCasts: 4); +class AetherochemicalGrenado(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.AetherochemicalGrenado), 8); +class DiffractiveLaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), new AOEShapeRect(45, 2)); +class SnakeShot(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SnakeShot), new AOEShapeCone(20, 120.Degrees())); +class CullingBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CullingBlade1), new AOEShapeCone(60, 15.Degrees())) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + + // zone rasterization can end up missing the arena center since it only contains the tips of a bunch of very pointy triangles + if (Casters.FirstOrDefault() is Actor c) + hints.AddForbiddenZone(ShapeDistance.Circle(c.Position, 0.5f), Module.CastFinishAt(c.CastInfo)); + } +} +class TerminusEst(BossModule module) : Components.GenericAOEs(module) +{ + private Actor? Caster; + private readonly List Actors = []; + + public override void OnActorCreated(Actor actor) + { + if (actor.OID == (uint)OID.TerminusEst) + Actors.Add(actor); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Caster is Actor c) + foreach (var t in Actors) + yield return new AOEInstance(new AOEShapeRect(40, 2), t.Position, t.Rotation, Module.CastFinishAt(c.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + // check if we already have terminuses out, because he can use this spell for a diff mechanic + if (spell.Action.ID == (uint)AID.TheOrder && Actors.Count > 0) + Caster = caster; + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.TerminusEst1) + { + Actors.Remove(caster); + // reset for next iteration + if (Actors.Count == 0) + Caster = null; + } + } +} + +class VitusQuoMessallaStates : StateMachineBuilder +{ + public VitusQuoMessallaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68802, NameID = 8872)] +public class VitusQuoMessalla(WorldState ws, Actor primary) : BossModule(ws, primary, new(-266, -507), new ArenaBoundsCircle(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs b/BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs new file mode 100644 index 0000000000..44d334849b --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheGreatShipVylbrand.cs @@ -0,0 +1,116 @@ +namespace BossMod.Shadowbringers.Quest.TheGreatShipVylbrand; + +public enum OID : uint +{ + Boss = 0x3107 +} + +public enum AID : uint +{ + W10TrolleyWallop = 22950, // 3104->self, 6.0s cast, range 40 60-degree cone + W10TrolleyTap = 23362, // 3104->self, 3.5s cast, range 8 120-degree cone + W10TrolleyTorque = 22949, // 3104->self, 6.0s cast, range 16 circle + Bulldoze = 22955, // 3107->location, 8.0s cast, width 6 rect charge + Bulldoze1 = 22957, // 233C->location, 8.0s cast, width 6 rect charge + TunnelShaker1 = 22959, // 233C->self, 5.0s cast, range 60 30-degree cone + Uplift = 22961, // 233C->self, 6.0s cast, range 10 circle + Uplift1 = 22962, // 233C->self, 8.0s cast, range 10-20 donut + Uplift2 = 22963, // 233C->self, 10.0s cast, range 20-30 donut +} + +class Torque(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W10TrolleyTorque), new AOEShapeCircle(16)); +class Tap(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W10TrolleyTap), new AOEShapeCone(8, 60.Degrees())); +class Wallop(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W10TrolleyWallop), new AOEShapeCone(40, 30.Degrees())); +class Bulldoze(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.Bulldoze), 3); +class Bulldoze2(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.Bulldoze1), 3); +class TunnelShaker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TunnelShaker1), new AOEShapeCone(60, 15.Degrees())); +class Uplift(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeCircle(10), new AOEShapeDonut(10, 20), new AOEShapeDonut(20, 30)]) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Uplift) + { + AddSequence(caster.Position, Module.CastFinishAt(spell)); + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + var order = (AID)spell.Action.ID switch + { + AID.Uplift => 0, + AID.Uplift1 => 1, + AID.Uplift2 => 2, + _ => -1 + }; + if (!AdvanceSequence(order, caster.Position, WorldState.FutureTime(2))) + ReportError($"unexpected order {order}"); + } +} + +class BombTether : Components.BaitAwayTethers +{ + private DateTime? Activation; + + public BombTether(BossModule module) : base(module, new AOEShapeCircle(6), 97) + { + CenterAtTarget = true; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Activation != null) + hints.AddForbiddenZone(new AOEShapeDonut(1.5f, 100), new(9.15f, -8.44f), activation: Activation.Value); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (CurrentBaits.Count > 0) + hints.Add("Intercept tether!", CurrentBaits.Any(b => b.Target != actor)); + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + base.OnTethered(source, tether); + if (tether.ID == TID) + Activation = WorldState.FutureTime(15); + } + + public override void OnUntethered(Actor source, ActorTetherInfo tether) + { + base.OnUntethered(source, tether); + if (tether.ID == TID) + Activation = null; + } +} + +public class SecondOrderRocksplitterStates : StateMachineBuilder +{ + public SecondOrderRocksplitterStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.WorldState.CurrentCFCID != 764; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69551)] +public class SecondOrderRocksplitter(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsCircle(27)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + hints.InteractWithTarget = Enemies(0x1EB0F7).FirstOrDefault(x => x.IsTargetable); + + foreach (var e in hints.PotentialTargets) + if (e.Actor.OID == 0x3106) + e.Priority = AIHints.Enemy.PriorityPointless; + } +} diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs b/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs new file mode 100644 index 0000000000..9c23a90128 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheHardenedHeart.cs @@ -0,0 +1,143 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.TheHardenedHeart; + +public enum OID : uint +{ + Boss = 0x2919, + Helper = 0x233C, +} + +public enum AID : uint +{ + SanctifiedFireIII = 18090, // 2922/2923->players/2917/2915/2914, 8.0s cast, range 6 circle + TwistedTalent1 = 13637, // Helper->player/2916/2914/2915/2917, 5.0s cast, range 5 circle + AbyssalCharge1 = 15539, // 25BB->self, 3.0s cast, range 40+R width 4 rect + DeadlyBite = 15543, // 291D/291C->player/2914, no cast, single-target + RustingClaw = 15540, // 291B/291A->self, 5.0s cast, range 8+R ?-degree cone +} + +class SanctifiedFireIII(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.SanctifiedFireIII), 6) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == StackAction && WorldState.Actors.Find(spell.TargetID) is Actor stackTarget && stackTarget.OID == 0x2915) + AddStack(stackTarget, Module.CastFinishAt(spell)); + } +} + +class TwistedTalent(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.TwistedTalent1), 5); +class AbyssalCharge(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AbyssalCharge1), new AOEShapeRect(40, 2)); + +class AutoBranden(WorldState ws) : UnmanagedRotation(ws, 3) +{ + protected override void Exec(Actor? primaryTarget) + { + if (primaryTarget == null) + return; + + var numAOETargets = Hints.PotentialTargets.Count(x => x.Actor.Position.InCircle(Player.Position, 5)); + + if (numAOETargets > 1) + { + if (ComboAction == Roleplay.AID.Sunshadow) + UseAction(Roleplay.AID.GreatestEclipse, Player); + + UseAction(Roleplay.AID.Sunshadow, Player); + } + + if (Player.HPMP.CurHP * 3 < Player.HPMP.MaxHP) + UseAction(Roleplay.AID.ChivalrousSpirit, Player); + + var gcd = ComboAction switch + { + Roleplay.AID.RightfulSword => Roleplay.AID.Swashbuckler, + Roleplay.AID.FastBlade => Roleplay.AID.RightfulSword, + _ => Roleplay.AID.FastBlade + }; + + UseAction(gcd, primaryTarget); + if (Player.DistanceToHitbox(primaryTarget) <= 3) + UseAction(Roleplay.AID.FightOrFlight, Player, -10); + + if (primaryTarget.CastInfo?.Interruptible ?? false) + UseAction(Roleplay.AID.Interject, primaryTarget, -10); + } +} + +class TankbusterTether(BossModule module) : BossComponent(module) +{ + private record class Tether(Actor Source, Actor Target, DateTime Activation); + private Tether? DwarfTether; + + private bool Danger => DwarfTether?.Target.OID == 0x2917; + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == 84 && WorldState.Actors.Find(tether.Target) is Actor target) + DwarfTether = new(source, target, DwarfTether?.Activation ?? WorldState.FutureTime(10)); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (DwarfTether?.Target.OID == 0x2917) + hints.AddForbiddenZone(ShapeDistance.InvertedRect(DwarfTether.Source.Position, DwarfTether.Target.Position, 1), DwarfTether.Activation); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Danger) + hints.Add("Take tether!"); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (DwarfTether is Tether t) + Arena.AddLine(t.Source.Position, t.Target.Position, ArenaColor.Danger); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.DeadlyBite) + DwarfTether = null; + } +} + +class BrandenAI(BossModule module) : RotationModule(module); + +class RustingClaw(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RustingClaw), new AOEShapeCone(10.3f, 45.Degrees())); + +class TadricTheVaingloriousStates : StateMachineBuilder +{ + public TadricTheVaingloriousStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68783, NameID = 8339)] +public class TadricTheVainglorious(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsSquare(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + { + h.Priority = h.Actor.FindStatus(775) == null ? (h.Actor.TargetID == actor.InstanceID ? 2 : 1) : 0; + if (h.Actor.OID is not (0x291D or 0x2919) && h.Actor.CastInfo == null) + { + h.DesiredPosition = Arena.Center; + if (h.Actor.TargetID == actor.InstanceID && !h.Actor.Position.InCircle(Arena.Center, 5)) + hints.ForcedTarget = h.Actor; + } + } + } +} + diff --git a/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs b/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs new file mode 100644 index 0000000000..4aa94e49a9 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheHuntersLegacy.cs @@ -0,0 +1,92 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.TheHuntersLegacy; + +public enum OID : uint +{ + Boss = 0x29EE, + Helper = 0x233C +} + +public enum AID : uint +{ + BalamBlaster = 17137, // Boss->self, 4.5s cast, range 30+R 270-degree cone + BalamBlasterRear = 17138, // Boss->self, 4.5s cast, range 30+R 270-degree cone + ElectricWhisker = 17126, // Boss->self, 3.5s cast, range 8+R 90-degree cone + RoaringThunder = 17135, // Boss->self, 4.0s cast, range 8-30 donut + StreakLightning = 17148, // 233C->location, 2.5s cast, range 3 circle + AlternatingCurrent1 = 17150, // Helper->self, 4.0s cast, range 60 width 5 rect + RumblingThunderStack = 17134, // Helper->player, 6.0s cast, range 5 circle + Thunderbolt1 = 17140, // Helper->players/29EC, 6.0s cast, range 5 circle + StreakLightning1 = 17147, // Helper->location, 2.5s cast, range 3 circle +} + +class Thunderbolt(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Thunderbolt1), 5); +class BalamBlaster(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BalamBlaster), new AOEShapeCone(38.05f, 135.Degrees())); +class BalamBlasterRear(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BalamBlasterRear), new AOEShapeCone(38.05f, 135.Degrees())); +class ElectricWhisker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ElectricWhisker), new AOEShapeCone(16.05f, 45.Degrees())); +class RoaringThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RoaringThunder), new AOEShapeDonut(8, 30)); +class StreakLightning(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.StreakLightning), 3); +class StreakLightning1(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.StreakLightning1), 3); +class AlternatingCurrent(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AlternatingCurrent1), new AOEShapeRect(60, 2.5f)); +class RumblingThunder(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.RumblingThunderStack), 5, 1); + +class RendaRae(WorldState ws) : UnmanagedRotation(ws, 20) +{ + protected override void Exec(Actor? primaryTarget) + { + var dot = StatusDetails(primaryTarget, Roleplay.SID.AcidicBite, Player.InstanceID); + if (dot.Left < 2.5f) + UseAction(Roleplay.AID.AcidicBite, primaryTarget, 10); + + UseAction(Roleplay.AID.RadiantArrow, primaryTarget, -5); + UseAction(Roleplay.AID.HeavyShot, primaryTarget); + + if (primaryTarget?.CastInfo?.Interruptible ?? false) + UseAction(Roleplay.AID.DullingArrow, primaryTarget, 5); + + if (Player.HPMP.MaxHP * 0.8f > Player.HPMP.CurHP) + UseAction(Roleplay.AID.HuntersPrudence, Player, -15); + } +} + +class RendaRaeAI(BossModule module) : RotationModule(module); + +class RonkanAura(BossModule module) : BossComponent(module) +{ + private Actor? AuraCenter => Module.Enemies(0x1EADA5).FirstOrDefault(); + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (AuraCenter is Actor a) + Arena.ZoneCircle(a.Position, 10, ArenaColor.SafeFromAOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (AuraCenter is Actor a) + hints.AddForbiddenZone(new AOEShapeDonut(10, 100), a.Position, activation: WorldState.FutureTime(5)); + } +} + +class BalamQuitzStates : StateMachineBuilder +{ + public BalamQuitzStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68812, NameID = 8397)] +public class BalamQuitz(WorldState ws, Actor primary) : BossModule(ws, primary, new(-247.11f, 688.33f), new ArenaBoundsCircle(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs new file mode 100644 index 0000000000..f6a259691a --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Sophrosyne.cs @@ -0,0 +1,29 @@ +namespace BossMod.Shadowbringers.Quest.TheLostAndTheFound.Sophrosyne; + +public enum OID : uint +{ + Boss = 0x29AA, + Helper = 0x233C, +} + +public enum AID : uint +{ + Charge = 16999, // 29AB->29A9, 3.0s cast, width 4 rect charge +} + +class Charge(BossModule module) : Components.ChargeAOEs(module, ActionID.MakeSpell(AID.Charge), 2); + +class SophrosyneStates : StateMachineBuilder +{ + public SophrosyneStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68806, NameID = 8395)] +public class Sophrosyne(WorldState ws, Actor primary) : BossModule(ws, primary, new(632, 64.15f), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs new file mode 100644 index 0000000000..d6806f5362 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheLostAndTheFound/Yxtlilton.cs @@ -0,0 +1,87 @@ +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.TheLostAndTheFound.Yxtlilton; + +public enum OID : uint +{ + Boss = 0x29B0, + Helper = 0x233C, +} + +public enum AID : uint +{ + TheCodexOfDarknessII = 17010, // Boss->self, 3.0s cast, range 100 circle + TheCodexOfGravity = 17014, // Boss->player, 4.5s cast, range 6 circle +} + +class CodexOfDarknessII(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.TheCodexOfDarknessII)); +class CodexOfGravity(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.TheCodexOfGravity), 6) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + if (Stacks.Count > 0) + hints.AddForbiddenZone(new AOEShapeDonut(1.5f, 100), Arena.Center, default, Stacks[0].Activation); + } +} + +class LamittAI(WorldState ws) : UnmanagedRotation(ws, 25) +{ + protected override void Exec(Actor? primaryTarget) + { + if (primaryTarget == null) + return; + + var party = World.Party.WithoutSlot().ToList(); + + Hints.GoalZones.Add(p => party.Count(act => act.Position.InCircle(p, 15 + Player.HitboxRadius + act.HitboxRadius))); + + var lowest = party.MinBy(p => p.PredictedHPRatio)!; + var esunable = party.FirstOrDefault(x => x.FindStatus(482) != null); + var doomed = party.FirstOrDefault(x => x.FindStatus(1769) != null); + var partyHealth = party.Average(p => p.PredictedHPRatio); + + // pre heal during doom cast since it does insane damage for some reason + if (primaryTarget.CastInfo is { Action.ID: 17011 } ci && ci.TargetID == Player.InstanceID) + { + if (Player.PredictedHPRatio <= 0.8f) + UseAction(Roleplay.AID.RonkanCureII, Player); + } + + if (partyHealth < 0.6f) + UseAction(Roleplay.AID.RonkanMedica, Player); + + if (lowest.HPMP.CurHP * 3 <= lowest.HPMP.MaxHP) + UseAction(Roleplay.AID.RonkanCureII, lowest); + + if (esunable != null) + UseAction(Roleplay.AID.RonkanEsuna, esunable); + + if (doomed != null) + { + UseAction(Roleplay.AID.RonkanRenew, doomed); + UseAction(Roleplay.AID.RonkanCureII, doomed); + } + + UseAction(Roleplay.AID.RonkanStoneII, primaryTarget); + } +} + +class AutoLamitt(BossModule module) : RotationModule(module); + +class YxtliltonStates : StateMachineBuilder +{ + public YxtliltonStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68806, NameID = 8393)] +public class Yxtlilton(WorldState ws, Actor primary) : BossModule(ws, primary, new(-120, -770), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs b/BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs new file mode 100644 index 0000000000..af4008c356 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheOracleOfLight.cs @@ -0,0 +1,40 @@ +namespace BossMod.Shadowbringers.Quest.TheOracleOfLight; + +public enum OID : uint +{ + Boss = 0x299D, + Helper = 0x233C, +} + +public enum AID : uint +{ + HotPursuit1 = 17622, // 2AF0->location, 3.0s cast, range 5 circle + NexusOfThunder1 = 17621, // 2AF0->self, 7.0s cast, range 60+R width 5 rect + NexusOfThunder2 = 17823, // 2AF0->self, 8.5s cast, range 60+R width 5 rect + Burn = 18035, // 2BE6->self, 4.5s cast, range 8 circle + UnbridledWrath = 18036, // 299E->self, 5.5s cast, range 90 width 90 rect +} + +class HotPursuit(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HotPursuit1), 5); +class NexusOfThunder1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder1), new AOEShapeRect(60, 2.5f)); +class NexusOfThunder2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NexusOfThunder2), new AOEShapeRect(60, 2.5f)); +class Burn(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Burn), new AOEShapeCircle(8), maxCasts: 8); +class UnbridledWrath(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.UnbridledWrath), 20, kind: Kind.DirForward, stopAtWall: true); + +class RanjitStates : StateMachineBuilder +{ + public RanjitStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68841, NameID = 8374)] +public class Ranjit(WorldState ws, Actor primary) : BossModule(ws, primary, new(126.75f, -311.25f), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs b/BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs new file mode 100644 index 0000000000..6de41c42a6 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/TheSoulOfTemperance.cs @@ -0,0 +1,76 @@ +namespace BossMod.Shadowbringers.Quest.TheSoulOfTemperance; + +public enum OID : uint +{ + Boss = 0x29CE, + BossP2 = 0x29D0, + Helper = 0x233C, +} + +public enum AID : uint +{ + SanctifiedAero1 = 16911, // 2A0C->self, 4.0s cast, range 40+R width 6 rect + SanctifiedStone = 17322, // 29D0->self, 5.0s cast, single-target + HolyBlur = 17547, // 2969/29CF/274F/296A/2996->self, 5.0s cast, range 40 circle + Focus = 17548, // 29CF/296A/2996/2969->players, 5.0s cast, width 4 rect charge + TemperedVirtue = 15928, // BossP2->self, 6.0s cast, range 15 circle + WaterAndWine = 15604, // 2AF1->self, 5.0s cast, range 12 circle + ForceOfRestraint = 15603, // 2AF1->self, 5.0s cast, range 60+R width 4 rect + SanctifiedHoly1 = 16909, // BossP2->self, 4.0s cast, range 8 circle + SanctifiedHoly2 = 17604, // 2A0C->location, 4.0s cast, range 6 circle +} + +class SanctifiedHoly1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedHoly1), new AOEShapeCircle(8)); +class SanctifiedHoly2(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedHoly2), 6); +class ForceOfRestraint(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ForceOfRestraint), new AOEShapeRect(60, 2)); +class HolyBlur(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.HolyBlur)); +class Focus(BossModule module) : Components.BaitAwayChargeCast(module, ActionID.MakeSpell(AID.Focus), 2); +class TemperedVirtue(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TemperedVirtue), new AOEShapeCircle(15)); +class WaterAndWine(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WaterAndWine), new AOEShapeDonut(6, 12)); +class SanctifiedStone(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.SanctifiedStone), 5, 1); + +class SanctifiedAero(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedAero1), new AOEShapeRect(40.5f, 3)); + +class Repose(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + static bool SleepProof(Actor a) + { + if (a.Statuses.Any(x => x.ID is 1967 or 1968)) + return true; + + return a.PendingStatuses.Any(s => s.StatusId == 3); + } + + if (WorldState.Actors.FirstOrDefault(x => x.IsTargetable && !x.IsAlly && x.OID != (uint)OID.Boss && !SleepProof(x)) is Actor e) + hints.ActionsToExecute.Push(ActionID.MakeSpell(WHM.AID.Repose), e, ActionQueue.Priority.VeryHigh); + } +} + +class SophrosyneStates : StateMachineBuilder +{ + public SophrosyneStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.Enemies(OID.BossP2).Any(x => x.IsTargetable) || module.WorldState.CurrentCFCID != 673; + TrivialPhase(1) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.Enemies(OID.BossP2).All(x => x.IsDeadOrDestroyed) || module.WorldState.CurrentCFCID != 673; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68808, NameID = 8777)] +public class Sophrosyne(WorldState ws, Actor primary) : BossModule(ws, primary, new(-651.8f, -127.25f), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs b/BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs new file mode 100644 index 0000000000..d332164bac --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/ToHaveLovedAndLost.cs @@ -0,0 +1,79 @@ +namespace BossMod.Shadowbringers.Quest.ToHaveLovedAndLost; + +public enum OID : uint +{ + Boss = 0x2927, + Helper = 0x233C, +} + +public enum AID : uint +{ + Bloodstain = 4747, // Boss->self, 2.5s cast, range 5 circle + BrandOfSin = 16132, // Boss->self, 3.0s cast, range 80 circle + BladeOfJustice = 16134, // Boss->players, 8.0s cast, range 6 circle + SanctifiedHolyII = 17427, // Boss->self, 3.0s cast, range 5 circle + SanctifiedHolyIII = 17430, // 2AB3/2AB2->location, 3.0s cast, range 6 circle + HereticsFork = 17552, // 2779->self, 4.0s cast, range 40 width 6 cross + SpiritsWithout = 4746, // Boss->self, 2.5s cast, range 3+R width 3 rect + SeraphBlade = 16131, // Boss->self, 5.0s cast, range 40+R ?-degree cone + Fracture = 15576, // 2612->location, 5.0s cast, range 3 circle + Fracture1 = 13208, // 2612->location, 5.0s cast, range 3 circle + Fracture2 = 13207, // 2612->location, 5.0s cast, range 3 circle + Fracture3 = 15374, // 2612->location, 5.0s cast, range 3 circle + Fracture4 = 16612, // 2612->location, 5.0s cast, range 3 circle + Fracture5 = 13209, // 2612->location, 5.0s cast, range 3 circle + HereticsQuoit = 17470, // 2968->self, 5.0s cast, range -15 donut + SanctifiedHoly1 = 17431, // 2AB3/2AB2->players/2928, 5.0s cast, range 6 circle +} + +class HereticsFork(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HereticsFork), new AOEShapeCross(40, 3)); +class SpiritsWithout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SpiritsWithout), new AOEShapeRect(3.5f, 1.5f)); +class SeraphBlade(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SeraphBlade), new AOEShapeCone(40, 90.Degrees())); +class HereticsQuoit(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HereticsQuoit), new AOEShapeDonut(5, 15)); +class SanctifiedHoly(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SanctifiedHoly1), 6); + +class Fracture(BossModule module) : Components.GenericTowers(module) +{ + private readonly AID[] TowerCasts = [AID.Fracture, AID.Fracture1, AID.Fracture2, AID.Fracture3, AID.Fracture4, AID.Fracture5]; + + private bool IsTower(ActionID act) => TowerCasts.Contains((AID)act.ID); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (IsTower(spell.Action)) + Towers.Add(new(spell.LocXZ, 3, activation: Module.CastFinishAt(spell))); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (IsTower(spell.Action)) + Towers.RemoveAll(t => t.Position.AlmostEqual(spell.LocXZ, 1)); + } +} +class Bloodstain(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Bloodstain), new AOEShapeCircle(5)); +class BrandOfSin(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.BrandOfSin), 10); +class BladeOfJustice(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.BladeOfJustice), 6, minStackSize: 1); +class SanctifiedHolyII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedHolyII), new AOEShapeCircle(5)); +class SanctifiedHolyIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SanctifiedHolyIII), 6); + +class DikaiosyneStates : StateMachineBuilder +{ + public DikaiosyneStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68784, NameID = 8922)] +public class Dikaiosyne(WorldState ws, Actor primary) : BossModule(ws, primary, new(-798.6f, -40.49f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs b/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs new file mode 100644 index 0000000000..180476c5a6 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs @@ -0,0 +1,149 @@ +<<<<<<<< HEAD:BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs +namespace BossMod.Shadowbringers.Quest.MSQ.VowsOfVitrueDeedsOfCruelty; +======== +using BossMod.QuestBattle; + +namespace BossMod.Shadowbringers.Quest.VowsOfVirtueDeedsOfCruelty; +>>>>>>>> merge:BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs + +public enum OID : uint +{ + Boss = 0x2C85, // R6.000, x1 + TerminusEstVisual = 0x2C98, // R1.000, x3 + SigniferPraetorianus = 0x2C9A, // R0.500, x0 (spawn during fight), the adds on the catwalk that just rain down Fire II + LembusPraetorianus = 0x2C99, // R2.400, x0 (spawn during fight), two large magitek ships + MagitekBit = 0x2C9C, // R0.600, x0 (spawn during fight) + BossHelper = 0x233C +} + +public enum AID : uint +{ + LoadData = 18786, // Boss->self, 3.0s cast, single-target + AutoAttack = 870, // Boss/LembusPraetorianus->player, no cast, single-target + MagitekRayRightArm = 18783, // Boss->self, 3.2s cast, range 45+R width 8 rect + MagitekRayLeftArm = 18784, // Boss->self, 3.2s cast, range 45+R width 8 rect + SystemError = 18785, // Boss->self, 1.0s cast, single-target + AngrySalamander = 18787, // Boss->self, 3.0s cast, range 40+R width 6 rect + FireII = 18959, // SigniferPraetorianus->location, 3.0s cast, range 5 circle + TerminusEstBossCast = 18788, // Boss->self, 3.0s cast, single-target + TerminusEstLocationHelper = 18889, // BossHelper->self, 4.0s cast, range 3 circle + TerminusEstVisual = 18789, // TerminusEstVisual->self, 1.0s cast, range 40+R width 4 rect + HorridRoar = 18779, // 2CC5->location, 2.0s cast, range 6 circle, this is your own attack. It spawns an aoe at the location of any enemy it initally hits + GarleanFire = 4007, // LembusPraetorianus->location, 3.0s cast, range 5 circle + MagitekBit = 18790, // Boss->self, no cast, single-target + MetalCutterCast = 18793, // Boss->self, 6.0s cast, single-target + MetalCutter = 18794, // BossHelper->self, 6.0s cast, range 30+R 20-degree cone + AtomicRayCast = 18795, // Boss->self, 6.0s cast, single-target + AtomicRay = 18796, // BossHelper->location, 6.0s cast, range 10 circle + MagitekRayBit = 18791, // MagitekBit->self, 6.0s cast, range 50+R width 2 rect + SelfDetonate = 18792, // MagitekBit->self, 7.0s cast, range 40+R circle, enrage if bits are not killed before cast +} + +abstract class MagitekRay(BossModule module, AID aid) : Components.SimpleAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(45, 4)); +class MagitekRayRightArm(BossModule module) : MagitekRay(module, AID.MagitekRayRightArm); +class MagitekRayLeftArm(BossModule module) : MagitekRay(module, AID.MagitekRayLeftArm); + +class AngrySalamander(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AngrySalamander), new AOEShapeRect(40, 3)); +class TerminusEstRects(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + private static readonly AOEShapeRect _shape = new(40, 2); + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TerminusEstLocationHelper) + { + _aoes.AddRange( + [ + new(_shape, spell.LocXZ, spell.Rotation, Module.CastFinishAt(spell)), + new(_shape, spell.LocXZ, spell.Rotation - 90.Degrees(), Module.CastFinishAt(spell)), + new(_shape, spell.LocXZ, spell.Rotation + 90.Degrees(), Module.CastFinishAt(spell)) + ]); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.TerminusEstVisual) + { + _aoes.Clear(); + ++NumCasts; + } + } +} +class TerminusEstCircle(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.TerminusEstLocationHelper), 3); +class FireII(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.FireII), 5); +class GarleanFire(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.GarleanFire), 5); +class MetalCutter(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MetalCutter), new AOEShapeCone(30, 10.Degrees())); +class MagitekRayBits(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MagitekRayBit), new AOEShapeRect(50, 1)); +class AtomicRay(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.AtomicRay), 10); +class SelfDetonate(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SelfDetonate), "Enrage if bits are not killed before cast"); + +class EstinienAI(WorldState ws) : UnmanagedRotation(ws, 3) +{ + protected override void Exec(Actor? primaryTarget) + { + if (primaryTarget == null) + return; + + if (Hints.PotentialTargets.Any(x => (OID)x.Actor.OID is OID.SigniferPraetorianus or OID.MagitekBit)) + UseAction(Roleplay.AID.HorridRoar, Player); + + if (World.Party.LimitBreakCur == 10000) + UseAction(Roleplay.AID.DragonshadowDive, primaryTarget, 100); + + if (primaryTarget.OID == (uint)OID.Boss) + { + var dotRemaining = StatusDetails(primaryTarget, Roleplay.SID.StabWound, Player.InstanceID).Left; + if (dotRemaining < 2.3f) + UseAction(Roleplay.AID.Drachenlance, primaryTarget); + } + + UseAction(Roleplay.AID.AlaMorn, primaryTarget); + UseAction(Roleplay.AID.Stardiver, primaryTarget, -10); + } +} + +class AutoEstinien(BossModule module) : RotationModule(module); + +class ArchUltimaStates : StateMachineBuilder +{ + public ArchUltimaStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "croizat", GroupType = BossModuleInfo.GroupType.Quest, GroupID = 69218, NameID = 9189)] +<<<<<<<< HEAD:BossMod/Modules/Shadowbringers/Quest/MSQ/VowsOfVitrueDeedsOfCruelty.cs +public class VowsOfVirtueDeedsOfCruelty(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(19.5f)); +======== +public class ArchUltima(WorldState ws, Actor primary) : BossModule(ws, primary, new(240, 230), new ArenaBoundsSquare(20)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = (OID)h.Actor.OID switch + { + OID.MagitekBit => 2, + OID.LembusPraetorianus => 1, + _ => 0 + }; + } + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} +>>>>>>>> merge:BossMod/Modules/Shadowbringers/Quest/VowsOfVirtueDeedsOfCruelty.cs diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs new file mode 100644 index 0000000000..3b69ab9773 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/Enums.cs @@ -0,0 +1,29 @@ +namespace BossMod.Stormblood.Quest.ARequiemForHeroes; + +public enum OID : uint +{ + BossP1 = 0x268A, + BossP2 = 0x268C, + Helper = 0x233C, + AmeNoHabakiri = 0x2692, // R3.000, x0 (spawn during fight) + TheStorm = 0x2760, // R3.000, x0 (spawn during fight) + TheSwell = 0x275F, // R3.000, x0 (spawn during fight) + DarkAether = 0x2694, // R1.200, x0 (spawn during fight) +} + +public enum AID : uint +{ + FloodOfDarkness = 14808, // Helper->self, 3.5s cast, range 6 circle + VeinSplitter = 14839, // Boss->self, 4.0s cast, range 10 circle + LightlessSpark = 14838, // Boss->self, 4.0s cast, range 40+R 90-degree cone + LightlessSparkAdds = 14824, // 268D->self, 4.0s cast, range 40+R 90-degree cone + ArtOfTheSwell = 14812, // Boss->self, 4.0s cast, range 33 circle + TheSwellUnbound = 14813, // Helper->self, 8.0s cast, range 8-20 donut + ArtOfTheSword1 = 14819, // Helper->self, 4.0s cast, range 40+R width 6 rect + ArtOfTheSword2 = 14818, // Helper->self, 6.0s cast, range 40+R width 6 rect + ArtOfTheSword3 = 14820, // Helper->self, 2.0s cast, range 40+R width 6 rect + ArtOfTheStorm = 14814, // Boss->self, 4.0s cast, range 8 circle + TheStormUnboundCast = 14815, // Helper->self, 3.0s cast, range 5 circle + TheStormUnboundRepeat = 14816, // Helper->self, no cast, range 5 circle + EntropicFlame = 14833, // Helper->self, 4.0s cast, range 50+R width 8 rect +} diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs new file mode 100644 index 0000000000..52ef8c972b --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P1.cs @@ -0,0 +1,51 @@ +using BossMod.QuestBattle; + +namespace BossMod.Stormblood.Quest.ARequiemForHeroes; + +class AutoHien(WorldState ws) : UnmanagedRotation(ws, 3) +{ + protected override void Exec(Actor? primaryTarget) + { + if (primaryTarget == null) + return; + + Hints.GoalZones.Add(Hints.GoalSingleTarget(primaryTarget, 3)); + + var ajisai = StatusDetails(primaryTarget, Roleplay.SID.Ajisai, Player.InstanceID); + + switch (ComboAction) + { + case Roleplay.AID.Gofu: + UseAction(Roleplay.AID.Yagetsu, primaryTarget); + break; + + case Roleplay.AID.Kyokufu: + UseAction(Roleplay.AID.Gofu, primaryTarget); + break; + + default: + if (ajisai.Left < 5) + UseAction(Roleplay.AID.Ajisai, primaryTarget); + UseAction(Roleplay.AID.Kyokufu, primaryTarget); + break; + } + + if (Player.HPMP.CurHP < 5000) + UseAction(Roleplay.AID.SecondWind, Player, -10); + + UseAction(Roleplay.AID.HissatsuGyoten, primaryTarget, -10); + } +} + +class HienAI(BossModule module) : RotationModule(module); + +public class ZenosP1States : StateMachineBuilder +{ + public ZenosP1States(BossModule module) : base(module) + { + TrivialPhase().ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68721, NameID = 6039, PrimaryActorOID = (uint)OID.BossP1)] +public class ZenosP1(WorldState ws, Actor primary) : BossModule(ws, primary, new(233, -93.25f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs new file mode 100644 index 0000000000..c42f8422a5 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ARequiemForHeroes/P2.cs @@ -0,0 +1,89 @@ +namespace BossMod.Stormblood.Quest.ARequiemForHeroes; + +class StormUnbound(BossModule module) : Components.Exaflare(module, 5) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TheStormUnboundCast) + { + Lines.Add(new() + { + Next = caster.Position, + Advance = caster.Rotation.ToDirection() * 5, + NextExplosion = Module.CastFinishAt(spell), + TimeToMove = 1, + ExplosionsLeft = 4, + MaxShownExplosions = 2 + }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.TheStormUnboundCast or AID.TheStormUnboundRepeat) + { + foreach (var l in Lines.Where(l => l.Next.AlmostEqual(caster.Position, 1))) + AdvanceLine(l, caster.Position); + ++NumCasts; + } + } +} + +class LightlessSpark2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LightlessSparkAdds), new AOEShapeCone(40, 45.Degrees())); + +class ArtOfTheStorm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheStorm), new AOEShapeCircle(8)); +class EntropicFlame(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EntropicFlame), new AOEShapeRect(50, 4)); + +class FloodOfDarkness(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FloodOfDarkness), new AOEShapeCircle(6), maxCasts: 6); +class VeinSplitter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), new AOEShapeCircle(10)); +class LightlessSpark(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LightlessSpark), new AOEShapeCone(40, 45.Degrees())); +class SwellUnbound(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TheSwellUnbound), new AOEShapeDonut(8, 20)); +class Swell(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.ArtOfTheSwell), 8) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Casters.Count > 0) + hints.AddForbiddenZone(new AOEShapeDonut(8, 50), Arena.Center); + } +} +class ArtOfTheSword1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword1), new AOEShapeRect(40, 3)); +class ArtOfTheSword2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword2), new AOEShapeRect(40, 3)); +class ArtOfTheSword3(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword3), new AOEShapeRect(40, 3)); + +class DarkAether(BossModule module) : Components.GenericAOEs(module) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Module.Enemies(OID.DarkAether).Select(e => new AOEInstance(new AOEShapeCircle(1.5f), e.Position, e.Rotation)); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var c in ActiveAOEs(slot, actor)) + hints.AddForbiddenZone(new AOEShapeRect(3, 1.5f, 1.5f), c.Origin, c.Rotation, c.Activation); + } +} + +class Adds(BossModule module) : Components.AddsMulti(module, [(uint)OID.TheStorm, (uint)OID.TheSwell, (uint)OID.AmeNoHabakiri]); + +public class ZenosP2States : StateMachineBuilder +{ + public ZenosP2States(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68721, NameID = 6039, PrimaryActorOID = (uint)OID.BossP2)] +public class ZenosP2(WorldState ws, Actor primary) : BossModule(ws, primary, new(233, -93.25f), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs b/BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs new file mode 100644 index 0000000000..a050938ed8 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/AnArtForTheLiving.cs @@ -0,0 +1,114 @@ +namespace BossMod.Stormblood.Quest.AnArtForTheLiving; + +public enum OID : uint +{ + Boss = 0x1CBA, + Helper = 0x233C, + ExplosiveIndicator = 0x1CD7, // R0.500, x0 (spawn during fight) + AetherochemicalExplosive = 0x1CD5, // R1.000, x1 (spawn during fight) +} + +public enum AID : uint +{ + PiercingLaser = 8683, // Boss->self, 3.0s cast, range 30+R width 6 rect + NerveGas = 8707, // 1CD8->self, 3.0s cast, range 30+R 120-degree cone + NerveGasLeft = 8708, // FX1979->self, 3.0s cast, range 30+R 180-degree cone + NerveGasRight = 8709, // 1CD8->self, 3.0s cast, range 30+R 180-degree cone + W111TonzeSwing = 8697, // 1CD1->self, 4.0s cast, range 8+R circle + W11TonzeSwipe = 8699, // 1CD1->self, 3.0s cast, range 5+R ?-degree cone +} + +public enum SID : uint +{ + Invincibility = 325 +} + +class OneOneOneTonzeSwing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W111TonzeSwing), new AOEShapeCircle(12)); +class OneOneTonzeSwipe(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.W11TonzeSwipe), new AOEShapeCone(9, 45.Degrees())); // may be the wrong angle + +class NerveGas1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NerveGas), new AOEShapeCone(35, 60.Degrees())); +class NerveGas2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NerveGasRight), new AOEShapeCone(35, 90.Degrees())); +class NerveGas3(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NerveGasLeft), new AOEShapeCone(35, 90.Degrees())); + +class PiercingLaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PiercingLaser), new AOEShapeRect(33.68f, 3)); + +class AetherochemicalExplosive(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor Actor, bool Primed, DateTime Activation)> Explosives = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Explosives.Where(e => !e.Actor.IsDead || !e.Primed).Select(e => new AOEInstance(new AOEShapeCircle(5), e.Actor.Position, Activation: e.Activation)); + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID is OID.ExplosiveIndicator) + { + Explosives.Add((actor, false, WorldState.CurrentTime.AddSeconds(3))); + } + + if ((OID)actor.OID is OID.AetherochemicalExplosive) + { + var slot = Explosives.FindIndex(e => e.Actor.Position.AlmostEqual(actor.Position, 1)); + if (slot >= 0) + Explosives[slot] = (actor, true, Explosives[slot].Activation); + else + Module.ReportError(this, $"found explosive {actor} with no matching telegraph"); + } + } + + public override void OnActorDestroyed(Actor actor) + { + if ((OID)actor.OID == OID.AetherochemicalExplosive) + Explosives.RemoveAll(e => e.Actor.Position.AlmostEqual(actor.Position, 1)); + } +} + +class Adds(BossModule module) : Components.AddsMulti(module, [0x1CB6, 0x1CD1, 0x1CD6, 0x1CD8]) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = e.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} + +class SummoningNodeStates : StateMachineBuilder +{ + public SummoningNodeStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68165, NameID = 6695)] +public class SummoningNode(WorldState ws, Actor primary) : BossModule(ws, primary, new(-111, -295), ArenaBounds) +{ + private static readonly List vertices = [ + new(-4.5f, 22.66f), + new(4.5f, 22.66f), + new(18f, 14.75f), + new(22.2f, 7.4f), + new(22.7f, 7.4f), + new(22.7f, -7.4f), + new(22.2f, -7.4f), + new(18.15f, -15.77f), + new(4.5f, -23.68f), + new(-4.5f, -23.68f), + new(-18.15f, -15.77f), + new(-22.2f, -7.4f), + new(-22.7f, -7.4f), + new(-22.7f, 6.4f), + new(-22.2f, 6.4f), + new(-18f, 14.75f) + ]; + + public static readonly ArenaBoundsCustom ArenaBounds = new(30, new(vertices)); +} diff --git a/BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs b/BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs new file mode 100644 index 0000000000..8faa744c35 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/BestServedWithColdSteel.cs @@ -0,0 +1,156 @@ +namespace BossMod.Stormblood.Quest.BestServedWithColdSteel; + +public enum OID : uint +{ + Boss = 0x1A52, // R2.100f, x1 + Grynewaht = 0x1A53, + Helper = 0x233C, +} + +public enum AID : uint +{ + CermetPile = 8117, // 1A52->self, 3.0fs cast, range 4$1fR width 6 rect + Firebomb = 8495, // Boss->location, 3.0fs cast, range 4 circle + OpenFire1 = 8121, // 19D9->location, 3.0fs cast, range 6 circle + AugmentedSuffering = 8492, // Boss->self, 3.5fs cast, range $1fR circle + AugmentedUprising = 8493, // Boss->self, 3.0fs cast, range $1fR 120-degree cone + SelfDetonate = 8122, // 1A56->self, no cast, range 6 circle + SelfDetonate1 = 9169, // Boss->self, 60.0s cast, range 100 circle +} + +public enum TetherID : uint +{ + Mine = 54, // 1A56->player +} + +class AugmentedUprising(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); +class AugmentedSuffering(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), new AOEShapeCircle(6.5f)); +class OpenFire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.OpenFire1), 6); + +class CermetPile(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CermetPile), new AOEShapeRect(42.1f, 3f)); +class Firebomb(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Firebomb), 4); + +class MagitekTurret(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.SelfDetonate)) +{ + class Mine(Actor source, Actor target, WPos sourcePosLastFrame, DateTime tethered) + { + public Actor source = source; + public Actor target = target; + public WPos sourcePosLastFrame = sourcePosLastFrame; + public DateTime tethered = tethered; + + public float DistanceLeft(WorldState ws) + { + var elapsed = (float)(ws.CurrentTime - tethered).TotalSeconds; + // approximation, turret starts moving after about 3.7s on average, but 4 is a nice round number + return Math.Clamp(12 - elapsed, 0, 8) * 3; + } + } + + private readonly List Mines = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var m in Mines.Where(m => m.target == actor)) + { + var mineToPlayer = m.target.Position - m.source.Position; + var projectedExplosion = mineToPlayer.Length() > m.DistanceLeft(WorldState) + ? (m.target.Position - m.source.Position).Normalized() * m.DistanceLeft(WorldState) + // offset danger zone slightly toward mine so that AI can dodge + // if centered on player it doesn't know which direction to go + : mineToPlayer * 0.9f; + yield return new AOEInstance(new AOEShapeCircle(6), m.source.Position + projectedExplosion, default, Activation: m.tethered.AddSeconds(12)); + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.Mine && WorldState.Actors.Find(tether.Target) is Actor target) + Mines.Add(new(source, target, source.Position, WorldState.CurrentTime)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.SelfDetonate) + Mines.RemoveAll(m => m.source == caster); + } + + public override void OnActorDestroyed(Actor actor) + { + Mines.RemoveAll(m => m.source == actor); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var m in Mines.Where(m => m.target == pc)) + Arena.AddLine(m.source.Position, pc.Position, ArenaColor.Danger); + } +} + +class MagitekSelfDetonate(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.SelfDetonate1)) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + NumCasts++; + } +} + +class MagitekVanguardIPrototypeStates : StateMachineBuilder +{ + private readonly MagitekVanguardIPrototype _module; + + private float BossHPRatio => (float)_module.PrimaryActor.HPMP.CurHP / _module.PrimaryActor.HPMP.MaxHP; + + public MagitekVanguardIPrototypeStates(MagitekVanguardIPrototype module) : base(module) + { + _module = module; + DeathPhase(0, P1); + } + + private void P1(uint id) + { + Condition(id, 300, () => BossHPRatio < 0.9f, "Adds 1") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + + Condition(id + 2, 300, () => BossHPRatio < 0.75f, "Adds 2"); + Condition(id + 4, 300, () => BossHPRatio < 0.65f, "Turrets").ActivateOnEnter(); + Condition(id + 6, 300, () => BossHPRatio < 0.55f, "Adds 3"); + Condition(id + 8, 300, () => BossHPRatio < 0.4f, "Cutscene").ActivateOnEnter(); + ComponentCondition(id + 10, 18, m => m.NumCasts > 0); + CastEnd(id + 12, 60, "Self-detonate").SetHint(StateMachine.StateHint.DowntimeStart); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67989, NameID = 5650)] +public class MagitekVanguardIPrototype(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, CustomBounds) +{ + private static readonly List vertices = [ + new(-487.40f, -230.79f), new(-487.56f, -188.08f), new(-478.75f, -181.25f), new(-439.37f, -183.46f), new(-457.85f, -211.90f), new(-461.13f, -228.75f) + ]; + + public static readonly WPos ArenaCenter = new(-465.40f, -202.09f); + public static readonly ArenaBoundsCustom CustomBounds = new(30, new(vertices.Select(v => v - ArenaCenter.ToWDir()))); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + { + if (h.Actor.OID == 0x1A52) + h.Priority = 1; + else if (h.Actor.TargetID == actor.InstanceID) + h.Priority = 2; + else + h.Priority = 0; + } + } +} diff --git a/BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs b/BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs new file mode 100644 index 0000000000..e78d012603 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/BloodOnTheDeck.cs @@ -0,0 +1,42 @@ +namespace BossMod.Stormblood.Quest; +public enum OID : uint +{ + Boss = 0x1BED, + Helper = 0x233C, + ShamShinobi = 0x1BE8, // R0.500, x4 (spawn during fight) + AdjunctOstyrgreinHelper = 0x1BEB, // R0.500, x0 (spawn during fight), Helper type + AdjunctOstyrgrein = 0x1BEA, // R0.500, x0 (spawn during fight) + Vanara = 0x1BE9, // R3.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + ScytheTail = 8407, // Vanara->self, 5.0s cast, range 4+R circle + Butcher = 8405, // Vanara->self, 5.0s cast, range 6+R ?-degree cone + TenkaGoken = 8408, // AdjunctOstyrgrein->self, 5.0s cast, range 8+R 120-degree cone + Bombslinger1 = 8411, // AdjunctOstyrgreinHelper->location, 3.0s cast, range 6 circle +} + +class ScytheTail(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ScytheTail), new AOEShapeCircle(7)); +class Butcher(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Butcher), new AOEShapeCone(9, 45.Degrees())); +class TenkaGoken(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TenkaGoken), new AOEShapeCone(8.5f, 60.Degrees())); +class Bombslinger(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Bombslinger1), 6); + +class GurumiBorlumiStates : StateMachineBuilder +{ + public GurumiBorlumiStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68098, NameID = 6289)] +public class GurumiBorlumi(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 15.8f), new ArenaBoundsRect(8, 7.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Stormblood/Quest/DragonSound.cs b/BossMod/Modules/Stormblood/Quest/DragonSound.cs new file mode 100644 index 0000000000..80b77ec816 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/DragonSound.cs @@ -0,0 +1,72 @@ +namespace BossMod.Stormblood.Quest.DragonSound; + +public enum OID : uint +{ + Boss = 0x1CDD, // R6.840, x1 + Faunehm = 0x18D6, // R0.500, x9 +} + +public enum AID : uint +{ + AbyssicBuster = 8929, // Boss->self, 2.0s cast, range 25+R 90-degree cone + Heavensfall1 = 8935, // 18D6->location, 2.0s cast, range 5 circle + DarkStar = 8931, // Boss->self, 2.0s cast, range 50+R circle +} + +public enum SID : uint +{ + Enervation = 1401, // Boss->1CDE/player, extra=0x0 +} + +class AbyssicBuster(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AbyssicBuster), new AOEShapeCone(31.84f, 45.Degrees())); +class Heavensfall(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Heavensfall1), 5); +class DarkStar(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DarkStar)); + +// scripted interaction, no idea if it's required to complete the duty but might as well do it +class Enervation(BossModule module) : BossComponent(module) +{ + private bool Active; + private Actor? OrnKhai; + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if (actor.OID == 0 && status.ID == (uint)SID.Enervation) + Active = true; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if (actor.OID == 0 && status.ID == (uint)SID.Enervation) + Active = false; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (!Active) + return; + + OrnKhai ??= WorldState.Actors.FirstOrDefault(x => x.OID == 0x1CDF); + if (OrnKhai == null) + return; + + hints.ActionsToExecute.Push(ActionID.MakeSpell(DRG.AID.ElusiveJump), actor, ActionQueue.Priority.Medium, facingAngle: -actor.AngleTo(OrnKhai)); + + hints.GoalZones.Add(p => p.InCircle(OrnKhai.Position, 3) ? 100 : 0); + } +} + +class FaunehmStates : StateMachineBuilder +{ + public FaunehmStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68450, NameID = 6347)] +public class Faunehm(WorldState ws, Actor primary) : BossModule(ws, primary, new(4, 248.5f), new ArenaBoundsCircle(25)); + diff --git a/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs b/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs new file mode 100644 index 0000000000..9936b94e45 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/EmissaryOfTheDawn.cs @@ -0,0 +1,38 @@ +using BossMod.QuestBattle.Stormblood.MSQ; + +namespace BossMod.Stormblood.Quest.EmissaryOfTheDawn; + +public enum OID : uint +{ + Boss = 0x234B, + Helper = 0x233C, +} + +class AlphiAI(BossModule module) : 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), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs b/BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs new file mode 100644 index 0000000000..67963a398a --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/HisForgottenHome.cs @@ -0,0 +1,70 @@ +namespace BossMod.Stormblood.Quest.HisForgottenHome; +public enum OID : uint +{ + Boss = 0x213A, + Helper = 0x233C, + SoftshellOfTheRed = 0x213B, // R1.600, x4 (spawn during fight) + SoftshellOfTheRed1 = 0x213C, // R1.600, x0 (spawn during fight) + SoftshellOfTheRed2 = 0x213D, // R1.600, x0 (spawn during fight) +} + +public enum AID : uint +{ + Kasaya = 8585, // SoftshellOfTheRed->self, 2.5s cast, range 6+R 120-degree cone + WaterIII = 5831, // Boss->location, 3.0s cast, range 8 circle + BlizzardIII = 10874, // Boss->location, 3.0s cast, range 5 circle +} + +class Kasaya(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Kasaya), new AOEShapeCone(7.6f, 60.Degrees())); +class WaterIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.WaterIII), 8); + +class BlizzardIIIIcon(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(5), 26, centerAtTarget: true) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.BlizzardIII) + CurrentBaits.Clear(); + } + + public override void OnActorDestroyed(Actor actor) + { + if (actor == Module.PrimaryActor) + CurrentBaits.Clear(); + } +} +class BlizzardIIICast(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 6, ActionID.MakeSpell(AID.BlizzardIII), m => m.Enemies(0x1E8D9C).Where(x => x.EventState != 7), 0); + +class SlickshellCaptainStates : StateMachineBuilder +{ + public SlickshellCaptainStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.Raid.Player()?.IsDeadOrDestroyed ?? true; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68563, NameID = 6891)] +public class SlickshellCaptain(WorldState ws, Actor primary) : BossModule(ws, primary, BoundsCenter, CustomBounds) +{ + public static readonly WPos BoundsCenter = new(468.92f, 301.30f); + + private static readonly List vertices = [ + new(464.25f, 320.19f), new(455.65f, 313.35f), new(457.72f, 308.20f), new(445.00f, 292.92f), new(468.13f, 283.56f), new(495.55f, 299.63f), new(487.19f, 313.73f) + ]; + + public static readonly ArenaBoundsCustom CustomBounds = new(30, new(vertices.Select(v => v - BoundsCenter))); + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // attack anyone targeting isse + foreach (var h in hints.PotentialTargets) + h.Priority = WorldState.Actors.Find(h.Actor.TargetID)?.OID == 0x2138 ? 1 : 0; + } +} + diff --git a/BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs b/BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs new file mode 100644 index 0000000000..036208cf37 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/HopeOnTheWaves.cs @@ -0,0 +1,80 @@ +namespace BossMod.Stormblood.Quest.HopeOnTheWaves; + +public enum OID : uint +{ + Boss = 0x21B1, + Helper = 0x233C, +} + +public enum AID : uint +{ + CermetPile = 9425, // Boss->self, 2.5s cast, range 40+R width 6 rect + SelfDetonate = 10928, // Boss->self, 30.0s cast, range 100 circle + CircleOfDeath = 9428, // 2115->self, 3.0s cast, range 6+R circle + W2TonzeMagitekMissile = 10929, // 2115->location, 3.0s cast, range 6 circle + SelfDetonate1 = 10930, // 21B6->self, 5.0s cast, range 6 circle + MagitekMissile1 = 10893, // 21B7->location, 10.0s cast, range 60 circle + AssaultCannon = 10823, // 21B5->self, 2.5s cast, range 75+R width 2 rect +} + +class AssaultCannon(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AssaultCannon), new AOEShapeRect(75, 1)); +class CircleOfDeath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CircleOfDeath), new AOEShapeCircle(10.24f)); +class TwoTonzeMagitekMissile(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.W2TonzeMagitekMissile), 6); +class MagitekMissileProximity(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekMissile1), 11.75f); +class CermetPile(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CermetPile), new AOEShapeRect(42, 3)); +class SelfDetonate(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SelfDetonate), "Kill before detonation!", true); +class MineSelfDetonate(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SelfDetonate1), new AOEShapeCircle(6)); + +class Adds(BossModule module) : BossComponent(module) +{ + private Actor? Alphinaud => WorldState.Actors.FirstOrDefault(a => a.OID == 0x21AC); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + WPos? lbCenter = Alphinaud?.CastInfo is { Action.ID: 10894 } castInfo + ? castInfo.LocXZ + : null; + + foreach (var e in hints.PotentialTargets) + { + if (lbCenter != null && e.Actor.OID == 0x2114) + { + e.ShouldBeTanked = true; + e.DesiredPosition = lbCenter.Value; + e.Priority = 5; + } + else if (e.Actor.CastInfo?.Action.ID == (uint)AID.SelfDetonate) + e.Priority = 5; + else + e.Priority = 0; + } + } +} + +class ImperialCenturionStates : StateMachineBuilder +{ + public ImperialCenturionStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.WorldState.CurrentCFCID != 472; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68560, NameID = 4148)] +public class ImperialCenturion(WorldState ws, Actor primary) : BossModule(ws, primary, new(473.25f, 751.75f), BoundsP2) +{ + public static readonly ArenaBoundsCustom BoundsP2 = new(30, new(CurveApprox.Ellipse(34, 21, 0.05f).Select(p => p.Rotate(140.Degrees())))); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Stormblood/Quest/Naadam.cs b/BossMod/Modules/Stormblood/Quest/Naadam.cs new file mode 100644 index 0000000000..0f2be8492c --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/Naadam.cs @@ -0,0 +1,142 @@ +namespace BossMod.Stormblood.Quest.Naadam; + +public enum OID : uint +{ + Boss = 0x1B31, + Helper = 0x233C, + MagnaiTheOlder = 0x1B38, // R0.500, x0 (spawn during fight) + StellarChuluu = 0x1B3F, // R1.800, x0 (spawn during fight) + StellarChuluu1 = 0x1B40, // R1.800, x0 (spawn during fight) + Grynewaht = 0x1B3A, // R0.500, x0 (spawn during fight) + Ovoo = 0x1EA4E1 +} + +public enum AID : uint +{ + ViolentEarth = 8389, // MagnaiTheOlder1->location, 3.0s cast, range 6 circle + DispellingWind = 8394, // SaduHeavensflame->self, 3.0s cast, range 40+R width 8 rect + Epigraph = 8339, // 1A58->self, 3.0s cast, range 45+R width 8 rect + DiffractiveLaser = 9122, // ArmoredWeapon->location, 3.0s cast, range 5 circle + AugmentedSuffering = 8492, // Grynewaht->self, 3.5s cast, range 6+R circle + AugmentedUprising = 8493, // Grynewaht->self, 3.0s cast, range 8+R 120-degree cone +} + +public enum SID : uint +{ + EarthenAccord = 778 +} + +class DiffractiveLaser(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), 5); +class AugmentedSuffering(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedSuffering), new AOEShapeCircle(6.5f)); +class AugmentedUprising(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AugmentedUprising), new AOEShapeCone(8.5f, 60.Degrees())); + +class ViolentEarth(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ViolentEarth), 6); +class DispellingWind(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DispellingWind), new AOEShapeRect(40.5f, 4)); +class Epigraph(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Epigraph), new AOEShapeRect(45, 4)); + +class DrawOvoo : BossComponent +{ + private Actor? Ovoo => WorldState.Actors.FirstOrDefault(o => o.OID == 0x1EA4E1); + + public DrawOvoo(BossModule module) : base(module) + { + KeepOnPhaseChange = true; + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actor(Ovoo, ArenaColor.Object, true); + } +} + +class ActivateOvoo(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.MountId == 117) + hints.WantDismount = true; + + var beingAttacked = false; + + foreach (var e in hints.PotentialTargets) + { + if (e.Actor.TargetID == actor.InstanceID) + beingAttacked = true; + else + e.Priority = AIHints.Enemy.PriorityForbidden; + } + + var ovoo = WorldState.Actors.FirstOrDefault(x => x.OID == 0x1EA4E1); + if (!beingAttacked && (ovoo?.IsTargetable ?? false)) + hints.InteractWithTarget = ovoo; + } +} + +class ProtectOvoo(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + { + if (e.Actor.FindStatus(SID.EarthenAccord) != null) + e.Priority = 5; + else if (e.Actor.OID == (uint)OID.StellarChuluu) + e.Priority = 1; + else + e.Priority = 0; + } + } +} + +class ProtectSadu(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var chuluu = WorldState.Actors.Where(x => (OID)x.OID == OID.StellarChuluu1).Select(x => x.InstanceID).ToList(); + + foreach (var e in hints.PotentialTargets) + { + if (chuluu.Contains(e.Actor.TargetID)) + e.Priority = 5; + else if ((OID)e.Actor.OID == OID.Grynewaht) + e.Priority = 1; + else + e.Priority = 0; + } + } +} + +class OvooStates : StateMachineBuilder +{ + public OvooStates(BossModule module) : base(module) + { + bool DutyEnd() => module.WorldState.CurrentCFCID != 246; + + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.WorldState.Actors.Any(x => x.OID == (uint)OID.MagnaiTheOlder && x.IsTargetable) || DutyEnd(); + TrivialPhase(1) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.WorldState.Actors.Any(x => x.OID == (uint)OID.Grynewaht && x.IsTargetable) || DutyEnd(); + TrivialPhase(2) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = DutyEnd; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68051, PrimaryActorOID = (uint)OID.Ovoo)] +public class Ovoo(WorldState ws, Actor primary) : BossModule(ws, primary, new(354, 296.5f), new ArenaBoundsCircle(20)) +{ + protected override bool CheckPull() => Raid.Player()?.Position.InCircle(PrimaryActor.Position, 15) ?? false; + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs b/BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs new file mode 100644 index 0000000000..23fff69301 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/OurUnsungHeroes.cs @@ -0,0 +1,57 @@ +namespace BossMod.Stormblood.Quest.OurUnsungHeroes; + +public enum OID : uint +{ + Boss = 0x1CAF, // R2.700, x1 + FallenKuribu = 0x18D6, // R0.500, x5 + ShadowSprite = 0x1CB4, // R0.800, x0 (spawn during fight) +} + +public enum AID : uint +{ + Glory = 5604, // Boss->self, 3.0s cast, range 40+R 90-degree cone + CureIV = 8635, // Boss->self, 5.0s cast, range 40 circle + CureIII1 = 8636, // FallenKuribu->players/1CAD/1CAE, no cast, range 10 circle + CureV1 = 8637, // FallenKuribu->players, no cast, range 6 circle + DarkII = 4366, // ShadowSprite->self, 2.5s cast, range 50+R 60-degree cone +} + +public enum IconID : uint +{ + CureIII = 71, // player/1CAD/1CAE + Stack = 62, // player +} + +public enum SID : uint +{ + Invincibility = 325, // Boss->Boss, extra=0x0 +} + +class CureIV(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CureIV), new AOEShapeCircle(12)); +class Glory(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Glory), new AOEShapeCone(42.7f, 45.Degrees())); +class CureIII(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.CureIII, ActionID.MakeSpell(AID.CureIII1), 10, 5.15f); +class CureV(BossModule module) : Components.StackWithIcon(module, (uint)IconID.Stack, ActionID.MakeSpell(AID.CureV1), 6, 5.15f); +class DarkII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkII), new AOEShapeCone(50.8f, 30.Degrees())); + +class FallenKuribuStates : StateMachineBuilder +{ + public FallenKuribuStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 265, NameID = 6345)] +public class FallenKuribu(WorldState ws, Actor primary) : BossModule(ws, primary, new(232.3f, 407.7f), new ArenaBoundsCircle(20)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.Invincibility) == null ? 1 : 0; + } +} diff --git a/BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs b/BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs new file mode 100644 index 0000000000..06db3c281d --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/RaisingTheSword.cs @@ -0,0 +1,75 @@ +namespace BossMod.Stormblood.Quest.RaisingTheSword; + +public enum OID : uint +{ + Boss = 0x1B51, + Helper = 0x233C, + AldisSwordOfNald = 0x18D6, // R0.500, x10 + TaintedWindSprite = 0x1B52, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + ShudderingSwipeCast = 8136, // Boss->player, 3.0s cast, single-target + ShudderingSwipeAOE = 8137, // 18D6->self, 3.0s cast, range 60+R 30-degree cone + NaldsWhisper = 8141, // 18D6->self, 9.0s cast, range 4 circle + VictorySlash = 8134, // Boss->self, 3.0s cast, range 6+R 120-degree cone +} + +class VictorySlash(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VictorySlash), new AOEShapeCone(6.5f, 60.Degrees())); +class ShudderingSwipeCone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ShudderingSwipeAOE), new AOEShapeCone(60, 15.Degrees())); +class ShudderingSwipeKB(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.ShudderingSwipeCast), stopAtWall: true) +{ + private TheFourWinds? winds; + private readonly List Casters = []; + + public override IEnumerable Sources(int slot, Actor actor) => Casters.Select(c => new Source(c.Position, 10, Module.CastFinishAt(c.CastInfo), null, c.AngleTo(actor), Kind.DirForward)); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.ShudderingSwipeCast) + Casters.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.ShudderingSwipeCast) + Casters.Remove(caster); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + winds ??= Module.FindComponent(); + + var aoes = (winds?.Sources(Module) ?? []).Select(a => ShapeDistance.Circle(a.Position, 6)).ToList(); + if (aoes.Count == 0) + return; + + var windzone = ShapeDistance.Union(aoes); + if (Casters.FirstOrDefault() is Actor c) + hints.AddForbiddenZone(p => + { + var dir = c.DirectionTo(p); + var projected = p + dir * 10; + return windzone(projected); + }, Module.CastFinishAt(c.CastInfo)); + } +} +class NaldsWhisper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.NaldsWhisper), new AOEShapeCircle(20)); +class TheFourWinds(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.TaintedWindSprite).Where(x => x.EventState != 7)); + +class AldisSwordOfNaldStates : StateMachineBuilder +{ + public AldisSwordOfNaldStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 270, NameID = 6311)] +public class AldisSwordOfNald(WorldState ws, Actor primary) : BossModule(ws, primary, new(-89.3f, 0), new ArenaBoundsCircle(20.5f)); diff --git a/BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs b/BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs new file mode 100644 index 0000000000..92d76ac875 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ReturnOfTheBull.cs @@ -0,0 +1,104 @@ +namespace BossMod.Stormblood.Quest.ReturnOfTheBull; +public enum OID : uint +{ + Boss = 0x1FD2, + Helper = 0x233C, + Lakshmi = 0x18D6, // R0.500, x12, Helper type + DreamingKshatriya = 0x1FDD, // R1.000, x0 (spawn during fight) + DreamingFighter = 0x1FDB, // R0.500, x0 (spawn during fight) + Aether = 0x1FD3, // R1.000, x0 (spawn during fight) + FordolaShield = 0x1EA080, +} + +public enum AID : uint +{ + BlissfulSpear = 9872, // Lakshmi->self, 11.0s cast, range 40 width 8 cross + BlissfulHammer = 9874, // Lakshmi->self, no cast, range 7 circle + ThePallOfLight = 9877, // Boss->players/1FD8, 5.0s cast, range 6 circle + ThePathOfLight = 9875, // Boss->self, 5.0s cast, range 40+R 120-degree cone +} + +class PathOfLight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ThePathOfLight), new AOEShapeCone(43.5f, 60.Degrees())); +class BlissfulSpear(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BlissfulSpear), new AOEShapeCross(40, 4)); +class ThePallOfLight(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ThePallOfLight), 6, 1); +class BlissfulHammer(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(7), 109, ActionID.MakeSpell(AID.BlissfulHammer), 12.15f, true); +class FordolaShield(BossModule module) : BossComponent(module) +{ + public Actor? Shield => WorldState.Actors.FirstOrDefault(a => (OID)a.OID == OID.FordolaShield); + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (Shield != null) + Arena.AddCircleFilled(Shield.Position, 4, ArenaColor.SafeFromAOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Shield != null) + hints.AddForbiddenZone(new AOEShapeDonut(4, 100), Shield.Position, default, WorldState.FutureTime(5)); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Shield != null && !actor.Position.InCircle(Shield.Position, 4)) + hints.Add("Go to safe zone!"); + } +} + +class Deflect(BossModule module) : BossComponent(module) +{ + public IEnumerable Spheres => Module.Enemies(OID.Aether).Where(x => !x.IsDeadOrDestroyed); + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Spheres, 0xFFFFA080); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var deflectAction = WorldState.Client.DutyActions[0].Action; + var deflectRadius = deflectAction.ID == 10006 ? 4 : 20; + + var closestSphere = Spheres.MaxBy(x => x.Position.Z); + if (closestSphere != null) + { + var optimalDeflectPosition = closestSphere.Position with { Z = closestSphere.Position.Z + 1 }; + + hints.GoalZones.Add(hints.GoalSingleTarget(optimalDeflectPosition, deflectRadius - 2, 10)); + + if (actor.DistanceToHitbox(closestSphere) < deflectRadius - 1) + hints.ActionsToExecute.Push(deflectAction, actor, ActionQueue.Priority.VeryHigh); + } + } +} + +class LakshmiStates : StateMachineBuilder +{ + public LakshmiStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68508, NameID = 6385)] +public class Lakshmi(WorldState ws, Actor primary) : BossModule(ws, primary, new(250, -353), new ArenaBoundsSquare(23)) +{ + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + e.Priority = (OID)e.Actor.OID switch + { + OID.Boss => 1, + OID.Aether => -1, + _ => 0 + }; + } +} + diff --git a/BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs b/BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs new file mode 100644 index 0000000000..8410fa5c46 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/RhalgrsBeacon.cs @@ -0,0 +1,123 @@ +namespace BossMod.Stormblood.Quest.RhalgrsBeacon; + +public enum OID : uint +{ + Boss = 0x1A88, + Helper = 0x233C, + TerminusEst = 0x1BCA, + MarkXLIIIArtilleryCannon = 0x1B4A, // R2.000, x3 + SkullsSpear = 0x1A8C, // R0.500, x3 + SkullsBlade = 0x1A8B, // R0.500, x3 + MagitekTurretII = 0x1BC7, // R0.600, x0 (spawn during fight) + ChoppingBlock = 0x1EA4D9, // R0.500, x0 (spawn during fight), voidzone event object +} + +public enum AID : uint +{ + TheOrder = 8370, // Boss->self, 3.0s cast, single-target + TerminusEst1 = 8337, // 1BCA->self, no cast, range 40+R width 4 rect + Gunblade = 8310, // Boss->player, 5.0s cast, single-target, 10y knockback + DiffractiveLaser = 8340, // 1BC7->self, 2.5s cast, range 18+R 60-degree cone + ChoppingBlock1 = 8346, // 1A57->location, 3.0s cast, range 5 circle +} + +class DiffractiveLaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), new AOEShapeCone(18.6f, 30.Degrees())); + +class TerminusEst(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.TheOrder)) +{ + private readonly List Termini = []; + private DateTime? CastFinish; + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Module.Enemies(OID.TerminusEst).Where(x => !x.IsDead), ArenaColor.Object, true); + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var t in Termini) + yield return new AOEInstance(new AOEShapeRect(41f, 2), t.Position, t.Rotation, Activation: CastFinish ?? WorldState.FutureTime(10)); + } + + public override void OnActorCreated(Actor actor) + { + if (actor.OID == (uint)OID.TerminusEst) + Termini.Add(actor); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + CastFinish = Module.CastFinishAt(spell); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.TerminusEst1) + Termini.Remove(caster); + } +} + +class Gunblade(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.Gunblade), stopAtWall: true) +{ + public readonly List Casters = []; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var caster = Casters.FirstOrDefault(); + if (caster == null) + return; + + var voidzones = Module.Enemies(OID.ChoppingBlock).Where(x => x.EventState != 7).Select(v => ShapeDistance.Circle(v.Position, 5)).ToList(); + if (voidzones.Count == 0) + return; + + var combined = ShapeDistance.Union(voidzones); + + float projectedDist(WPos pos) + { + var direction = (pos - caster.Position).Normalized(); + var projected = pos + 10 * direction; + return combined(projected); + } + + hints.AddForbiddenZone(projectedDist, Module.CastFinishAt(caster.CastInfo)); + } + + public override IEnumerable Sources(int slot, Actor actor) + { + foreach (var c in Casters) + yield return new(c.Position, 10, Module.CastFinishAt(c.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + Casters.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + Casters.Remove(caster); + } +} + +class ChoppingBlock(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 5, ActionID.MakeSpell(AID.ChoppingBlock1), m => m.Enemies(OID.ChoppingBlock).Where(x => x.EventState != 7), 0); + +class FordolaRemLupisStates : StateMachineBuilder +{ + public FordolaRemLupisStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68064, NameID = 5953)] +public class FordolaRemLupis(WorldState ws, Actor primary) : BossModule(ws, primary, new(-195.25f, 147.5f), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs b/BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs new file mode 100644 index 0000000000..a25f292749 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheBattleOnBekko.cs @@ -0,0 +1,76 @@ +namespace BossMod.Stormblood.Quest.TheBattleOnBekko; + +public enum OID : uint +{ + Boss = 0x1BF8, + Helper = 0x233C, + UgetsuSlayerOfAThousandSouls = 0x1BF9, // R0.500, x20, Helper type + Voidzone = 0x1E8EA9, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + HissatsuKyuten = 8433, // Boss->self, 3.0s cast, range 5+R circle + TenkaGoken = 9145, // Boss->self, 3.0s cast, range 8+R 120-degree cone + ShinGetsubaku = 8437, // 1BF9->location, 3.0s cast, range 6 circle + MijinGiri = 8435, // 1BF9->self, 2.5s cast, range 80+R width 10 rect + Ugetsuzan = 8439, // 1BF9->self, 2.5s cast, range -7 donut + Ugetsuzan2 = 8440, // 1BF9->self, 2.5s cast, range -12 donut + Ugetsuzan3 = 8441, // 1BF9->self, 2.5s cast, range -17 donut + KuruiYukikaze = 8446, // UgetsuSlayerOfAThousandSouls->self, 2.5s cast, range 44+R width 4 rect + KuruiGekko1 = 8447, // UgetsuSlayerOfAThousandSouls->self, 2.0s cast, range 30 circle + KuruiKasha1 = 8448, // UgetsuSlayerOfAThousandSouls->self, 2.5s cast, range 8+R ?-degree cone + Ugetsuzan4 = 8442, // UgetsuSlayerOfAThousandSouls->self, 2.5s cast, range -22 donut +} + +class KuruiGekko(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.KuruiGekko1)); +class KuruiKasha(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KuruiKasha1), new AOEShapeDonutSector(4.5f, 8.5f, 45.Degrees())); +class KuruiYukikaze(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KuruiYukikaze), new AOEShapeRect(44, 2), 8); +class HissatsuKyuten(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HissatsuKyuten), new AOEShapeCircle(5.5f)); +class TenkaGoken(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TenkaGoken), new AOEShapeCone(8.5f, 60.Degrees())); +class ShinGetsubaku(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ShinGetsubaku), 6); +class ShinGetsubakuVoidzone(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.Voidzone).Where(e => e.EventState != 7)); +class MijinGiri(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MijinGiri), new AOEShapeRect(80, 5, 2)); +class Ugetsuzan(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeDonutSector(2, 7, 90.Degrees()), new AOEShapeDonutSector(7, 12, 90.Degrees()), new AOEShapeDonutSector(12, 17, 90.Degrees())]) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Ugetsuzan) + AddSequence(caster.Position - caster.Rotation.ToDirection() * 4, Module.CastFinishAt(spell), caster.Rotation); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var idx = (AID)spell.Action.ID switch + { + AID.Ugetsuzan => 0, + AID.Ugetsuzan2 => 1, + AID.Ugetsuzan3 => 2, + AID.Ugetsuzan4 => 3, + _ => -1 + }; + AdvanceSequence(idx, caster.Position - caster.Rotation.ToDirection() * 4, WorldState.FutureTime(2.5f), caster.Rotation); + } +} + +class UgetsuSlayerOfAThousandSoulsStates : StateMachineBuilder +{ + public UgetsuSlayerOfAThousandSoulsStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68106, NameID = 6096)] +public class UgetsuSlayerOfAThousandSouls(WorldState ws, Actor primary) : BossModule(ws, primary, new(808.8f, 69.5f), new ArenaBoundsSquare(14)); + diff --git a/BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs b/BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs new file mode 100644 index 0000000000..49774827d1 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheFaceOfTrueEvil.cs @@ -0,0 +1,74 @@ +namespace BossMod.Stormblood.Quest.TheFaceOfTrueEvil; + +public enum OID : uint +{ + Boss = 0x1BEE, + Helper = 0x233C, + Musosai = 0x1BEF, // R0.500, x12, Helper type + Musosai1 = 0x1BF0, // R1.000, x0 (spawn during fight) + ViolentWind = 0x1BF1, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + HissatsuTo1 = 8415, // 1BEF->self, 3.0s cast, range 44+R width 4 rect + HissatsuKyuten = 8412, // Boss->self, 3.0s cast, range 5+R circle + Arashi = 8418, // Boss->self, 4.0s cast, single-target + Arashi1 = 8419, // 1BF0->self, no cast, range 4 circle + HissatsuKiku1 = 8417, // Musosai->self, 4.0s cast, range 44+R width 4 rect + Maiogi1 = 8421, // Musosai->self, 4.0s cast, range 80+R ?-degree cone + Musojin = 8422, // Boss->self, 25.0s cast, single-target + ArashiNoKiku = 8643, // Boss->self, 3.0s cast, single-target + ArashiNoMaiogi = 8642, // Boss->self, 3.0s cast, single-target +} + +class Musojin(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Musojin)); +class HissatsuKiku(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HissatsuKiku1), new AOEShapeRect(44.5f, 2)); +class Maiogi(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Maiogi1), new AOEShapeCone(80, 25.Degrees())); +class HissatsuTo(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HissatsuTo1), new AOEShapeRect(44.5f, 2)); +class HissatsuKyuten(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HissatsuKyuten), new AOEShapeCircle(5.5f)); +class Arashi(BossModule module) : Components.GenericAOEs(module) +{ + private DateTime? Activation; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Activation == null) + yield break; + + foreach (var e in Module.Enemies(OID.Musosai1)) + yield return new AOEInstance(new AOEShapeCircle(4), e.Position, default, Activation.Value); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.Arashi or AID.ArashiNoKiku or AID.ArashiNoMaiogi) + Activation = Module.CastFinishAt(spell); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.Arashi1) + Activation = null; + } +} +class ViolentWind(BossModule module) : Components.Adds(module, (uint)OID.ViolentWind); + +class MusosaiStates : StateMachineBuilder +{ + public MusosaiStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68101, NameID = 6111)] +public class Musosai(WorldState ws, Actor primary) : BossModule(ws, primary, new(-217.27f, -158.31f), new ArenaBoundsSquare(15)); + diff --git a/BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs b/BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs new file mode 100644 index 0000000000..4d392dfffc --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheMeasureOfHisReach.cs @@ -0,0 +1,47 @@ +namespace BossMod.Stormblood.Quest.TheMeasureOfHisReach; + +public enum OID : uint +{ + Boss = 0x1C48, + Helper = 0x233C, + Whitefang = 0x1C5A +} + +public enum AID : uint +{ + HowlingIcewind = 8397, // 1C4F->self, 2.5s cast, range 44+R width 4 rect + Dragonspirit = 8450, // 1C5A/1C5B->self, 3.0s cast, range 6+R circle + HowlingMoonlight = 8398, // 1C59->self, 7.0s cast, range 22+R circle + HowlingBloomshower = 8399, // 1C4F->self, 2.5s cast, range 8+R ?-degree cone +} + +class Moonlight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HowlingMoonlight), new AOEShapeCircle(10)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + // hits everyone (proximity damage) + foreach (var c in Casters) + hints.PredictedDamage.Add((Raid.WithSlot().Mask(), Module.CastFinishAt(c.CastInfo))); + } +} +class Icewind(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HowlingIcewind), new AOEShapeRect(44, 2)); +class Dragonspirit(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Dragonspirit), new AOEShapeCircle(7.5f)); +class Bloomshower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HowlingBloomshower), new AOEShapeDonutSector(4, 8, 45.Degrees())); + +class HakuroWhitefangStates : StateMachineBuilder +{ + public HakuroWhitefangStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + ; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68088, NameID = 5975)] +public class HakuroWhitefang(WorldState ws, Actor primary) : BossModule(ws, primary, new(504, -133), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs b/BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs new file mode 100644 index 0000000000..0bc2115ab8 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheOrphansAndTheBrokenBlade.cs @@ -0,0 +1,57 @@ +namespace BossMod.Stormblood.Quest.TheOrphansAndTheBrokenBlade; + +public enum OID : uint +{ + Boss = 0x1C5E, + Helper = 0x233C, +} + +public enum AID : uint +{ + ShadowOfDeath1 = 8459, // 1C5F->location, 3.0s cast, range 5 circle + HeadsmansDelight = 8457, // Boss->1C5C, 5.0s cast, range 5 circle + SpiralHell = 8453, // 1C5F->self, 3.0s cast, range 40+R width 4 rect + HeadmansDelight = 9298, // 1C5F->player/1C5C, no cast, single-target +} + +class SpiralHell(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SpiralHell), new AOEShapeRect(40, 2)); +class HeadsmansDelight(BossModule module) : Components.GenericStackSpread(module) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.HeadsmansDelight && WorldState.Actors.Find(spell.TargetID) is Actor tar) + Stacks.Add(new(tar, 5, activation: Module.CastFinishAt(spell))); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.HeadmansDelight) + Stacks.Clear(); + } +} +class ShadowOfDeath(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ShadowOfDeath1), 5); +class DarkChain(BossModule module) : Components.Adds(module, 0x1C60) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + hints.PrioritizeTargetsByOID(0x1C60, 5); + } +} + +class OmpagneDeepblackStates : StateMachineBuilder +{ + public OmpagneDeepblackStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68453, NameID = 6300)] +public class OmpagneDeepblack(WorldState ws, Actor primary) : BossModule(ws, primary, new(-166.8f, 290), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} diff --git a/BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs b/BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs new file mode 100644 index 0000000000..ce71d1b239 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/ThePowerToProtect.cs @@ -0,0 +1,77 @@ +namespace BossMod.Stormblood.Quest.ThePowerToProtect; + +public enum OID : uint +{ + Boss = 0x1BCB, // R5.400, x1 + CorpseBrigadeKnuckledancer = 0x1C0C, // R0.500, x2 (spawn during fight) + CorpseBrigadeBowdancer = 0x1C0D, // R0.500, x2 (spawn during fight) + HeweraldIronaxe = 0x1C01, // R0.500, x1 + CorpseBrigadeFiredancer = 0x1C00, // R0.500, x0 (spawn during fight) + CorpseBrigadeBowdancer1 = 0x1BFF, // R0.500, x0 (spawn during fight) + CorpseBrigadeKnuckledancer1 = 0x1BFE, // R0.500, x0 (spawn during fight) + CorpseBrigadeBarber = 0x1BFD, // R0.500, x0 (spawn during fight) + SalvagedSlasher = 0x1C1F, // R1.050, x0 (spawn during fight) + CorpseBrigadeVanguard = 0x1C02, // R2.000, x0 (spawn during fight) + FireII = 0x1EA4C6, +} + +public enum AID : uint +{ + IronTempest = 1003, // HeweraldIronaxe->self, 3.5s cast, range 5+R circle + FireII = 2175, // CorpseBrigadeFiredancer->location, 2.5s cast, range 5 circle + Overpower = 720, // HeweraldIronaxe->self, 2.5s cast, range 6+R 90-degree cone + Rive = 1135, // HeweraldIronaxe->self, 2.5s cast, range 30+R width 2 rect + DiffractiveLaser = 8348, // Boss->location, 4.0s cast, range 5 circle +} + +public enum SID : uint +{ + ExtremeCaution = 1269, // Boss->player, extra=0x0 + +} + +class ExtremeCaution(BossModule module) : Components.StayMove(module) +{ + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.ExtremeCaution && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + PlayerStates[slot] = new(Requirement.Stay, status.ExpireAt); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.ExtremeCaution && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + PlayerStates[slot] = default; + } +} +class IronTempest(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IronTempest), new AOEShapeCircle(5.5f)); +class FireII(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 5, ActionID.MakeSpell(AID.FireII), m => m.Enemies(OID.FireII).Where(x => x.EventState != 7), 0); +class Overpower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Overpower), new AOEShapeCone(6.5f, 45.Degrees())); +class Rive(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Rive), new AOEShapeRect(30.5f, 1)); +class DiffractiveLaser(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.DiffractiveLaser), 5); + +class IoStates : StateMachineBuilder +{ + public IoStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 67966, NameID = 5667)] +public class Io(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, B) +{ + public static readonly WPos ArenaCenter = new(76.28f, -659.47f); + public static readonly WPos[] Corners = [new(101.93f, -666.63f), new(94.49f, -639.63f), new(50.64f, -652.38f), new(57.58f, -679.32f)]; + + public static readonly ArenaBoundsCustom B = new(25, new(Corners.Select(c => c - ArenaCenter))); + + protected override void DrawEnemies(int pcSlot, Actor pc) => Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); +} + diff --git a/BossMod/Modules/Stormblood/Quest/TheResonant.cs b/BossMod/Modules/Stormblood/Quest/TheResonant.cs new file mode 100644 index 0000000000..713d29fdec --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheResonant.cs @@ -0,0 +1,86 @@ +namespace BossMod.Stormblood.Quest.TheResonant; + +public enum OID : uint +{ + Boss = 0x1B7D, + Helper = 0x233C, + FordolaRemLupis = 0x18D6, // R0.500, x4, Helper type + MarkXLIIIArtilleryCannon = 0x1B7E, // R0.600, x0 (spawn during fight) + FordolaRemLupis1 = 0x1BCA, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + MagitekRay = 9104, // 1B7E->self, 2.5s cast, range 45+R width 2 rect + ChoppingBlock1 = 9110, // 18D6->location, 3.0s cast, range 5 circle + TheOrder = 9106, // Boss->self, 5.0s cast, single-target + TerminusEst1 = 9108, // FordolaRemLupis1->self, no cast, range 40+R width 4 rect + Skullbreaker1 = 9112, // FordolaRemLupis->self, 6.0s cast, range 40 circle +} + +public enum SID : uint +{ + Resonant = 780, +} + +class Skullbreaker(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Skullbreaker1), new AOEShapeCircle(12)); + +class TerminusEst(BossModule module) : Components.GenericAOEs(module) +{ + private DateTime? Activation; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Activation == null) + yield break; + + var casters = Module.Enemies(0x1BCA).Where(e => e.Position.AlmostEqual(Arena.Center, 0.5f)); + foreach (var c in casters) + yield return new AOEInstance(new AOEShapeRect(41, 2), c.Position, c.Rotation, Activation: Activation.Value); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.TheOrder) + Activation = Module.CastFinishAt(spell).AddSeconds(0.8f); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (int)AID.TerminusEst1) + Activation = null; + } +} +class MagitekRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRay), new AOEShapeRect(45.6f, 1)); +class ChoppingBlock(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ChoppingBlock1), 5); + +class Siphon(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + { + if (h.Actor.FindStatus(SID.Resonant) != null) + { + h.Priority = AIHints.Enemy.PriorityForbidden; + hints.ActionsToExecute.Push(WorldState.Client.DutyActions[0].Action, h.Actor, ActionQueue.Priority.ManualEmergency); // use emergency mode to bypass forbidden state - duty action is the only thing we can use on fordola without being stunned + } + } + } +} + +public class FordolaRemLupisStates : StateMachineBuilder +{ + public FordolaRemLupisStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68086, NameID = 6104)] +public class FordolaRemLupis(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 0), new ArenaBoundsSquare(19.5f)); diff --git a/BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs b/BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs new file mode 100644 index 0000000000..3e51ac891c --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheTimeBetweenTheSeconds.cs @@ -0,0 +1,90 @@ +namespace BossMod.Stormblood.Quest.TheTimeBetweenTheSeconds; + +public enum OID : uint +{ + Boss = 0x1A36, + Helper = 0x233C, + ZenosYaeGalvus = 0x1CEE, // R0.500, x9 + DomanSignifer = 0x1A3A, // R0.500, x3 + DomanHoplomachus = 0x1A39, // R0.500, x2 + ZenosYaeGalvus1 = 0x1EBC, // R0.920, x1 + DarkReflection = 0x1A37, // R0.920, x2 + LightlessFlame = 0x1CED, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + VeinSplitter = 8987, // 1A36->self, 3.5s cast, range 10 circle + Concentrativity = 8986, // 1A36->self, 3.0s cast, range 80 circle + LightlessFlame = 8988, // 1CED->self, 1.0s cast, range 10+R circle + LightlessSpark = 8985, // 1A36->self, 3.0s cast, range 40+R 90-degree cone + ArtOfTheSword1 = 8993, // 1CEE->self, 3.0s cast, range 40+R width 6 rect +} + +class ArtOfTheSword(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ArtOfTheSword1), new AOEShapeRect(41, 3)); +class VeinSplitter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VeinSplitter), new AOEShapeCircle(10)); +class Concentrativity(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Concentrativity)); +class LightlessFlame(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.LightlessFlame)) +{ + private readonly Dictionary Flames = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Flames.Values.Select(p => new AOEInstance(new AOEShapeCircle(11), p.position, Activation: p.activation)); + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.LightlessFlame) + Flames.Add(actor.InstanceID, (actor.Position, WorldState.CurrentTime.AddSeconds(7))); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.LightlessFlame) + Flames[caster.InstanceID] = (caster.Position, Module.CastFinishAt(spell)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.LightlessFlame) + Flames.Remove(caster.InstanceID); + } +} +class LightlessSpark(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LightlessSpark), new AOEShapeCone(40.92f, 45.Degrees())); +class P2Boss(BossModule module) : BossComponent(module) +{ + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(Module.Enemies(OID.ZenosYaeGalvus1), ArenaColor.Enemy); + Arena.Actors(Module.Enemies(OID.DarkReflection), ArenaColor.Enemy); + } +} + +class ZenosYaeGalvusStates : StateMachineBuilder +{ + public ZenosYaeGalvusStates(BossModule module) : base(module) + { + SimplePhase(0, id => BuildState(id, "P1 enrage", 1800), "P1") + .Raw.Update = () => !Module.PrimaryActor.IsTargetable; + SimplePhase(1, id => BuildState(id, "P2 enrage", 1800).ActivateOnEnter().ActivateOnEnter(), "P2") + .Raw.Update = () => !Module.Enemies(OID.ZenosYaeGalvus1).Any(); + } + + private State BuildState(uint id, string name, float duration = 10000) + { + return SimpleState(id, duration, name) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68034, NameID = 5954)] +public class ZenosYaeGalvus(WorldState ws, Actor primary) : BossModule(ws, primary, new(-247, 546.5f), CustomBounds) +{ + private static readonly List vertices = [ + new(-226.91f, 523.65f), new(-254.46f, 524.46f), new(-254.66f, 541.06f), new(-269.99f, 544.12f), new(-269.58f, 565.97f), new(-254.58f, 565.89f), new(-249.05f, 554.06f), new(-229.18f, 562.35f) +]; + + public static readonly ArenaBoundsCustom CustomBounds = new(25, new(vertices.Select(v => v - new WDir(-247, 546.5f)))); +} + diff --git a/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs b/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs new file mode 100644 index 0000000000..b3da138e51 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TheWillOfTheMoon.cs @@ -0,0 +1,169 @@ +using BossMod.QuestBattle; +using RPID = BossMod.Roleplay.AID; + +namespace BossMod.Stormblood.Quest.TheWillOfTheMoon; + +public enum OID : uint +{ + Boss = 0x24A0, + Magnai = 0x24A1, + Helper = 0x233C, + KhunShavar = 0x252F, // R1.820, x0 (spawn during fight) + Hien = 0x24A3, + Daidukul = 0x24A2, // R0.500, x1 + TheScaleOfTheFather = 0x2532, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + DispellingWind = 13223, // Boss->self, 3.0s cast, range 40+R width 8 rect + Epigraph = 13225, // 252D->self, 3.0s cast, range 45+R width 8 rect + WhisperOfLivesPast = 13226, // 252E->self, 3.5s cast, range -12 donut + AncientBlizzard = 13227, // 252F->self, 3.0s cast, range 40+R 45-degree cone + Tornado = 13228, // 252F->location, 5.0s cast, range 6 circle + Epigraph2 = 13222, // 2530->self, 3.0s cast, range 45+R width 8 rect + FlatlandFury = 13244, // 2532->self, 17.0s cast, range 10 circle + FlatlandFuryEnrage = 13329, // 249F->self, 25.0s cast, range 10 circle + ViolentEarth = 13236, // 233C->location, 3.0s cast, range 6 circle + WindChisel = 13518, // 233C->self, 2.0s cast, range 34+R 20-degree cone + TranquilAnnihilation = 13233, // _Gen_DaidukulTheMirthful->24A3, 15.0s cast, single-target +} + +public enum SID : uint +{ + Invincibility = 775, // none->Boss, extra=0x0 +} + +class DispellingWind(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DispellingWind), new AOEShapeRect(40, 4)); +class Epigraph(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Epigraph), new AOEShapeRect(45, 4)); +class Whisper(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WhisperOfLivesPast), new AOEShapeDonut(6, 12)); +class Blizzard(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AncientBlizzard), new AOEShapeCone(40, 22.5f.Degrees())); +class Tornado(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Tornado), 6); +class Epigraph1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Epigraph2), new AOEShapeRect(45, 4)); + +public class FlatlandFury(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FlatlandFury), new AOEShapeCircle(10)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // if all 9 adds are alive, instead of drawing forbidden zones (which would fill the whole arena), force AI to target nearest one to kill it + if (ActiveCasters.Count() == 9) + hints.ForcedTarget = ActiveCasters.MinBy(actor.DistanceToHitbox); + else + base.AddAIHints(slot, actor, assignment, hints); + } +} + +public class FlatlandFuryEnrage(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FlatlandFuryEnrage), new AOEShapeCircle(10)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (ActiveCasters.Count() < 9) + base.AddAIHints(slot, actor, assignment, hints); + } +} + +public class ViolentEarth(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.ViolentEarth), 6); +public class WindChisel(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WindChisel), new AOEShapeCone(34, 10.Degrees())); + +public class Scales(BossModule module) : Components.Adds(module, (uint)OID.TheScaleOfTheFather); + +class AutoYshtola(WorldState ws) : UnmanagedRotation(ws, 25) +{ + private Actor Magnai => World.Actors.First(x => (OID)x.OID == OID.Magnai); + private Actor Hien => World.Actors.First(x => (OID)x.OID == OID.Hien); + private Actor Daidukul => World.Actors.First(x => (OID)x.OID == OID.Daidukul); + + protected override void Exec(Actor? primaryTarget) + { + var hienMinHP = Daidukul.CastInfo?.Action.ID == (uint)AID.TranquilAnnihilation + ? 28000 + : 10000; + + if (Hien.PredictedHPRaw < hienMinHP) + { + if (Player.DistanceToHitbox(Hien) > 25) + Hints.ForcedMovement = Player.DirectionTo(Hien).ToVec3(); + + UseAction(RPID.CureIISeventhDawn, Hien); + } + + if (Hien.CastInfo?.Action.ID == 13234) + Hints.GoalZones.Add(Hints.GoalSingleTarget(Hien.Position, 2, 5)); + + var aero = StatusDetails(Magnai, WHM.SID.Aero2, Player.InstanceID); + if (aero.Left < 4.6f) + UseAction(RPID.AeroIISeventhDawn, Magnai); + + UseAction(RPID.StoneIVSeventhDawn, primaryTarget); + + if (Player.HPMP.CurMP < 5000) + UseAction(RPID.Aetherwell, Player); + } +} + +class YshtolaAI(BossModule module) : RotationModule(module); + +class P1Hints(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + { + if (e.Actor.FindStatus(SID.Invincibility) != null) + e.Priority = AIHints.Enemy.PriorityInvincible; + + // they do very little damage and sadu will raise them after a short delay, no point in attacking + if ((OID)e.Actor.OID == OID.KhunShavar) + e.Priority = AIHints.Enemy.PriorityPointless; + } + } +} + +class P2Hints(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var e in hints.PotentialTargets) + { + e.Priority = e.Actor.OID == (uint)OID.Magnai ? 1 : 0; + } + } +} + +class SaduHeavensflameStates : StateMachineBuilder +{ + public SaduHeavensflameStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.Enemies(OID.Magnai).Any(); + TrivialPhase(1) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .OnEnter(() => + { + Module.Arena.Center = new(-186.5f, 550.5f); + }) + .Raw.Update = () => Module.Raid.Player()?.IsDeadOrDestroyed ?? true; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68683, NameID = 6152)] +public class SaduHeavensflame(WorldState ws, Actor primary) : BossModule(ws, primary, new(-223, 519), new ArenaBoundsCircle(20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs b/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs new file mode 100644 index 0000000000..3a59b827e1 --- /dev/null +++ b/BossMod/Modules/Stormblood/Quest/TortoiseInTime.cs @@ -0,0 +1,119 @@ +namespace BossMod.Stormblood.Quest.TortoiseInTime; + +public enum OID : uint +{ + Boss = 0x2339, + Helper = 0x233C, + Soroban = 0x2351, // R0.500, x8 + MonkeyMagick = 0x23C2, // R1.000, x0 (spawn during fight) + Font = 0x233B, // R4.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + Eddy1 = 11511, // 2351->location, 3.0s cast, range 6 circle + GreatFlood1 = 11513, // 2351->self, no cast, range 60 circle + SpiritBurst = 11706, // 23C2->self, 1.0s cast, range 6 circle + WaterDrop = 11301, // 2351->234F, 8.0s cast, range 6 circle + Whitewater1 = 11521, // 2351->self, 3.0s cast, range 40+R width 7 rect + Upwell = 11515, // 233B->self, 3.0s cast, range 37+R ?-degree cone +} + +class Whitewater(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Whitewater1), new AOEShapeRect(40.5f, 3.5f)); +class Upwell(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Upwell), new AOEShapeCone(41, 15.Degrees())); +class SpiritBurst(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SpiritBurst), new AOEShapeCircle(6)); +class WaterDrop(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.WaterDrop), 6); + +class ExplosiveTataru(BossModule module) : BossComponent(module) +{ + private readonly List Balls = []; + private Actor? Tataru; + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == 3) + { + Balls.Add(source); + Tataru ??= WorldState.Actors.Find(tether.Target); + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.SpiritBurst) + { + Balls.Remove(caster); + if (Balls.Count == 0) + Tataru = null; + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (Tataru != null) + Arena.AddCircle(Tataru.Position, 6, ArenaColor.Danger); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Tataru != null) + hints.AddForbiddenZone(ShapeDistance.Circle(Tataru.Position, 6)); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Tataru != null && actor.Position.InCircle(Tataru.Position, 6)) + hints.Add("GTFO from Tataru!"); + } +} + +class Eddy(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Eddy1), 6); + +class ShieldHint(BossModule module) : BossComponent(module) +{ + private const float Radius = 7; + private Actor? Shield; + + public override void OnActorEState(Actor actor, ushort state) + { + if (actor.OID == 0x1EA9C7 && state == 2) + Shield = actor; + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (Shield is Actor s) + Arena.ZoneCircle(s.Position, Radius, ArenaColor.SafeFromAOE); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.GreatFlood1) + Shield = null; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Shield is Actor s) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(s.Position, Radius), Module.CastFinishAt(Module.PrimaryActor.CastInfo)); + } +} + +class SorobanStates : StateMachineBuilder +{ + public SorobanStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.Quest, GroupID = 68552, NameID = 7240)] +public class Soroban(WorldState ws, Actor primary) : BossModule(ws, primary, new(62, -372), new ArenaBoundsSquare(19)); + 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/CurveApprox.cs b/BossMod/Util/CurveApprox.cs index 5a225d7d3d..6c6723ec5a 100644 --- a/BossMod/Util/CurveApprox.cs +++ b/BossMod/Util/CurveApprox.cs @@ -94,6 +94,18 @@ public static WPos[] CircleSector(WPos center, float radius, Angle angleStart, A return points; } + public static IEnumerable Ellipse(float axis1, float axis2, float maxError) + { + int numSegments = CalculateCircleSegments((axis1 + axis2) / 2f, (2 * MathF.PI).Radians(), maxError); + var angle = (2 * MathF.PI / numSegments).Radians(); + for (int i = 0; i < numSegments; ++i) + { + var t = i * angle; + yield return new WDir(axis1 * t.Cos(), axis2 * t.Sin()); + } + } + public static IEnumerable Ellipse(WPos center, float axis1, float axis2, float maxError) => Ellipse(axis1, axis2, maxError).Select(off => center + off); + // return polygon points approximating full donut; implicitly closed path - outer arc + inner arc public static WDir[] Donut(float innerRadius, float outerRadius, float maxError) { 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