From 7809db987ff32ab2e968ec88c648a348ae93416f Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Mon, 30 Dec 2024 01:34:01 +0100 Subject: [PATCH] daivadipa refactor, ai movement delay setting --- BossMod/AI/AIBehaviour.cs | 19 +- BossMod/AI/AIConfig.cs | 3 + BossMod/AI/AIManagementWindow.cs | 15 + BossMod/Components/GenericAOEs.cs | 26 +- .../Dawntrail/Alliance/A11Prishe/A11Prishe.cs | 2 +- .../Dawntrail/Alliance/A12Fafnir/A12Fafnir.cs | 2 +- .../Alliance/A13ArkAngels/A13ArkAngels.cs | 2 +- .../Alliance/A14ShadowLord/A14ShadowLord.cs | 2 +- .../Alliance/A14ShadowLord/BindingSigil.cs | 16 +- .../Alliance/A14ShadowLord/DarkNebula.cs | 18 +- .../Alliance/A14ShadowLord/GigaSlash.cs | 2 +- .../Ch01CloudOfDarkness.cs | 2 +- BossMod/Modules/Endwalker/FATE/Daivadipa.cs | 259 ++++++++---------- BossMod/Pathfinding/NavigationDecision.cs | 1 + 14 files changed, 181 insertions(+), 188 deletions(-) diff --git a/BossMod/AI/AIBehaviour.cs b/BossMod/AI/AIBehaviour.cs index 5126f021c5..c6a5fe4fe6 100644 --- a/BossMod/AI/AIBehaviour.cs +++ b/BossMod/AI/AIBehaviour.cs @@ -20,6 +20,7 @@ sealed class AIBehaviour(AIController ctrl, RotationModuleManager autorot, Prese private WPos _masterPrevPos; private WPos _masterMovementStart; private DateTime _masterLastMoved; + private DateTime _navStartTime; // if current time is < this, navigation won't start public void Dispose() { @@ -40,6 +41,7 @@ public async Task Execute(Actor player, Actor master) var pyreticImminent = autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && autorot.Hints.ImminentSpecialMode.activation <= WorldState.FutureTime(1); var misdirectionMode = autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Misdirection && autorot.Hints.ImminentSpecialMode.activation <= WorldState.CurrentTime; var forbidActions = _config.ForbidActions || _afkMode || gazeImminent || pyreticImminent; + var hadNavi = _naviDecision.Destination != null; Targeting target = new(); if (!forbidActions && (AIPreset != null || autorot.Preset != null)) @@ -76,6 +78,10 @@ public async Task Execute(Actor player, Actor master) var masterIsMoving = TrackMasterMovement(master); var moveWithMaster = masterIsMoving && (master == player || _followMaster); ForceMovementIn = moveWithMaster || gazeImminent || pyreticImminent ? 0 : _naviDecision.LeewaySeconds; + + if (_config.MoveDelay != 0 && !hadNavi && _naviDecision.Destination != null) + _navStartTime = WorldState.FutureTime(_config.MoveDelay); + if (!forbidActions) { autorot.Preset = target.Target != null ? AIPreset : null; @@ -131,7 +137,7 @@ private void AdjustTargetPositional(Actor player, ref Targeting targeting) // if target-of-target is player, don't try flanking, it's probably impossible... - unless target is currently casting (TODO: reconsider?) // skip if targeting a dummy, they don't rotate - if (targeting.Target.Actor.TargetID == player.InstanceID && targeting.Target.Actor.CastInfo == null && targeting.Target.Actor.OID != 0x385) + if (targeting.Target.Actor.TargetID == player.InstanceID && targeting.Target.Actor.CastInfo == null && targeting.Target.Actor.NameID != 541) targeting.PreferredPosition = Positional.Any; } @@ -150,7 +156,10 @@ private void AdjustTargetPositional(Actor player, ref Targeting targeting) } if (_config.FollowTarget && target != null) { - var decision = await Task.Run(() => NavigationDecision.Build(_naviCtx, WorldState, autorot.Hints, player, target.Position, target.HitboxRadius + (_config.DesiredPositional != Positional.Any ? 2.6f : _config.MaxDistanceToTarget), target.Rotation, target != player ? _config.DesiredPositional : Positional.Any)).ConfigureAwait(true); + var positional = _config.DesiredPositional; + if (positional is not Positional.Any and not Positional.Front && target.TargetID == player.InstanceID && target.CastInfo == null && target.NameID != 541) // if player is target, rear/flank is usually impossible unless target is casting + positional = Positional.Any; + var decision = await Task.Run(() => NavigationDecision.Build(_naviCtx, WorldState, autorot.Hints, player, target.Position, target.HitboxRadius + (positional != Positional.Any ? 2.6f : _config.MaxDistanceToTarget), target.Rotation, positional)).ConfigureAwait(true); return (decision, targeting); } } @@ -243,7 +252,8 @@ private void UpdateMovement(Actor player, Actor master, Targeting target, bool g { var toDest = _naviDecision.Destination != null ? _naviDecision.Destination.Value - player.Position : new(); var distSq = toDest.LengthSq(); - ctrl.NaviTargetPos = _naviDecision.Destination; + + ctrl.NaviTargetPos = _config.MoveDelay == 0 ? _naviDecision.Destination : WorldState.CurrentTime >= _navStartTime ? _naviDecision.Destination : null; ctrl.NaviTargetVertical = master != player ? master.PosRot.Y : null; ctrl.AllowInterruptingCastByMovement = player.CastInfo != null && _naviDecision.LeewaySeconds <= player.CastInfo.RemainingTime - 0.5; ctrl.ForceCancelCast = false; @@ -290,7 +300,8 @@ public void DrawDebug() var player = autorot.WorldState.Party.Player(); var dist = _naviDecision.Destination != null && player != null ? (_naviDecision.Destination.Value - player.Position).Length() : 0; - ImGui.TextUnformatted($"Max-cast={Math.Min(ForceMovementIn, 1000):f3}, afk={_afkMode}, follow={_followMaster}, algo={_naviDecision.DecisionType} {_naviDecision.Destination} (d={dist:f3}), master standing for {Math.Clamp((WorldState.CurrentTime - _masterLastMoved).TotalSeconds, 0, 1000):f1}"); + var algo = WorldState.CurrentTime <= _navStartTime ? _naviDecision.DecisionType = NavigationDecision.Decision.Waiting : _naviDecision.DecisionType; + ImGui.TextUnformatted($"Max-cast={Math.Min(ForceMovementIn, 1000):f3}, afk={_afkMode}, follow={_followMaster}, algo={algo} {_naviDecision.Destination} (d={dist:f3}), master standing for {Math.Clamp((WorldState.CurrentTime - _masterLastMoved).TotalSeconds, 0, 1000):f1}"); } private bool TargetIsForbidden(ulong actorId) => autorot.Hints.ForbiddenTargets.Any(e => e.Actor.InstanceID == actorId); diff --git a/BossMod/AI/AIConfig.cs b/BossMod/AI/AIConfig.cs index c86106bad3..23e481486d 100644 --- a/BossMod/AI/AIConfig.cs +++ b/BossMod/AI/AIConfig.cs @@ -61,5 +61,8 @@ sealed class AIConfig : ConfigNode [PropertyDisplay("Allow AI to be out of pathfinding map bounds")] public bool AllowAIToBeOutsideBounds = false; + [PropertyDisplay("Movement decision delay", tooltip: "Only change this at your own risk and keep this value low! Too high and it won't move in time for some mechanics. Make sure to readjust the value for different content.")] + public float MoveDelay = 0; + public string? AIAutorotPresetName; } diff --git a/BossMod/AI/AIManagementWindow.cs b/BossMod/AI/AIManagementWindow.cs index de78b425bc..bdec5fa957 100644 --- a/BossMod/AI/AIManagementWindow.cs +++ b/BossMod/AI/AIManagementWindow.cs @@ -104,6 +104,21 @@ private Task UIAsync() _config.Modified.Fire(); } } + + ImGui.Text("Movement decision delay"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(100); + var movementDelayStr = _config.MoveDelay.ToString(CultureInfo.InvariantCulture); + if (ImGui.InputText("##MovementDelay", ref movementDelayStr, 64)) + { + movementDelayStr = movementDelayStr.Replace(',', '.'); + if (float.TryParse(movementDelayStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var delay)) + { + _config.MoveDelay = delay; + _config.Modified.Fire(); + } + } + ImGui.SameLine(); ImGui.Text("Autorotation AI preset"); ImGui.SameLine(); ImGui.SetNextItemWidth(250); diff --git a/BossMod/Components/GenericAOEs.cs b/BossMod/Components/GenericAOEs.cs index 699dba2ef5..e4aabf8147 100644 --- a/BossMod/Components/GenericAOEs.cs +++ b/BossMod/Components/GenericAOEs.cs @@ -33,12 +33,13 @@ public override void DrawArenaBackground(int pcSlot, Actor pc) } // self-targeted aoe that happens at the end of the cast -public class SelfTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, int maxCasts = int.MaxValue, uint color = 0) : GenericAOEs(module, aid) +public class SelfTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, float riskyAfterSeconds = 0, int maxCasts = int.MaxValue, uint color = 0) : GenericAOEs(module, aid) { public readonly AOEShape Shape = shape; public int MaxCasts = maxCasts; // used for staggered aoes, when showing all active would be pointless public uint Color = color; public bool Risky = true; // can be customized if needed + public float RiskyAfterSeconds = riskyAfterSeconds; // can be used to delay risky status of AOEs, so AI waits longer to dodge, if 0 it will just use the bool Risky public readonly List Casters = []; public IEnumerable ActiveCasters => Casters.Take(MaxCasts); @@ -53,7 +54,7 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) for (var i = 0; i < min; ++i) { var caster = Casters[i]; - AOEInstance aoeInstance = new(Shape, caster.Position, caster.CastInfo!.Rotation, Module.CastFinishAt(caster.CastInfo)); + AOEInstance aoeInstance = new(Shape, caster.Position, caster.CastInfo!.Rotation, Module.CastFinishAt(caster.CastInfo), Color, RiskyAfterSeconds == 0 ? Risky : caster.CastInfo.ElapsedTime > RiskyAfterSeconds); aoes.Add(aoeInstance); } return aoes; @@ -97,14 +98,15 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } // location-targeted circle aoe that happens at the end of the cast -public class LocationTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, int maxCasts = int.MaxValue, bool targetIsLocation = false) : GenericAOEs(module, aid) +public class LocationTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, float riskyWithSecondsLeft = 0, int maxCasts = int.MaxValue, bool targetIsLocation = false) : GenericAOEs(module, aid) { - public LocationTargetedAOEs(BossModule module, ActionID aid, float radius, int maxCasts = int.MaxValue, bool targetIsLocation = false) : this(module, aid, new AOEShapeCircle(radius), maxCasts, targetIsLocation) { } + public LocationTargetedAOEs(BossModule module, ActionID aid, float radius, float riskyWithSecondsLeft = 0, int maxCasts = int.MaxValue, bool targetIsLocation = false) : this(module, aid, new AOEShapeCircle(radius), riskyWithSecondsLeft, maxCasts, targetIsLocation) { } public readonly AOEShape Shape = shape; public readonly int MaxCasts = maxCasts; // used for staggered aoes, when showing all active would be pointless public uint Color; // can be customized if needed public bool Risky = true; // can be customized if needed public readonly bool TargetIsLocation = targetIsLocation; // can be customized if needed + public float RiskyWithSecondsLeft = riskyWithSecondsLeft; // can be used to delay risky status of AOEs, so AI waits longer to dodge, if 0 it will just use the bool Risky public readonly List Casters = []; public IEnumerable ActiveCasters => Casters.Take(MaxCasts); @@ -114,17 +116,18 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) var count = Casters.Count; if (count == 0) yield break; + var time = WorldState.CurrentTime; for (var i = 0; i < (count > MaxCasts ? MaxCasts : count); ++i) - yield return Casters[i]; + { + var caster = Casters[i]; + yield return RiskyWithSecondsLeft == 0 ? caster : caster with { Risky = caster.Activation.AddSeconds(-RiskyWithSecondsLeft) <= time }; + } } public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if (spell.Action == WatchedAction) - { - var loc = TargetIsLocation ? WorldState.Actors.Find(caster.CastInfo!.TargetID)?.Position : spell.LocXZ; - Casters.Add(new(Shape, loc ?? default, spell.Rotation, Module.CastFinishAt(spell), Color, Risky)); - } + Casters.Add(new(Shape, (TargetIsLocation ? WorldState.Actors.Find(caster.CastInfo!.TargetID)?.Position : spell.LocXZ) ?? default, spell.Rotation, Module.CastFinishAt(spell), Color, Risky)); } public override void OnCastFinished(Actor caster, ActorCastInfo spell) @@ -135,9 +138,10 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } // 'charge at location' aoes that happen at the end of the cast -public class ChargeAOEs(BossModule module, ActionID aid, float halfWidth) : GenericAOEs(module, aid) +public class ChargeAOEs(BossModule module, ActionID aid, float halfWidth, float riskyAfterSeconds = 0) : GenericAOEs(module, aid) { public readonly float HalfWidth = halfWidth; + public float RiskyAfterSeconds = riskyAfterSeconds; // can be used to delay risky status of AOEs, so AI waits longer to dodge, if 0 it will just use the bool Risky public readonly List<(Actor caster, AOEShape shape, Angle direction)> Casters = []; public override IEnumerable ActiveAOEs(int slot, Actor actor) @@ -149,7 +153,7 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) for (var i = 0; i < count; ++i) { var csr = Casters[i]; - AOEInstance aoeInstance = new(csr.shape, csr.caster.Position, csr.direction, Module.CastFinishAt(csr.caster.CastInfo)); + AOEInstance aoeInstance = new(csr.shape, csr.caster.Position, csr.direction, Module.CastFinishAt(csr.caster.CastInfo), Risky: csr.caster.CastInfo?.ElapsedTime > RiskyAfterSeconds); aoes.Add(aoeInstance); } return aoes; diff --git a/BossMod/Modules/Dawntrail/Alliance/A11Prishe/A11Prishe.cs b/BossMod/Modules/Dawntrail/Alliance/A11Prishe/A11Prishe.cs index 9c8f396aa2..9075623cfd 100644 --- a/BossMod/Modules/Dawntrail/Alliance/A11Prishe/A11Prishe.cs +++ b/BossMod/Modules/Dawntrail/Alliance/A11Prishe/A11Prishe.cs @@ -5,7 +5,7 @@ class Holy(BossModule module) : Components.SpreadFromCastTargets(module, ActionI class BanishgaIV(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.BanishgaIV)); class Banishga(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Banishga)); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13351, SortOrder = 2)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13351, SortOrder = 2, PlanLevel = 100)] public class A11Prishe(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, DefaultBounds) { public static readonly WPos ArenaCenter = new(800, 400); diff --git a/BossMod/Modules/Dawntrail/Alliance/A12Fafnir/A12Fafnir.cs b/BossMod/Modules/Dawntrail/Alliance/A12Fafnir/A12Fafnir.cs index 4262b36fff..4fcfcf99bb 100644 --- a/BossMod/Modules/Dawntrail/Alliance/A12Fafnir/A12Fafnir.cs +++ b/BossMod/Modules/Dawntrail/Alliance/A12Fafnir/A12Fafnir.cs @@ -46,7 +46,7 @@ class BalefulBreath(BossModule module) : Components.LineStack(module, (uint)Icon public override bool KeepOnPhaseChange => true; } -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13662, SortOrder = 4)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13662, SortOrder = 4, PlanLevel = 100)] public class A12Fafnir(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, new ArenaBoundsCircle(34.5f)) { public static readonly WPos ArenaCenter = new(-500, 600); diff --git a/BossMod/Modules/Dawntrail/Alliance/A13ArkAngels/A13ArkAngels.cs b/BossMod/Modules/Dawntrail/Alliance/A13ArkAngels/A13ArkAngels.cs index 4df2d3fddf..16ac97c064 100644 --- a/BossMod/Modules/Dawntrail/Alliance/A13ArkAngels/A13ArkAngels.cs +++ b/BossMod/Modules/Dawntrail/Alliance/A13ArkAngels/A13ArkAngels.cs @@ -18,7 +18,7 @@ class TachiGekko(BossModule module) : Components.CastGaze(module, ActionID.MakeS class Raiton(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Raiton)); class Utsusemi(BossModule module) : Components.StretchTetherSingle(module, (uint)TetherID.Utsusemi, 10, needToKite: true); -[ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.BossGK, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13640, SortOrder = 7)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.BossGK, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13640, SortOrder = 7, PlanLevel = 100)] public class A13ArkAngels(WorldState ws, Actor primary) : BossModule(ws, primary, new(865, -820), new ArenaBoundsCircle(34.5f)) { public static readonly ArenaBoundsCircle DefaultBounds = new(25); diff --git a/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/A14ShadowLord.cs b/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/A14ShadowLord.cs index 14a57f7e1a..5643d5f22c 100644 --- a/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/A14ShadowLord.cs +++ b/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/A14ShadowLord.cs @@ -6,7 +6,7 @@ class DoomArc(BossModule module) : Components.RaidwideCast(module, ActionID.Make class UnbridledRage(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeRect(100, 4), (uint)IconID.UnbridledRage, ActionID.MakeSpell(AID.UnbridledRageAOE), 5.9f); class DarkNova(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.DarkNova), 6); -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13653, SortOrder = 8)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1015, NameID = 13653, SortOrder = 8, PlanLevel = 100)] public class A14ShadowLord(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, DefaultBounds) { private const int RadiusSmall = 8; diff --git a/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/BindingSigil.cs b/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/BindingSigil.cs index 4f67a3c99a..865f7a6c9b 100644 --- a/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/BindingSigil.cs +++ b/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/BindingSigil.cs @@ -9,13 +9,15 @@ class BindingSigil(BossModule module) : Components.GenericAOEs(module) public override IEnumerable ActiveAOEs(int slot, Actor actor) { var count = _aoes.Count; - if (count > 0) - for (var i = 0; i < count; ++i) - { - var aoe = _aoes[i]; - if ((aoe.Activation - _aoes[0].Activation).TotalSeconds <= 1) - yield return aoe; - } + if (count == 0) + yield break; + + for (var i = 0; i < count; ++i) + { + var aoe = _aoes[i]; + if ((aoe.Activation - _aoes[0].Activation).TotalSeconds <= 1) + yield return aoe; + } } public override void OnCastStarted(Actor caster, ActorCastInfo spell) diff --git a/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/DarkNebula.cs b/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/DarkNebula.cs index 8f15fcac41..68343afb5d 100644 --- a/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/DarkNebula.cs +++ b/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/DarkNebula.cs @@ -19,17 +19,17 @@ class DarkNebula(BossModule module) : Components.Knockback(module) public override IEnumerable Sources(int slot, Actor actor) { var count = Casters.Count; - if (count != 0) + if (count == 0) + yield break; + + for (var i = 0; i < count; ++i) { - for (var i = 0; i < count; ++i) + if (i < 2) { - if (i < 2) - { - var caster = Casters[i]; - var dir = caster.CastInfo?.Rotation ?? caster.Rotation; - var kind = dir.ToDirection().OrthoL().Dot(actor.Position - caster.Position) > 0 ? Kind.DirLeft : Kind.DirRight; - yield return new(caster.Position, 20, Module.CastFinishAt(caster.CastInfo), null, dir, kind); - } + var caster = Casters[i]; + var dir = caster.CastInfo?.Rotation ?? caster.Rotation; + var kind = dir.ToDirection().OrthoL().Dot(actor.Position - caster.Position) > 0 ? Kind.DirLeft : Kind.DirRight; + yield return new(caster.Position, 20, Module.CastFinishAt(caster.CastInfo), null, dir, kind); } } } diff --git a/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/GigaSlash.cs b/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/GigaSlash.cs index 314b2c4b52..1952baeb14 100644 --- a/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/GigaSlash.cs +++ b/BossMod/Modules/Dawntrail/Alliance/A14ShadowLord/GigaSlash.cs @@ -9,7 +9,7 @@ class GigaSlash(BossModule module) : Components.GenericAOEs(module) public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (AOEs.Count > 0) + if (AOEs.Count != 0) yield return AOEs[0] with { Risky = Module.FindComponent()?.Casters.Count == 0 }; } diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs index 3a5ec728ad..c488be32b5 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs @@ -12,7 +12,7 @@ class LoomingChaos(BossModule module) : Components.CastCounter(module, ActionID. // TODO: tankswap hints component for phase1 // TODO: phase 2 squares, break timer, teleport zones -[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624)] +[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624, PlanLevel = 100)] public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultCenter, DefaultArena) { public static readonly WPos DefaultCenter = new(100, 100); diff --git a/BossMod/Modules/Endwalker/FATE/Daivadipa.cs b/BossMod/Modules/Endwalker/FATE/Daivadipa.cs index ed6b162d7a..34f53f64cb 100644 --- a/BossMod/Modules/Endwalker/FATE/Daivadipa.cs +++ b/BossMod/Modules/Endwalker/FATE/Daivadipa.cs @@ -15,235 +15,192 @@ public enum OID : uint public enum AID : uint { AutoAttack = 872, // Boss->player, no cast, single-target + Drumbeat = 26510, // Boss->player, 5.0s cast, single-target LeftwardTrisula = 26508, // Boss->self, 7.0s cast, range 65 180-degree cone RightwardParasu = 26509, // Boss->self, 7.0s cast, range 65 180-degree cone Lamplight = 26497, // Boss->self, 2.0s cast, single-target - LoyalFlame = 26499, // Boss->self, 5.0s cast, single-target, blue first - LoyalFlame2 = 26498, // Boss->self, 5.0s cast, single-target, red first - LitPath1 = 26501, // OrbOfImmolation->self, 1.0s cast, range 50 width 10 rect, blue orb - LitPath2 = 26500, // OrbOfImmolation2->self, 1.0s cast, range 50 width 10 rect, red orbs + LoyalFlameBlue = 26499, // Boss->self, 5.0s cast, single-target, blue first + LoyalFlameRed = 26498, // Boss->self, 5.0s cast, single-target, red first + LitPathBlue = 26501, // OrbOfImmolation->self, 1.0s cast, range 50 width 10 rect, blue orb + LitPathRed = 26500, // OrbOfImmolation2->self, 1.0s cast, range 50 width 10 rect, red orbs CosmicWeave = 26513, // Boss->self, 4.0s cast, range 18 circle - YawningHells = 26511, // Boss->self, no cast, single-target - YawningHells2 = 26512, // Helper1->location, 3.0s cast, range 8 circle + YawningHellsVisual = 26511, // Boss->self, no cast, single-target + YawningHells = 26512, // Helper1->location, 3.0s cast, range 8 circle ErrantAkasa = 26514, // Boss->self, 5.0s cast, range 60 90-degree cone - InfernalRedemption = 26517, // Boss->self, 5.0s cast, single-target - InfernalRedemption2 = 26518, // Helper3->location, no cast, range 60 circle - IgnitingLights = 26503, // Boss->self, 2.0s cast, single-target + InfernalRedemptionVisual = 26517, // Boss->self, 5.0s cast, single-target + InfernalRedemption = 26518, // Helper3->location, no cast, range 60 circle + IgnitingLights1 = 26503, // Boss->self, 2.0s cast, single-target IgnitingLights2 = 26502, // Boss->self, 2.0s cast, single-target - Burn = 26507, // OrbOfConflagration->self, 1.0s cast, range 10 circle, blue orbs - Burn2 = 26506, // OrbOfConflagration2->self, 1.0s cast, range 10 circle, red orbs - KarmicFlames = 26515, // Boss->self, 5.5s cast, single-target - KarmicFlames2 = 26516, // Helper2->location, 5.0s cast, range 50 circle, damage fall off, safe distance should be about 20 - DivineCall = 27080, // Boss->self, 4.0s cast, range 65 circle, forced backwards march + BurnBlue = 26507, // OrbOfConflagration->self, 1.0s cast, range 10 circle, blue orbs + BurnRed = 26506, // OrbOfConflagration2->self, 1.0s cast, range 10 circle, red orbs + KarmicFlamesVisual = 26515, // Boss->self, 5.5s cast, single-target + KarmicFlames = 26516, // Helper2->location, 5.0s cast, range 50 circle, damage fall off, safe distance should be about 20 + DivineCall1 = 27080, // Boss->self, 4.0s cast, range 65 circle, forced backwards march DivineCall2 = 26520, // Boss->self, 4.0s cast, range 65 circle, forced right march DivineCall3 = 27079, // Boss->self, 4.0s cast, range 65 circle, forced forward march - DivineCall4 = 26519, // Boss->self, 4.0s cast, range 65 circle, forced left march + DivineCall4 = 26519 // Boss->self, 4.0s cast, range 65 circle, forced left march } public enum SID : uint { - Hover = 1515, // none->OrbOfImmolation, extra=0x64 AboutFace = 1959, // Boss->player, extra=0x0 RightFace = 1961, // Boss->player, extra=0x0 ForwardMarch = 1958, // Boss->player, extra=0x0 LeftFace = 1960, // Boss->player, extra=0x0 - ForcedMarch = 1257, // Boss->player, extra=0x2/0x8/0x1/0x4 } class LitPath(BossModule module) : Components.GenericAOEs(module) { + public readonly List AOEs = new(5); private static readonly AOEShapeRect rect = new(50, 5); - private DateTime _activation; - private bool redblue1; - private bool redblue2; - private bool bluered1; - private bool bluered2; public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_activation != default) + var count = AOEs.Count; + if (count == 0) + yield break; + for (var i = 0; i < count; ++i) { - foreach (var o in Module.Enemies(OID.OrbOfImmolationBlue)) - { - if (bluered1 && (o.Rotation.AlmostEqual(90.Degrees(), Angle.DegToRad) || o.Rotation.AlmostEqual(180.Degrees(), Angle.DegToRad))) - yield return new(rect, o.Position, o.Rotation, _activation.AddSeconds(1.9f)); - if (redblue2 && !redblue1 && (o.Rotation.AlmostEqual(90.Degrees(), Angle.DegToRad) || o.Rotation.AlmostEqual(180.Degrees(), Angle.DegToRad))) - yield return new(rect, o.Position, o.Rotation, _activation.AddSeconds(4)); - } - foreach (var o in Module.Enemies(OID.OrbOfImmolationRed)) - { - if (bluered2 && !bluered1 && (o.Rotation.AlmostEqual(90.Degrees(), Angle.DegToRad) || o.Rotation.AlmostEqual(180.Degrees(), Angle.DegToRad))) - yield return new(rect, o.Position, o.Rotation, _activation.AddSeconds(4)); - if (redblue1 && (o.Rotation.AlmostEqual(90.Degrees(), Angle.DegToRad) || o.Rotation.AlmostEqual(180.Degrees(), Angle.DegToRad))) - yield return new(rect, o.Position, o.Rotation, _activation.AddSeconds(1.9f)); - } + var aoe = AOEs[i]; + if ((aoe.Activation - AOEs[0].Activation).TotalSeconds <= 1) + yield return aoe; } } public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if (!Module.Enemies(OID.OrbOfImmolationRed).All(x => x.IsDead) && !Module.Enemies(OID.OrbOfImmolationBlue).All(x => x.IsDead)) + if ((AID)spell.Action.ID is AID.LoyalFlameBlue or AID.LoyalFlameRed) { - if ((AID)spell.Action.ID == AID.LoyalFlame) - { - _activation = Module.CastFinishAt(spell); - bluered1 = true; - bluered2 = true; - } - else if ((AID)spell.Action.ID == AID.LoyalFlame2) - { - _activation = Module.CastFinishAt(spell); - redblue1 = true; - redblue2 = true; - } + var isBlue = (AID)spell.Action.ID == AID.LoyalFlameBlue; + AddAOEs(Module.Enemies(OID.OrbOfImmolationBlue), spell, isBlue ? 2.2f : 4.4f); + AddAOEs(Module.Enemies(OID.OrbOfImmolationRed), spell, isBlue ? 4.4f : 2.2f); + if (!isBlue) + AOEs.Reverse(); } } - public override void OnCastFinished(Actor caster, ActorCastInfo spell) + private void AddAOEs(List orbs, ActorCastInfo spell, float delay) { - if ((AID)spell.Action.ID == AID.LitPath1) - { - bluered1 = false; - redblue2 = false; - if (++NumCasts == 5) - { - NumCasts = 0; - _activation = default; - } - } - else if ((AID)spell.Action.ID == AID.LitPath2) + for (var i = 0; i < orbs.Count; ++i) { - bluered2 = false; - redblue1 = false; - if (++NumCasts == 5) - { - NumCasts = 0; - _activation = default; - } + var orb = orbs[i]; + AOEs.Add(new(rect, orb.Position, orb.Position.X < -632 ? Angle.AnglesCardinals[3] : Angle.AnglesCardinals[2], Module.CastFinishAt(spell, delay))); } } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (AOEs.Count != 0 && (AID)spell.Action.ID is AID.LitPathBlue or AID.LitPathRed) + AOEs.RemoveAt(0); + } } class Burn(BossModule module) : Components.GenericAOEs(module) { + private readonly List _aoes = new(16); private static readonly AOEShapeCircle circle = new(10); - private DateTime _activation; - private bool redblue1; - private bool redblue2; - private bool bluered1; - private bool bluered2; public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_activation != default) + var count = _aoes.Count; + if (count == 0) + yield break; + for (var i = 0; i < count; ++i) { - foreach (var o in Module.Enemies(OID.OrbOfConflagrationBlue)) - { - if (bluered1) - yield return new(circle, o.Position, default, _activation.AddSeconds(2.1f)); - if (redblue2 && !redblue1) - yield return new(circle, o.Position, default, _activation.AddSeconds(6.1f)); - } - foreach (var o in Module.Enemies(OID.OrbOfConflagrationRed)) - { - if (bluered2 && !bluered1) - yield return new(circle, o.Position, default, _activation.AddSeconds(6.1f)); - if (redblue1) - yield return new(circle, o.Position, default, _activation.AddSeconds(2.1f)); - } + var aoe = _aoes[i]; + if ((aoe.Activation - _aoes[0].Activation).TotalSeconds <= 1) + yield return aoe; } } public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if (!Module.Enemies(OID.OrbOfConflagrationRed).All(x => x.IsDead) && !Module.Enemies(OID.OrbOfConflagrationBlue).All(x => x.IsDead)) + if ((AID)spell.Action.ID is AID.LoyalFlameBlue or AID.LoyalFlameRed) { - if ((AID)spell.Action.ID == AID.LoyalFlame) - { - _activation = Module.CastFinishAt(spell); - bluered1 = true; - bluered2 = true; - } - else if ((AID)spell.Action.ID == AID.LoyalFlame2) - { - _activation = Module.CastFinishAt(spell); - redblue1 = true; - redblue2 = true; - } + var isBlue = (AID)spell.Action.ID == AID.LoyalFlameBlue; + AddAOEs(Module.Enemies(OID.OrbOfConflagrationBlue), spell, isBlue ? 2.2f : 6.2f); + AddAOEs(Module.Enemies(OID.OrbOfConflagrationRed), spell, isBlue ? 6.2f : 2.2f); + if (!isBlue) + _aoes.Reverse(); } } - public override void OnCastFinished(Actor caster, ActorCastInfo spell) + private void AddAOEs(List orbs, ActorCastInfo spell, float delay) { - if ((AID)spell.Action.ID == AID.Burn) - { - bluered1 = false; - redblue2 = false; - if (++NumCasts == 16) - { - NumCasts = 0; - _activation = default; - } - } - else if ((AID)spell.Action.ID == AID.Burn2) + for (var i = 0; i < orbs.Count; ++i) { - bluered2 = false; - redblue1 = false; - ++NumCasts; - if (++NumCasts == 16) - { - NumCasts = 0; - _activation = default; - } + var orb = orbs[i]; + _aoes.Add(new(circle, orb.Position, default, Module.CastFinishAt(spell, delay))); } } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (_aoes.Count != 0 && (AID)spell.Action.ID is AID.BurnBlue or AID.BurnRed) + _aoes.RemoveAt(0); + } } class Drumbeat(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Drumbeat)); -class LeftwardTrisula(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LeftwardTrisula), new AOEShapeCone(65, 90.Degrees())); -class RightwardParasu(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightwardParasu), new AOEShapeCone(65, 90.Degrees())); + +abstract class Cleave(BossModule module, AID aid) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCone(65, 90.Degrees())); +class LeftwardTrisula(BossModule module) : Cleave(module, AID.LeftwardTrisula); +class RightwardParasu(BossModule module) : Cleave(module, AID.RightwardParasu); + class ErrantAkasa(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ErrantAkasa), new AOEShapeCone(60, 45.Degrees())); class CosmicWeave(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CosmicWeave), new AOEShapeCircle(18)); -class KarmicFlames(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KarmicFlames2), new AOEShapeCircle(20)); -class YawningHells(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.YawningHells2), 8); -class InfernalRedemption(BossModule module) : Components.RaidwideCastDelay(module, ActionID.MakeSpell(AID.InfernalRedemption), ActionID.MakeSpell(AID.InfernalRedemption2), 1); +class KarmicFlames(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.KarmicFlames), new AOEShapeCircle(20)); +class YawningHells(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.YawningHells), 8); +class InfernalRedemption(BossModule module) : Components.RaidwideCastDelay(module, ActionID.MakeSpell(AID.InfernalRedemptionVisual), ActionID.MakeSpell(AID.InfernalRedemption), 1); class DivineCall(BossModule module) : Components.StatusDrivenForcedMarch(module, 2, (uint)SID.ForwardMarch, (uint)SID.AboutFace, (uint)SID.LeftFace, (uint)SID.RightFace) { + private readonly LitPath _lit = module.FindComponent()!; + private readonly LeftwardTrisula _aoe1 = module.FindComponent()!; + private readonly RightwardParasu _aoe2 = module.FindComponent()!; + + private static readonly Dictionary directionhints = new() + { + { AID.DivineCall1, "Apply backwards march debuff" }, + { AID.DivineCall2, "Apply right march debuff" }, + { AID.DivineCall3, "Apply forwards march debuff" }, + { AID.DivineCall4, "Apply left march debuff" } + }; + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) { - return (Module.FindComponent()?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false) || - (Module.FindComponent()?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false) || - Module.FindComponent() is var burn && burn != null && burn.ActiveAOEs(slot, actor).Any() && !burn.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) || - Module.FindComponent() is var lit && lit != null && lit.ActiveAOEs(slot, actor).Any() && !lit.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)); + return _aoe1.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) || + _aoe2.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) || + _lit.AOEs.Count != 0 && !_lit.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)); } public override void AddGlobalHints(GlobalHints hints) { - if (Module.PrimaryActor.CastInfo?.IsSpell(AID.DivineCall) ?? false) - hints.Add("Apply backwards march debuff"); - else if (Module.PrimaryActor.CastInfo?.IsSpell(AID.DivineCall2) ?? false) - hints.Add("Apply right march debuff"); - else if (Module.PrimaryActor.CastInfo?.IsSpell(AID.DivineCall3) ?? false) - hints.Add("Apply forwards march debuff"); - else if (Module.PrimaryActor.CastInfo?.IsSpell(AID.DivineCall4) ?? false) - hints.Add("Apply left march debuff"); + if (Module.PrimaryActor.CastInfo != null) + foreach (var entry in directionhints) + { + if (Module.PrimaryActor.CastInfo.IsSpell(entry.Key)) + { + hints.Add(entry.Value); + break; + } + } } public override void AddHints(int slot, Actor actor, TextHints hints) { - var forward = actor.FindStatus(SID.ForwardMarch) != null; - var left = actor.FindStatus(SID.LeftFace) != null; - var right = actor.FindStatus(SID.RightFace) != null; - var backwards = actor.FindStatus(SID.AboutFace) != null; - var marching = actor.FindStatus(SID.ForcedMarch) != null; - var last = ForcedMovements(actor).LastOrDefault(); - if (DestinationUnsafe(slot, actor, last.to) && !marching && (forward || left || right || backwards) && ((Module.FindComponent()?.ActiveAOEs(slot, actor).Any() ?? false) || (Module.FindComponent()?.ActiveAOEs(slot, actor).Any() ?? false))) - hints.Add("Aim into AOEs!"); - else if (!marching) + const string hint = "Aim into AOEs!"; + var movements = ForcedMovements(actor).ToList(); + if (movements.Count == 0) + return; + if (_aoe1.Casters.Count != 0 || _aoe2.Casters.Count != 0) base.AddHints(slot, actor, hints); - + else if (_lit.AOEs.Count != 0) + if (DestinationUnsafe(slot, actor, movements.LastOrDefault().to)) + hints.Add(hint); + else + hints.Add(hint, false); } } @@ -255,14 +212,14 @@ public DaivadipaStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter(); + .ActivateOnEnter() + .ActivateOnEnter(); } } diff --git a/BossMod/Pathfinding/NavigationDecision.cs b/BossMod/Pathfinding/NavigationDecision.cs index 15c2c97928..47a67b178d 100644 --- a/BossMod/Pathfinding/NavigationDecision.cs +++ b/BossMod/Pathfinding/NavigationDecision.cs @@ -31,6 +31,7 @@ public enum Decision UptimeBlocked, BackIntoBounds, Casting, + Waiting, Optimal }