From 7daaea085c6844240ee3e92de2ece843e9cd6bb2 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Thu, 19 Dec 2024 22:07:49 +0000 Subject: [PATCH 01/12] Planner coordinates support for AI movement. --- BossMod/Autorotation/Plan.cs | 8 ++++++ BossMod/Autorotation/Preset.cs | 8 ++++++ BossMod/Autorotation/RotationModule.cs | 1 + BossMod/Autorotation/RotationModuleManager.cs | 7 +++++ BossMod/Autorotation/Strategy.cs | 4 +++ BossMod/Autorotation/UIStrategyValue.cs | 21 ++++++++++++-- .../Modules/Dawntrail/Ultimate/FRU/FRUAI.cs | 28 +++++++++---------- 7 files changed, 60 insertions(+), 17 deletions(-) diff --git a/BossMod/Autorotation/Plan.cs b/BossMod/Autorotation/Plan.cs index cd1f6f2203..58d2a09cf1 100644 --- a/BossMod/Autorotation/Plan.cs +++ b/BossMod/Autorotation/Plan.cs @@ -172,6 +172,10 @@ private void ReadEntryFields(ref Plan.Entry entry, in JsonElement jelem) entry.Value.Target = Enum.Parse(jtarget.GetString() ?? ""); if (jelem.TryGetProperty(nameof(StrategyValue.TargetParam), out var jtp)) entry.Value.TargetParam = jtp.GetInt32(); + if (jelem.TryGetProperty(nameof(StrategyValue.Offset1), out var joff1)) + entry.Value.Offset1 = joff1.GetSingle(); + if (jelem.TryGetProperty(nameof(StrategyValue.Offset2), out var joff2)) + entry.Value.Offset2 = joff2.GetSingle(); if (jelem.TryGetProperty(nameof(StrategyValue.Comment), out var jcomment)) entry.Value.Comment = jcomment.GetString() ?? ""; } @@ -189,6 +193,10 @@ private void WriteEntryFields(Utf8JsonWriter writer, in Plan.Entry entry) writer.WriteString(nameof(StrategyValue.Target), entry.Value.Target.ToString()); if (entry.Value.TargetParam != 0) writer.WriteNumber(nameof(StrategyValue.TargetParam), entry.Value.TargetParam); + if (entry.Value.Offset1 != 0) + writer.WriteNumber(nameof(StrategyValue.Offset1), entry.Value.Offset1); + if (entry.Value.Offset2 != 0) + writer.WriteNumber(nameof(StrategyValue.Offset2), entry.Value.Offset2); if (entry.Value.Comment.Length > 0) writer.WriteString(nameof(StrategyValue.Comment), entry.Value.Comment); } diff --git a/BossMod/Autorotation/Preset.cs b/BossMod/Autorotation/Preset.cs index fa079c2991..be1ae7e45b 100644 --- a/BossMod/Autorotation/Preset.cs +++ b/BossMod/Autorotation/Preset.cs @@ -112,6 +112,10 @@ public class JsonPresetConverter : JsonConverter s.Value.Target = Enum.Parse(jtarget.GetString() ?? ""); if (js.TryGetProperty(nameof(StrategyValue.TargetParam), out var jtp)) s.Value.TargetParam = jtp.GetInt32(); + if (js.TryGetProperty(nameof(StrategyValue.Offset1), out var joff1)) + s.Value.Offset1 = joff1.GetSingle(); + if (js.TryGetProperty(nameof(StrategyValue.Offset2), out var joff2)) + s.Value.Offset2 = joff2.GetSingle(); if (js.TryGetProperty(nameof(StrategyValue.Comment), out var jcomment)) s.Value.Comment = jcomment.GetString() ?? ""; @@ -144,6 +148,10 @@ public override void Write(Utf8JsonWriter writer, Preset value, JsonSerializerOp writer.WriteString(nameof(StrategyValue.Target), s.Value.Target.ToString()); if (s.Value.TargetParam != 0) writer.WriteNumber(nameof(StrategyValue.TargetParam), s.Value.TargetParam); + if (s.Value.Offset1 != 0) + writer.WriteNumber(nameof(StrategyValue.Offset1), s.Value.Offset1); + if (s.Value.Offset2 != 0) + writer.WriteNumber(nameof(StrategyValue.Offset2), s.Value.Offset2); if (s.Value.Comment.Length > 0) writer.WriteString(nameof(StrategyValue.Comment), s.Value.Comment); writer.WriteEndObject(); diff --git a/BossMod/Autorotation/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index a5b881061b..01b7f1661c 100644 --- a/BossMod/Autorotation/RotationModule.cs +++ b/BossMod/Autorotation/RotationModule.cs @@ -107,6 +107,7 @@ public bool TraitUnlocked(uint id) // utility to resolve the target overrides; returns null on failure - in this case module is expected to run smart-targeting logic // expected usage is `ResolveTargetOverride(strategy) ?? CustomSmartTargetingLogic(...)` protected Actor? ResolveTargetOverride(in StrategyValue strategy) => Manager.ResolveTargetOverride(strategy.Target, strategy.TargetParam); + protected WPos ResolveTargetLocation(in StrategyValue strategy) => Manager.ResolveTargetLocation(strategy.Target, strategy.TargetParam, strategy.Offset1, strategy.Offset2); protected float StatusDuration(DateTime expireAt) => Math.Max((float)(expireAt - World.CurrentTime).TotalSeconds, 0.0f); diff --git a/BossMod/Autorotation/RotationModuleManager.cs b/BossMod/Autorotation/RotationModuleManager.cs index 291cb91ce9..875ae55e24 100644 --- a/BossMod/Autorotation/RotationModuleManager.cs +++ b/BossMod/Autorotation/RotationModuleManager.cs @@ -116,6 +116,13 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) _ => null }; + public WPos ResolveTargetLocation(StrategyTarget strategy, int param, float off1, float off2) => strategy switch + { + StrategyTarget.PointAbsolute => new(off1, off2), + StrategyTarget.PointCenter => (Bossmods.ActiveModule?.Center ?? Player?.Position ?? default) + off1 * off2.Degrees().ToDirection(), + _ => (ResolveTargetOverride(strategy, param)?.Position ?? Player?.Position ?? default) + off1 * off2.Degrees().ToDirection(), + }; + private Plan? CalculateExpectedPlan() { var player = Player; diff --git a/BossMod/Autorotation/Strategy.cs b/BossMod/Autorotation/Strategy.cs index d27d8c06ed..06e9cb7242 100644 --- a/BossMod/Autorotation/Strategy.cs +++ b/BossMod/Autorotation/Strategy.cs @@ -9,6 +9,8 @@ public enum StrategyTarget PartyWithLowestHP, // parameter is whether self is allowed (1) or not (0) EnemyWithHighestPriority, // selects closest if there are multiple 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 Count } @@ -49,6 +51,8 @@ public record struct StrategyValue() public float PriorityOverride = float.NaN; // priority override for the action controlled by the config; not all configs support it, if not set the default priority is used public StrategyTarget Target; // target selection strategy public int TargetParam; // strategy-specific parameter + public float Offset1; // x or r coordinate + public float Offset2; // y or phi coordinate public string Comment = ""; // user-editable comment string public float ExpireIn = float.MaxValue; // time until strategy expires } diff --git a/BossMod/Autorotation/UIStrategyValue.cs b/BossMod/Autorotation/UIStrategyValue.cs index 116173057e..550a6878b4 100644 --- a/BossMod/Autorotation/UIStrategyValue.cs +++ b/BossMod/Autorotation/UIStrategyValue.cs @@ -34,7 +34,8 @@ public static string PreviewTarget(ref StrategyValue value, BossModuleRegistry.I StrategyTarget.EnemyByOID => $"{(moduleInfo?.ObjectIDType != null ? Enum.ToObject(moduleInfo.ObjectIDType, (uint)value.TargetParam).ToString() : "???")} (0x{value.TargetParam:X})", _ => "" }; - return targetDetails.Length > 0 ? $"{value.Target} ({targetDetails})" : $"{value.Target}"; + var offsetDetails = value.Target == StrategyTarget.PointAbsolute ? $" {value.Offset1}x{value.Offset2}" : value.Offset1 != 0 ? $" + R{value.Offset1}, dir={value.Offset2}" : ""; + return (targetDetails.Length > 0 ? $"{value.Target} ({targetDetails})" : $"{value.Target}") + offsetDetails; } public static bool DrawEditor(ref StrategyValue value, StrategyConfig cfg, BossModuleRegistry.Info? moduleInfo, int? level) @@ -186,16 +187,32 @@ public static bool DrawEditorTarget(ref StrategyValue value, ActionTargets suppo } break; } + + if (supportedTargets.HasFlag(ActionTargets.Area)) + { + if (value.Target == StrategyTarget.PointAbsolute) + { + modified |= ImGui.InputFloat("X", ref value.Offset1); + modified |= ImGui.InputFloat("Y", ref value.Offset2); + } + else + { + modified |= ImGui.DragFloat("Offset", ref value.Offset1, 0.1f, 0, 30); + modified |= ImGui.DragFloat("Direction", ref value.Offset2, 1, -180, 180); + } + } + return modified; } - public static bool AllowTarget(StrategyTarget t, ActionTargets supported, BossModuleRegistry.Info? moduleInfo) => t switch + public static bool AllowTarget(StrategyTarget t, ActionTargets supported, BossModuleRegistry.Info? moduleInfo) => supported.HasFlag(ActionTargets.Area) || t switch { StrategyTarget.Self => supported.HasFlag(ActionTargets.Self), StrategyTarget.PartyByAssignment => supported.HasFlag(ActionTargets.Party), StrategyTarget.PartyWithLowestHP => supported.HasFlag(ActionTargets.Party), StrategyTarget.EnemyWithHighestPriority => supported.HasFlag(ActionTargets.Hostile), StrategyTarget.EnemyByOID => supported.HasFlag(ActionTargets.Hostile) && moduleInfo != null, + StrategyTarget.PointAbsolute or StrategyTarget.PointCenter => false, _ => true }; } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs index bd59317702..99a1e1168c 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs @@ -8,7 +8,7 @@ namespace BossMod.Dawntrail.Ultimate.FRU; sealed class FRUAI(RotationModuleManager manager, Actor player) : AIRotationModule(manager, player) { public enum Track { Movement } - public enum MovementStrategy { None, Pathfind, Prepull, DragToCenter, MaxMeleeNearest, ClockSpot } + public enum MovementStrategy { None, Pathfind, Explicit, Prepull, DragToCenter, MaxMeleeNearest } public static RotationModuleDefinition Definition() { @@ -16,10 +16,10 @@ public static RotationModuleDefinition Definition() res.Define(Track.Movement).As("Movement", "Movement") .AddOption(MovementStrategy.None, "None", "No automatic movement") .AddOption(MovementStrategy.Pathfind, "Pathfind", "Use standard pathfinding to move") + .AddOption(MovementStrategy.Explicit, "Explicit", "Move to specific point", supportedTargets: ActionTargets.Area) .AddOption(MovementStrategy.Prepull, "Prepull", "Pre-pull position: as close to the clock-spot as possible") .AddOption(MovementStrategy.DragToCenter, "DragToCenter", "Drag boss to the arena center") - .AddOption(MovementStrategy.MaxMeleeNearest, "MaxMeleeNearest", "Move to nearest spot in max-melee") - .AddOption(MovementStrategy.ClockSpot, "ClockSpot", "Move to role-based clock-spot"); + .AddOption(MovementStrategy.MaxMeleeNearest, "MaxMeleeNearest", "Move to nearest spot in max-melee"); return res; } @@ -29,17 +29,17 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa { if (Bossmods.ActiveModule is FRU module && module.Raid.FindSlot(Player.InstanceID) is var playerSlot && playerSlot >= 0) { - SetForcedMovement(CalculateDestination(module, primaryTarget, strategy.Option(Track.Movement).As(), Service.Config.Get()[module.Raid.Members[playerSlot].ContentId])); + SetForcedMovement(CalculateDestination(module, primaryTarget, strategy.Option(Track.Movement), Service.Config.Get()[module.Raid.Members[playerSlot].ContentId])); } } - private WPos CalculateDestination(FRU module, Actor? primaryTarget, MovementStrategy strategy, PartyRolesConfig.Assignment assignment) => strategy switch + private WPos CalculateDestination(FRU module, Actor? primaryTarget, StrategyValues.OptionRef strategy, PartyRolesConfig.Assignment assignment) => strategy.As() switch { MovementStrategy.Pathfind => PathfindPosition(), + MovementStrategy.Explicit => ResolveTargetLocation(strategy.Value), MovementStrategy.Prepull => PrepullPosition(module, assignment), MovementStrategy.DragToCenter => DragToCenterPosition(module), MovementStrategy.MaxMeleeNearest => primaryTarget != null ? primaryTarget.Position + 7.5f * (Player.Position - primaryTarget.Position).Normalized() : Player.Position, - MovementStrategy.ClockSpot => ClockSpotPosition(module, assignment, 6), _ => Player.Position }; @@ -54,11 +54,15 @@ private WPos PathfindPosition() private WPos PrepullPosition(FRU module, PartyRolesConfig.Assignment assignment) { var safeRange = 12.5f; - var pos = ClockSpotPosition(module, assignment, assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.M1 or PartyRolesConfig.Assignment.M2 ? 5 : 10); - var off = pos - module.PrimaryActor.Position; + var desiredRange = assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.M1 or PartyRolesConfig.Assignment.M2 ? 5 : 10; + var dir = _config.P1CyclonicBreakSpots[assignment]; + if (dir < 0) + dir = 0; + var desiredPos = module.Center + desiredRange * (180 - 45 * dir).Degrees().ToDirection(); + var off = desiredPos - module.PrimaryActor.Position; var distSq = off.LengthSq(); if (distSq >= safeRange * safeRange) - return pos; + return desiredPos; off /= MathF.Sqrt(distSq); return module.PrimaryActor.Position + off * safeRange; } @@ -76,10 +80,4 @@ private WPos DragToCenterPosition(FRU module) var timeToMelee = ((dragSpot - module.PrimaryActor.Position).Length() - meleeDistance) / (Speed() + 8.5f); // assume 8.5 boss speed... return GCD > timeToMelee + 0.1f ? dragSpot : module.PrimaryActor.Position + meleeDistance * dragDir; } - - private WPos ClockSpotPosition(FRU module, PartyRolesConfig.Assignment assignment, float range) - { - var dir = _config.P1CyclonicBreakSpots[assignment]; - return dir >= 0 ? module.Center + range * (180 - 45 * dir).Degrees().ToDirection() : Player.Position; - } } From 1c8795ff51899cce6376108aea84e592170f8366 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Thu, 19 Dec 2024 22:11:36 +0000 Subject: [PATCH 02/12] CS rollup --- FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFXIVClientStructs b/FFXIVClientStructs index 5a54abcf87..7865bf8a4e 160000 --- a/FFXIVClientStructs +++ b/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 5a54abcf878b101868f6fc3a16e53118606aaf29 +Subproject commit 7865bf8a4ee11ffaf0ccf41c0882a641fc12454f From 03560d2976b9a8d696e6d0ee24184574ec145553 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Fri, 20 Dec 2024 09:09:32 +0000 Subject: [PATCH 03/12] Small tweaks. --- BossMod/AI/AIController.cs | 3 +-- BossMod/AI/AIRotationModule.cs | 2 +- BossMod/Autorotation/RotationModuleManager.cs | 2 +- BossMod/Autorotation/UIStrategyValue.cs | 2 +- BossMod/Autorotation/xan/Healers/SCH.cs | 2 +- BossMod/BossModule/BossModuleMainWindow.cs | 4 ++-- BossMod/Debug/DebugInput.cs | 3 +-- BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs | 6 ++---- BossMod/Util/WPosDir.cs | 6 ++++-- 9 files changed, 14 insertions(+), 16 deletions(-) diff --git a/BossMod/AI/AIController.cs b/BossMod/AI/AIController.cs index b31c380346..16b8a1579f 100644 --- a/BossMod/AI/AIController.cs +++ b/BossMod/AI/AIController.cs @@ -51,8 +51,7 @@ public void Update(Actor? player, AIHints hints, DateTime now) bool forbidMovement = moveRequested || !AllowInterruptingCastByMovement && _amex.MoveMightInterruptCast; if (NaviTargetPos != null && !forbidMovement && (NaviTargetPos.Value - player.Position).LengthSq() > 0.01f) { - var y = NaviTargetVertical != null && IsVerticalAllowed ? NaviTargetVertical.Value : player.PosRot.Y; - desiredPosition = new(NaviTargetPos.Value.X, y, NaviTargetPos.Value.Z); + desiredPosition = NaviTargetPos.Value.ToVec3(NaviTargetVertical != null && IsVerticalAllowed ? NaviTargetVertical.Value : player.PosRot.Y); } else { diff --git a/BossMod/AI/AIRotationModule.cs b/BossMod/AI/AIRotationModule.cs index cdb335551e..6f4e1f17a6 100644 --- a/BossMod/AI/AIRotationModule.cs +++ b/BossMod/AI/AIRotationModule.cs @@ -19,7 +19,7 @@ protected bool InMeleeRange(Actor target) protected void SetForcedMovement(WPos? pos, float tolerance = 0.1f) { var dir = (pos ?? Player.Position) - Player.Position; - Hints.ForcedMovement = dir.LengthSq() > tolerance * tolerance ? new(dir.X, Player.PosRot.Y, dir.Z) : default; + Hints.ForcedMovement = dir.LengthSq() > tolerance * tolerance ? dir.ToVec3(Player.PosRot.Y) : default; } protected WPos ClosestInRange(WPos pos, WPos target, float maxRange) diff --git a/BossMod/Autorotation/RotationModuleManager.cs b/BossMod/Autorotation/RotationModuleManager.cs index 875ae55e24..42726516a4 100644 --- a/BossMod/Autorotation/RotationModuleManager.cs +++ b/BossMod/Autorotation/RotationModuleManager.cs @@ -119,7 +119,7 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) public WPos ResolveTargetLocation(StrategyTarget strategy, int param, float off1, float off2) => strategy switch { StrategyTarget.PointAbsolute => new(off1, off2), - StrategyTarget.PointCenter => (Bossmods.ActiveModule?.Center ?? Player?.Position ?? default) + off1 * off2.Degrees().ToDirection(), + StrategyTarget.PointCenter or StrategyTarget.Automatic => (Bossmods.ActiveModule?.Center ?? Player?.Position ?? default) + off1 * off2.Degrees().ToDirection(), _ => (ResolveTargetOverride(strategy, param)?.Position ?? Player?.Position ?? default) + off1 * off2.Degrees().ToDirection(), }; diff --git a/BossMod/Autorotation/UIStrategyValue.cs b/BossMod/Autorotation/UIStrategyValue.cs index 550a6878b4..20f2d088a1 100644 --- a/BossMod/Autorotation/UIStrategyValue.cs +++ b/BossMod/Autorotation/UIStrategyValue.cs @@ -193,7 +193,7 @@ public static bool DrawEditorTarget(ref StrategyValue value, ActionTargets suppo if (value.Target == StrategyTarget.PointAbsolute) { modified |= ImGui.InputFloat("X", ref value.Offset1); - modified |= ImGui.InputFloat("Y", ref value.Offset2); + modified |= ImGui.InputFloat("Z", ref value.Offset2); } else { diff --git a/BossMod/Autorotation/xan/Healers/SCH.cs b/BossMod/Autorotation/xan/Healers/SCH.cs index b679545059..f0d8bdac0c 100644 --- a/BossMod/Autorotation/xan/Healers/SCH.cs +++ b/BossMod/Autorotation/xan/Healers/SCH.cs @@ -160,7 +160,7 @@ void autoplace() if (FairyOrder != PetOrder.Place && (Player.InCombat || CountdownRemaining > 0)) { if (Bossmods.ActiveModule?.Arena.Center is WPos p) - Hints.ActionsToExecute.Push(new ActionID(ActionType.PetAction, 3), null, ActionQueue.Priority.VeryHigh, targetPos: new(p.X, Player.PosRot.Y, p.Z)); + Hints.ActionsToExecute.Push(new ActionID(ActionType.PetAction, 3), null, ActionQueue.Priority.VeryHigh, targetPos: p.ToVec3(Player.PosRot.Y)); } } diff --git a/BossMod/BossModule/BossModuleMainWindow.cs b/BossMod/BossModule/BossModuleMainWindow.cs index d5e5ddeb3b..d07706d3d1 100644 --- a/BossMod/BossModule/BossModuleMainWindow.cs +++ b/BossMod/BossModule/BossModuleMainWindow.cs @@ -95,8 +95,8 @@ private void DrawMovementHints(BossComponent.MovementHints? arrows, float y) foreach ((var start, var end, uint color) in arrows) { - Vector3 start3 = new(start.X, y, start.Z); - Vector3 end3 = new(end.X, y, end.Z); + Vector3 start3 = start.ToVec3(y); + Vector3 end3 = end.ToVec3(y); Camera.Instance.DrawWorldLine(start3, end3, color); var dir = Vector3.Normalize(end3 - start3); var arrowStart = end3 - 0.4f * dir; diff --git a/BossMod/Debug/DebugInput.cs b/BossMod/Debug/DebugInput.cs index 7ade1f1192..91aba5a443 100644 --- a/BossMod/Debug/DebugInput.cs +++ b/BossMod/Debug/DebugInput.cs @@ -171,8 +171,7 @@ public void Draw() } if (_wannaMove) { - var dir = _moveDir.Degrees().ToDirection(); - _move.DesiredDirection = new(dir.X, 0, dir.Z); + _move.DesiredDirection = _moveDir.Degrees().ToDirection().ToVec3(); } //_navi.Update(player); diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs index 99a1e1168c..13b15c2c49 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs @@ -8,7 +8,7 @@ namespace BossMod.Dawntrail.Ultimate.FRU; sealed class FRUAI(RotationModuleManager manager, Actor player) : AIRotationModule(manager, player) { public enum Track { Movement } - public enum MovementStrategy { None, Pathfind, Explicit, Prepull, DragToCenter, MaxMeleeNearest } + public enum MovementStrategy { None, Pathfind, Explicit, Prepull, DragToCenter } public static RotationModuleDefinition Definition() { @@ -18,8 +18,7 @@ public static RotationModuleDefinition Definition() .AddOption(MovementStrategy.Pathfind, "Pathfind", "Use standard pathfinding to move") .AddOption(MovementStrategy.Explicit, "Explicit", "Move to specific point", supportedTargets: ActionTargets.Area) .AddOption(MovementStrategy.Prepull, "Prepull", "Pre-pull position: as close to the clock-spot as possible") - .AddOption(MovementStrategy.DragToCenter, "DragToCenter", "Drag boss to the arena center") - .AddOption(MovementStrategy.MaxMeleeNearest, "MaxMeleeNearest", "Move to nearest spot in max-melee"); + .AddOption(MovementStrategy.DragToCenter, "DragToCenter", "Drag boss to the arena center"); return res; } @@ -39,7 +38,6 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa MovementStrategy.Explicit => ResolveTargetLocation(strategy.Value), MovementStrategy.Prepull => PrepullPosition(module, assignment), MovementStrategy.DragToCenter => DragToCenterPosition(module), - MovementStrategy.MaxMeleeNearest => primaryTarget != null ? primaryTarget.Position + 7.5f * (Player.Position - primaryTarget.Position).Normalized() : Player.Position, _ => Player.Position }; diff --git a/BossMod/Util/WPosDir.cs b/BossMod/Util/WPosDir.cs index 3d2ebb0ea7..e1745a7dca 100644 --- a/BossMod/Util/WPosDir.cs +++ b/BossMod/Util/WPosDir.cs @@ -5,8 +5,8 @@ public record struct WDir(float X, float Z) { public WDir(Vector2 v) : this(v.X, v.Y) { } public readonly Vector2 ToVec2() => new(X, Z); - public readonly Vector3 ToVec3() => new(X, 0, Z); - public readonly Vector4 ToVec4() => new(X, 0, Z, 0); + public readonly Vector3 ToVec3(float y = 0) => new(X, y, Z); + public readonly Vector4 ToVec4(float y = 0, float w = 0) => new(X, y, Z, w); public readonly WPos ToWPos() => new(X, Z); public static WDir operator +(WDir a, WDir b) => new(a.X + b.X, a.Z + b.Z); @@ -55,6 +55,8 @@ public record struct WPos(float X, float Z) { public WPos(Vector2 v) : this(v.X, v.Y) { } public readonly Vector2 ToVec2() => new(X, Z); + public readonly Vector3 ToVec3(float y = 0) => new(X, y, Z); + public readonly Vector4 ToVec4(float y = 0, float w = 0) => new(X, y, Z, w); public readonly WDir ToWDir() => new(X, Z); public static WPos operator +(WPos a, WDir b) => new(a.X + b.X, a.Z + b.Z); From a36f7236f45dbb67fdc8e67613f4fd9e98c7b962 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Fri, 20 Dec 2024 09:13:33 +0000 Subject: [PATCH 04/12] Angle tooltip. --- BossMod/Autorotation/UIStrategyValue.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BossMod/Autorotation/UIStrategyValue.cs b/BossMod/Autorotation/UIStrategyValue.cs index 20f2d088a1..6c47cea197 100644 --- a/BossMod/Autorotation/UIStrategyValue.cs +++ b/BossMod/Autorotation/UIStrategyValue.cs @@ -199,6 +199,8 @@ public static bool DrawEditorTarget(ref StrategyValue value, ActionTargets suppo { modified |= ImGui.DragFloat("Offset", ref value.Offset1, 0.1f, 0, 30); modified |= ImGui.DragFloat("Direction", ref value.Offset2, 1, -180, 180); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"In degrees; 0 is south, increases CCW (so 90 is E, 180 is N, -90 is W)"); } } From 0a52f2f0408ed7126a55448b39a673cdbe27c915 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sat, 21 Dec 2024 10:46:34 +0000 Subject: [PATCH 05/12] FRU P1 AI --- BossMod/AI/AIRotationModule.cs | 6 +- BossMod/Autorotation/RotationModuleManager.cs | 2 +- BossMod/BossModule/AIHintsVisualizer.cs | 2 +- BossMod/Framework/Plugin.cs | 2 +- BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs | 2 - .../Modules/Dawntrail/Ultimate/FRU/FRUAI.cs | 10 +- .../Dawntrail/Ultimate/FRU/FRUConfig.cs | 6 +- .../Dawntrail/Ultimate/FRU/FRUStates.cs | 12 +- .../Dawntrail/Ultimate/FRU/P1Blastburn.cs | 47 ++++++++ .../Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs | 103 ++++++++++++++--- .../Dawntrail/Ultimate/FRU/P1BurntStrike.cs | 40 ------- .../Dawntrail/Ultimate/FRU/P1CyclonicBreak.cs | 2 +- .../Dawntrail/Ultimate/FRU/P1Explosion.cs | 104 +++++++++++++++--- .../Dawntrail/Ultimate/FRU/P1FallOfFaith.cs | 28 +++-- .../Ultimate/FRU/P1PowderMarkTrail.cs | 24 +++- .../Dawntrail/Ultimate/FRU/P1UtopianSky.cs | 28 +++-- 16 files changed, 305 insertions(+), 113 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs delete mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P1BurntStrike.cs diff --git a/BossMod/AI/AIRotationModule.cs b/BossMod/AI/AIRotationModule.cs index 6f4e1f17a6..4c703f7a76 100644 --- a/BossMod/AI/AIRotationModule.cs +++ b/BossMod/AI/AIRotationModule.cs @@ -10,12 +10,14 @@ public abstract class AIRotationModule(RotationModuleManager manager, Actor play protected float Deadline(DateTime deadline) => Math.Max(0, (float)(deadline - World.CurrentTime).TotalSeconds); protected float Speed() => Player.FindStatus(50) != null ? 7.8f : 6; - protected bool InMeleeRange(Actor target) + protected bool InMeleeRange(Actor target, WPos position) { var maxRange = target.HitboxRadius + Player.HitboxRadius + 3; - return (target.Position - Player.Position).LengthSq() < maxRange * maxRange; + return (target.Position - position).LengthSq() < maxRange * maxRange; } + protected bool InMeleeRange(Actor target) => InMeleeRange(target, Player.Position); + protected void SetForcedMovement(WPos? pos, float tolerance = 0.1f) { var dir = (pos ?? Player.Position) - Player.Position; diff --git a/BossMod/Autorotation/RotationModuleManager.cs b/BossMod/Autorotation/RotationModuleManager.cs index 42726516a4..0931d4e977 100644 --- a/BossMod/Autorotation/RotationModuleManager.cs +++ b/BossMod/Autorotation/RotationModuleManager.cs @@ -93,7 +93,7 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) { Hints.ForcedTarget = forced.Value.Target != StrategyTarget.Automatic ? ResolveTargetOverride(forced.Value.Target, forced.Value.TargetParam) - : (ResolveTargetOverride(StrategyTarget.EnemyWithHighestPriority, 0) ?? Bossmods.ActiveModule?.PrimaryActor); + : (ResolveTargetOverride(StrategyTarget.EnemyWithHighestPriority, 0) ?? (Bossmods.ActiveModule?.PrimaryActor is var primary && primary != null && !primary.IsDeadOrDestroyed && primary.IsTargetable ? primary : null)); } // auto actions diff --git a/BossMod/BossModule/AIHintsVisualizer.cs b/BossMod/BossModule/AIHintsVisualizer.cs index cf8328471a..9d41d634a2 100644 --- a/BossMod/BossModule/AIHintsVisualizer.cs +++ b/BossMod/BossModule/AIHintsVisualizer.cs @@ -16,7 +16,7 @@ public void Draw(UITree tree) { tree.LeafNodes(hints.PotentialTargets, e => $"[{e.Priority}] {e.Actor} (str={e.AttackStrength:f2}), dist={(e.Actor.Position - player.Position).Length():f2}, tank={e.ShouldBeTanked}/{e.PreferProvoking}/{e.DesiredPosition}/{e.DesiredRotation}"); } - tree.LeafNode($"Forced target: {hints.ForcedTarget}"); + tree.LeafNode($"Forced target: {hints.ForcedTarget}{((hints.ForcedTarget?.IsTargetable ?? true) ? "" : " (untargetable)")}"); tree.LeafNode($"Forced movement: {hints.ForcedMovement} (misdirection threshold={hints.MisdirectionThreshold})"); tree.LeafNode($"Special movement: {hints.ImminentSpecialMode.mode} in {Math.Max(0, (hints.ImminentSpecialMode.activation - ws.CurrentTime).TotalSeconds):f3}s"); foreach (var _1 in tree.Node("Forbidden zones", hints.ForbiddenZones.Count == 0)) diff --git a/BossMod/Framework/Plugin.cs b/BossMod/Framework/Plugin.cs index 182ae116a0..d66079f169 100644 --- a/BossMod/Framework/Plugin.cs +++ b/BossMod/Framework/Plugin.cs @@ -292,7 +292,7 @@ private unsafe void ExecuteHints() _movementOverride.DesiredDirection = _hints.ForcedMovement; _movementOverride.MisdirectionThreshold = _hints.MisdirectionThreshold; // update forced target, if needed (TODO: move outside maybe?) - if (_hints.ForcedTarget != null) + if (_hints.ForcedTarget != null && _hints.ForcedTarget.IsTargetable) { var obj = _hints.ForcedTarget.SpawnIndex >= 0 ? FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager.Instance()->Objects.IndexSorted[_hints.ForcedTarget.SpawnIndex].Value : null; if (obj != null && obj->EntityId != _hints.ForcedTarget.InstanceID) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs index bc7afb7f34..37cf674dfa 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs @@ -1,7 +1,5 @@ namespace BossMod.Dawntrail.Ultimate.FRU; -class P1BrightfireSmall(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BrightfireSmall), new AOEShapeCircle(5)); -class P1BrightfireLarge(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BrightfireLarge), new AOEShapeCircle(10)); class P2QuadrupleSlap(BossModule module) : Components.TankSwap(module, ActionID.MakeSpell(AID.QuadrupleSlapFirst), ActionID.MakeSpell(AID.QuadrupleSlapFirst), ActionID.MakeSpell(AID.QuadrupleSlapSecond), 4.1f, null, true); class P2CrystalOfLight(BossModule module) : Components.Adds(module, (uint)OID.CrystalOfLight); class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Junction)); diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs index 13b15c2c49..a18d0dd4f0 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs @@ -8,7 +8,7 @@ namespace BossMod.Dawntrail.Ultimate.FRU; sealed class FRUAI(RotationModuleManager manager, Actor player) : AIRotationModule(manager, player) { public enum Track { Movement } - public enum MovementStrategy { None, Pathfind, Explicit, Prepull, DragToCenter } + public enum MovementStrategy { None, Pathfind, PathfindMeleeGreed, Explicit, Prepull, DragToCenter } public static RotationModuleDefinition Definition() { @@ -16,6 +16,7 @@ public static RotationModuleDefinition Definition() res.Define(Track.Movement).As("Movement", "Movement") .AddOption(MovementStrategy.None, "None", "No automatic movement") .AddOption(MovementStrategy.Pathfind, "Pathfind", "Use standard pathfinding to move") + .AddOption(MovementStrategy.PathfindMeleeGreed, "PathfindMeleeGreed", "Melee greed: find closest safespot, then move to maxmelee closest to it") .AddOption(MovementStrategy.Explicit, "Explicit", "Move to specific point", supportedTargets: ActionTargets.Area) .AddOption(MovementStrategy.Prepull, "Prepull", "Pre-pull position: as close to the clock-spot as possible") .AddOption(MovementStrategy.DragToCenter, "DragToCenter", "Drag boss to the arena center"); @@ -34,7 +35,8 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa private WPos CalculateDestination(FRU module, Actor? primaryTarget, StrategyValues.OptionRef strategy, PartyRolesConfig.Assignment assignment) => strategy.As() switch { - MovementStrategy.Pathfind => PathfindPosition(), + MovementStrategy.Pathfind => PathfindPosition(null), + MovementStrategy.PathfindMeleeGreed => PathfindPosition(ResolveTargetOverride(strategy.Value) ?? primaryTarget), MovementStrategy.Explicit => ResolveTargetLocation(strategy.Value), MovementStrategy.Prepull => PrepullPosition(module, assignment), MovementStrategy.DragToCenter => DragToCenterPosition(module), @@ -42,10 +44,10 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa }; // TODO: account for leeway for casters - private WPos PathfindPosition() + private WPos PathfindPosition(Actor? meleeGreedTarget) { var res = NavigationDecision.Build(NavigationContext, World, Hints, Player, Speed()); - return res.Destination ?? Player.Position; + return meleeGreedTarget != null && res.Destination != null ? ClosestInMelee(res.Destination.Value, meleeGreedTarget) : (res.Destination ?? Player.Position); } // assumption: pull range is 12; hitbox is 5, so maxmelee is 8, meaning we have approx 4m to move during pull - with sprint, speed is 7.8, accel is 30 => over 0.26s accel period we move 1.014m, then need another 0.38s to reach boss (but it also moves) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs index df045658a4..81b14e43aa 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs @@ -19,9 +19,9 @@ public class FRUConfig() : ConfigNode() public bool P1FallOfFaithEW = false; [PropertyDisplay("P1 Explosions: tower fill priority (lower number goes north)")] - [GroupDetails(["MT (ignore)", "OT (ignore)", "Fixed N", "Fixed Center", "Fixed S", "Flex 1", "Flex 2", "Flex 3"])] + [GroupDetails(["Tank N", "Tank S", "Fixed N", "Fixed Center", "Fixed S", "Flex 1", "Flex 2", "Flex 3"])] [GroupPreset("H1-R2-H2 fixed, M1-M2-R1 flex", [0, 1, 2, 4, 5, 6, 7, 3])] - public GroupAssignmentUnique P1ExplosionsAssignment = GroupAssignmentUnique.DefaultRoles(); + public GroupAssignmentUnique P1ExplosionsAssignment = new() { Assignments = [0, 1, 2, 4, 5, 6, 7, 3] }; [PropertyDisplay("P2 Diamond Dust: cardinal assignments")] [GroupDetails(["Support N", "Support E", "Support S", "Support W", "DD N", "DD E", "DD S", "DD W"])] @@ -75,7 +75,7 @@ public class FRUConfig() : ConfigNode() [GroupPreset("Default", [0, 1, 6, 2, 5, 3, 7, 4])] public GroupAssignmentUnique P1UtopianSkyInitialSpots = new() { Assignments = [0, 1, 6, 2, 5, 3, 7, 4] }; - [PropertyDisplay("P1 Utopian Sky: spread spots (G1 CCW from N, G2 CW from NE")] + [PropertyDisplay("P1 Utopian Sky: spread spots (G1 CCW from N, G2 CW from NE", tooltip: "Only used by AI")] [GroupDetails(["G1 Close", "G1 Far Center", "G1 Far Left", "G1 Far Right", "G2 Close", "G2 Far Center", "G2 Far Left", "G2 Far Right"])] [GroupPreset("Default", [1, 5, 0, 4, 2, 6, 3, 7])] public GroupAssignmentUnique P1UtopianSkySpreadSpots = new() { Assignments = [1, 5, 0, 4, 2, 6, 3, 7] }; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index 844ec50952..ba1821f364 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -124,6 +124,7 @@ private void P1TurnOfTheHeavensBoundOfFaith(uint id, float delay) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .DeactivateOnExit(); ComponentCondition(id + 0x10, 1.7f, comp => comp.NumCasts > 0, "Line 2") .DeactivateOnExit(); @@ -131,15 +132,20 @@ private void P1TurnOfTheHeavensBoundOfFaith(uint id, float delay) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter(); + .ActivateOnEnter() + .ExecOnEnter(comp => comp.Risky = false); // it's fine to stay in aoes before kb ComponentCondition(id + 0x12, 3.9f, comp => comp.NumCasts > 0, "Line 3") .DeactivateOnExit(); ComponentCondition(id + 0x13, 2, comp => comp.NumCasts > 0, "Knockback") + .DeactivateOnExit() .DeactivateOnExit(); ComponentCondition(id + 0x20, 2.1f, comp => comp.NumCasts > 0, "Circles") + .ActivateOnEnter() + .ExecOnEnter(comp => comp.Risky = true) .DeactivateOnExit() .DeactivateOnExit(); ComponentCondition(id + 0x30, 4, comp => !comp.Active, "Stacks") // note: won't happen if both targets die, but that's a wipe anyway + .DeactivateOnExit() .DeactivateOnExit(); ActorTargetable(id + 0x100, _module.BossP1, true, 1.4f, "Boss reappears") .SetHint(StateMachine.StateHint.DowntimeEnd); @@ -169,17 +175,19 @@ private void P1PowderMarkTrailExplosions(uint id, float delay) ComponentCondition(id + 0x100, 5.1f, comp => comp.Towers.Count > 0) .ActivateOnEnter(); Condition(id + 0x101, 6.5f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "Narrow line") - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() + .ExecOnEnter(comp => comp.Risky = false) // fine to greed .DeactivateOnExit() .DeactivateOnExit(); Condition(id + 0x102, 2, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "Line/Knockback", checkDelay: 2) // note: kb and wide line have slightly different cast time... + .ExecOnEnter(comp => comp.Risky = true) .DeactivateOnExit() .DeactivateOnExit(); ComponentCondition(id + 0x103, 2, comp => comp.NumCasts > 0, "Towers") + .ActivateOnEnter() .DeactivateOnExit(); ComponentCondition(id + 0x104, 0.5f, comp => comp.NumCasts > 0, "Tankbusters") .DeactivateOnExit() diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs new file mode 100644 index 0000000000..3b3d9afa1d --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs @@ -0,0 +1,47 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P1Blastburn(BossModule module) : Components.Knockback(module, default, true) +{ + private Actor? _caster; + private bool _aoeDone; + + public override IEnumerable Sources(int slot, Actor actor) + { + if (_caster != null) + { + 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, 15, Module.CastFinishAt(_caster.CastInfo), null, dir, kind); + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_caster != null) + hints.AddForbiddenZone(ShapeDistance.InvertedRect(_caster.Position, _caster.CastInfo?.Rotation ?? _caster.Rotation, 40, 40, 2 + (_aoeDone ? 0 : 5)), Module.CastFinishAt(_caster.CastInfo)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.TurnOfHeavensBlastburn or AID.ExplosionBlastburn) + { + _caster = caster; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.TurnOfHeavensBlastburn: + case AID.ExplosionBlastburn: + _caster = null; + ++NumCasts; + break; + case AID.TurnOfHeavensBurntStrikeFire: + case AID.ExplosionBurntStrikeFire: + _aoeDone = true; + break; + } + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs index cc624b17be..9c96c8d56a 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs @@ -1,20 +1,30 @@ namespace BossMod.Dawntrail.Ultimate.FRU; +class P1TurnOfHeavensBurntStrikeFire(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurntStrikeFire), new AOEShapeRect(40, 5, 40)); +class P1TurnOfHeavensBurntStrikeLightning(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurntStrikeLightning), new AOEShapeRect(40, 5, 40)); +class P1TurnOfHeavensBurnout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurnout), new AOEShapeRect(40, 10, 40)); +class P1BrightfireSmall(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BrightfireSmall), new AOEShapeCircle(5)); +class P1BrightfireLarge(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BrightfireLarge), new AOEShapeCircle(10)); + // TODO: fixed tethers strat variant (tether target with clone on safe side goes S, other goes N, if any group has 5 players prio1 adjusts) class P1BoundOfFaith(BossModule module) : Components.UniformStackSpread(module, 6, 0, 4, 4) { + public WDir SafeSide; + public DateTime Activation; + public readonly int[] AssignedGroups = new int[PartyState.MaxPartySize]; private readonly FRUConfig _config = Service.Config.Get(); - private readonly int[] _assignedGroups = new int[PartyState.MaxPartySize]; private OID _safeHalo; - private int _safeSide; // -1 if X<0, +1 if X>0 + + public WDir AssignedLane(int slot) => new(0, AssignedGroups[slot] * 5.4f); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // we have dedicated components for this public override void DrawArenaForeground(int pcSlot, Actor pc) { base.DrawArenaForeground(pcSlot, pc); - if (_assignedGroups[pcSlot] != 0 && _safeSide != 0) + if (AssignedGroups[pcSlot] != 0 && SafeSide.X != 0) { - var safeDir = _safeSide * (90 - _assignedGroups[pcSlot] * 22.5f).Degrees(); - Arena.AddCircle(Module.Center + 19 * safeDir.ToDirection(), 1, ArenaColor.Safe); + Arena.AddCircle(Module.Center + SafeSide * 18.2f + AssignedLane(pcSlot), 1, ArenaColor.Safe); } } @@ -32,7 +42,8 @@ public override void OnTethered(Actor source, ActorTetherInfo tether) { if (tether.ID == (uint)TetherID.Fire && WorldState.Actors.Find(tether.Target) is var target && target != null) { - AddStack(target, WorldState.FutureTime(10.6f)); + Activation = WorldState.FutureTime(10.6f); + AddStack(target, Activation); if (Stacks.Count == 2) InitAssignments(); } @@ -41,9 +52,7 @@ public override void OnTethered(Actor source, ActorTetherInfo tether) public override void OnEventCast(Actor caster, ActorCastEvent spell) { if ((AID)spell.Action.ID == AID.BoundOfFaithSinsmoke) - { Stacks.Clear(); - } } private void InitAssignments() @@ -53,7 +62,7 @@ private void InitAssignments() WDir averageOffset = default; foreach (var aoe in Module.Enemies(_safeHalo)) averageOffset += aoe.Position - Module.Center; - _safeSide = averageOffset.X > 0 ? 1 : -1; + SafeSide.X = averageOffset.X > 0 ? 1 : -1; } // initial assignments @@ -61,28 +70,94 @@ private void InitAssignments() Span prio = [0, 0, 0, 0, 0, 0, 0, 0]; foreach (var (slot, group) in _config.P1BoundOfFaithAssignment.Resolve(Raid)) { - _assignedGroups[slot] = group < 4 ? -1 : 1; + AssignedGroups[slot] = group < 4 ? -1 : 1; prio[slot] = group & 3; if (IsStackTarget(Raid[slot])) tetherSlots[tetherSlots[0] < 0 ? 0 : 1] = slot; } // swaps - if (tetherSlots[0] >= 0 && _assignedGroups[tetherSlots[0]] == _assignedGroups[tetherSlots[1]]) + if (tetherSlots[0] >= 0 && AssignedGroups[tetherSlots[0]] == AssignedGroups[tetherSlots[1]]) { // flex tether with lower prio var tetherFlexSlot = prio[tetherSlots[0]] < prio[tetherSlots[1]] ? tetherSlots[0] : tetherSlots[1]; - _assignedGroups[tetherFlexSlot] = -_assignedGroups[tetherFlexSlot]; + AssignedGroups[tetherFlexSlot] = -AssignedGroups[tetherFlexSlot]; // now the group where we've moved flex slot has 5 people, find untethered with lowest prio for (var normalFlexSlot = 0; normalFlexSlot < PartyState.MaxPartySize; ++normalFlexSlot) { - if (normalFlexSlot != tetherFlexSlot && prio[normalFlexSlot] == 0 && _assignedGroups[normalFlexSlot] == _assignedGroups[tetherFlexSlot]) + if (normalFlexSlot != tetherFlexSlot && prio[normalFlexSlot] == 0 && AssignedGroups[normalFlexSlot] == AssignedGroups[tetherFlexSlot]) { - _assignedGroups[normalFlexSlot] = -_assignedGroups[normalFlexSlot]; + AssignedGroups[normalFlexSlot] = -AssignedGroups[normalFlexSlot]; break; } } } } } + +class P1BoundOfFaithAIKnockback(BossModule module) : BossComponent(module) +{ + private readonly P1BoundOfFaith? _comp = module.FindComponent(); + private bool _horizDone; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_comp == null || _comp.SafeSide == default) + return; + + var sideOffset = _horizDone ? 0 : 7; // before horizonal aoes are done, we don't show knockback, so adjust the unsafe zone + hints.AddForbiddenZone(ShapeDistance.HalfPlane(Module.Center + sideOffset * _comp.SafeSide, _comp.SafeSide), _comp.Activation); + + var lane = _comp.AssignedLane(slot); + if (_horizDone && lane.Z != 0) + { + hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.Center + lane, new WDir(1, 0), 20, 20, 0.7f), _comp.Activation); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.TurnOfHeavensBurnout) + _horizDone = true; + } +} + +class P1BoundOfFaithAIStack(BossModule module) : BossComponent(module) +{ + private readonly P1BoundOfFaith? _comp = module.FindComponent(); + private bool _haveFetters; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_comp == null) + return; + + if (_haveFetters) + { + foreach (var s in _comp.Stacks) + { + var targetSlot = Raid.FindSlot(s.Target.InstanceID); + var targetGroup = targetSlot >= 0 ? _comp.AssignedGroups[targetSlot] : 0; + if (targetGroup == _comp.AssignedGroups[slot]) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(s.Target.Position, 6), _comp.Activation); + else + hints.AddForbiddenZone(ShapeDistance.Circle(s.Target.Position, 6), _comp.Activation); + } + + // all else being equal, try staying closer to center + hints.GoalZones.Add(hints.GoalSingleTarget(Module.Center, 7.5f, 0.5f)); + } + else + { + // just go to center + hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.Center, new WDir(1, 0), 1, 1, 20), DateTime.MaxValue); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.FloatingFetters) + _haveFetters = true; + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BurntStrike.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BurntStrike.cs deleted file mode 100644 index 845b51d1ef..0000000000 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BurntStrike.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace BossMod.Dawntrail.Ultimate.FRU; - -class P1TurnOfHeavensBurntStrikeFire(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurntStrikeFire), new AOEShapeRect(40, 5, 40)); -class P1TurnOfHeavensBurntStrikeLightning(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurntStrikeLightning), new AOEShapeRect(40, 5, 40)); -class P1TurnOfHeavensBurnout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurnout), new AOEShapeRect(40, 10, 40)); -class P1ExplosionBurntStrikeFire(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurntStrikeFire), new AOEShapeRect(40, 5, 40)); -class P1ExplosionBurntStrikeLightning(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurntStrikeLightning), new AOEShapeRect(40, 5, 40)); -class P1ExplosionBurnout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurnout), new AOEShapeRect(40, 10, 40)); - -class P1Blastburn(BossModule module) : Components.Knockback(module, default, true) -{ - private Actor? _caster; - - public override IEnumerable Sources(int slot, Actor actor) - { - if (_caster != null) - { - 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, 15, Module.CastFinishAt(_caster.CastInfo), null, dir, kind); - } - } - - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID is AID.TurnOfHeavensBlastburn or AID.ExplosionBlastburn) - { - _caster = caster; - } - } - - public override void OnCastFinished(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID is AID.TurnOfHeavensBlastburn or AID.ExplosionBlastburn) - { - _caster = null; - ++NumCasts; - } - } -} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1CyclonicBreak.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1CyclonicBreak.cs index b0b4aca4b0..a4ae563e6f 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1CyclonicBreak.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1CyclonicBreak.cs @@ -88,7 +88,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme return; // no assignment var assignedDirection = (180 - 45 * clockspot).Degrees(); // TODO: think about melee vs ranged distance... - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, assignedDirection, 10, -6, 1), _spreadStack.Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, assignedDirection, 15, -5, 1), _spreadStack.Activation); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs index ca10a97e5f..47417762b3 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs @@ -1,45 +1,113 @@ namespace BossMod.Dawntrail.Ultimate.FRU; +class P1ExplosionBurntStrikeFire(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurntStrikeFire), new AOEShapeRect(40, 5, 40)); +class P1ExplosionBurntStrikeLightning(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurntStrikeLightning), new AOEShapeRect(40, 5, 40)); +class P1ExplosionBurnout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurnout), new AOEShapeRect(40, 10, 40)); + // TODO: non-fixed conga? class P1Explosion(BossModule module) : Components.GenericTowers(module) { + public WDir TowerDir; + public DateTime Activation; private readonly FRUConfig _config = Service.Config.Get(); + private bool _isWideLine; + private bool _lineDone; - public override void OnCastStarted(Actor caster, ActorCastInfo spell) + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - var numSoakers = (AID)spell.Action.ID switch + var role = _config.P1ExplosionsAssignment[assignment]; + if (role < 0 || TowerDir == default) + return; + + if (role < 2) { - AID.Explosion11 or AID.Explosion12 => 1, - AID.Explosion21 or AID.Explosion22 => 2, - AID.Explosion31 or AID.Explosion32 => 3, - AID.Explosion41 or AID.Explosion42 => 4, - _ => 0 - }; - if (numSoakers != 0) + // tanks: stay opposite towers on N/S side + // tweak for WAR: if PR is up, assume player will want to maintain full uptime on wide line by using it right before resolve - we want to stay far to increase travel time + var horizOffset = _isWideLine && !_lineDone && actor.Class == Class.WAR && actor.FindStatus(WAR.SID.PrimalRend) != null ? 17 : 0; + hints.AddForbiddenZone(ShapeDistance.HalfPlane(Module.Center - horizOffset * TowerDir, -TowerDir), Activation); + + var vertDir = new WDir(0, role == 0 ? -1 : +1); + hints.AddForbiddenZone(ShapeDistance.HalfPlane(Module.Center + 5 * vertDir, vertDir), Activation); + } + else + { + // others: soak assigned tower, or at least stay in lane with it (if knockback is imminent, or always for ranged) + var index = Towers.FindIndex(t => !t.ForbiddenSoakers[slot]); + if (index >= 0) + { + var needSoak = _lineDone || _isWideLine && actor.Role is Role.Healer or Role.Ranged; + ref var t = ref Towers.Ref(index); + if (needSoak) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(t.Position, t.Radius), t.Activation); + else + hints.AddForbiddenZone(ShapeDistance.InvertedRect(new(Module.Center.X, t.Position.Z), TowerDir, 20, 0, t.Radius), t.Activation); + } + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) { - Towers.Add(new(caster.Position, 4, numSoakers, numSoakers, default, Module.CastFinishAt(spell))); - if (Towers.Count == 3) - InitAssignments(); + case AID.Explosion11: + case AID.Explosion12: + AddTower(caster.Position, 1, Module.CastFinishAt(spell)); + break; + case AID.Explosion21: + case AID.Explosion22: + AddTower(caster.Position, 2, Module.CastFinishAt(spell)); + break; + case AID.Explosion31: + case AID.Explosion32: + AddTower(caster.Position, 3, Module.CastFinishAt(spell)); + break; + case AID.Explosion41: + case AID.Explosion42: + AddTower(caster.Position, 4, Module.CastFinishAt(spell)); + break; + case AID.ExplosionBurnout: + _isWideLine = true; + break; } } public override void OnCastFinished(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID is AID.Explosion11 or AID.Explosion12 or AID.Explosion21 or AID.Explosion22 or AID.Explosion31 or AID.Explosion32 or AID.Explosion41 or AID.Explosion42) + switch ((AID)spell.Action.ID) { - ++NumCasts; - Towers.RemoveAll(t => t.Position.AlmostEqual(caster.Position, 1)); + case AID.Explosion11: + case AID.Explosion12: + case AID.Explosion21: + case AID.Explosion22: + case AID.Explosion31: + case AID.Explosion32: + case AID.Explosion41: + case AID.Explosion42: + ++NumCasts; + Towers.RemoveAll(t => t.Position.AlmostEqual(caster.Position, 1)); + break; + case AID.ExplosionBurnout: + case AID.ExplosionBlastburn: + _lineDone = true; + break; } } - private void InitAssignments() + private void AddTower(WPos pos, int numSoakers, DateTime activation) { - Towers.SortBy(t => t.Position.Z); - if (Towers.Count != 3 || Towers.Sum(t => t.MinSoakers) != 6) + Activation = activation; + Towers.Add(new(pos, 4, numSoakers, numSoakers, default, activation)); + if (Towers.Count != 3) + return; + + // init assignments + if (Towers.Sum(t => t.MinSoakers) != 6) { ReportError($"Unexpected tower state"); return; } + Towers.SortBy(t => t.Position.Z); + TowerDir.X = Towers.Sum(t => t.Position.X - Module.Center.X) > 0 ? 1 : -1; Span slotByGroup = [-1, -1, -1, -1, -1, -1, -1, -1]; foreach (var (slot, group) in _config.P1ExplosionsAssignment.Resolve(Raid)) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs index cf19894d2a..fe2e009ac7 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs @@ -9,6 +9,7 @@ class P1FallOfFaith(BossModule module) : Components.CastCounter(module, default) private readonly List _currentBaiters = []; private BitMask _fireTethers; // bit i is set if i'th tether is fire private int _numFetters; + private DateTime _minHelpMove; // we want conga members to start moving with a slight delay private static readonly AOEShapeCone _shapeFire = new(60, 45.Degrees()); private static readonly AOEShapeCone _shapeLightning = new(60, 60.Degrees()); @@ -43,6 +44,20 @@ public override void AddGlobalHints(GlobalHints hints) hints.Add(string.Join(" -> ", Enumerable.Range(NumCasts, _tetherTargets.Count - NumCasts).Select(i => _fireTethers[i] ? "Fire" : "Lightning"))); } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var baitOrder = NextAssignedBaitOrder(slot); + if (baitOrder == 0 || _playerOrder[slot] >= 5 && WorldState.CurrentTime < _minHelpMove) + return; + var dest = TetherSpot(baitOrder); + if (_playerOrder[slot] != baitOrder) + dest += BaitOffset(_playerOrder[slot], _fireTethers[baitOrder - 1]); + // note: the baits need to be very precise + var destCellSize = 0.5f * Module.Bounds.MapResolution; + var destCellCenter = Module.Center + ((dest - Module.Center) / Module.Bounds.MapResolution).Floor() * Module.Bounds.MapResolution + new WDir(destCellSize, destCellSize); + hints.AddForbiddenZone(p => p.AlmostEqual(destCellCenter, destCellSize) ? 1 : -1); + } + public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot, Actor player, ref uint customColor) { // if player should bait, highlight source and other assistants @@ -88,14 +103,7 @@ public override void OnTethered(Actor source, ActorTetherInfo tether) var slot = Raid.FindSlot(tether.Target); if (slot >= 0) - { _playerOrder[slot] = _tetherTargets.Count; - //var odd = (order & 1) != 0; - //var firstBait = order <= 2; - //_states[slot].OddGroup = odd; - //_states[slot].Spot1 = TetherSpot(odd, !firstBait); - //_states[slot].Spot2 = TetherSpot(odd, firstBait); - } if (_tetherTargets.Count == 4) InitAssignments(); @@ -127,11 +135,7 @@ private void InitAssignments() conga.SortBy(c => c.prio); for (int i = 0; i < conga.Count; ++i) _playerOrder[conga[i].slot] = i + 5; - - //InitNormalSpots(conga[0].slot, true, true); - //InitNormalSpots(conga[1].slot, true, false); - //InitNormalSpots(conga[2].slot, false, false); - //InitNormalSpots(conga[3].slot, false, true); + _minHelpMove = WorldState.FutureTime(1); } private bool IsGroupEven(int order) => order is 2 or 4 or 7 or 8; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1PowderMarkTrail.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1PowderMarkTrail.cs index c3d39e4b49..78a93da77a 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1PowderMarkTrail.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1PowderMarkTrail.cs @@ -7,6 +7,7 @@ class P1PowderMarkTrail(BossModule module) : Components.GenericBaitAway(module, private DateTime _activation; private static readonly AOEShapeCircle _shape = new(10); + private const float _avoidBaitDistance = 13; public override void Update() { @@ -30,7 +31,28 @@ public override void AddHints(int slot, Actor actor, TextHints hints) } } - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // TODO: hints for late micro adjusts + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (WorldState.FutureTime(2) < _activation) + return; // start micro adjusts only when activation is imminent; before that we have other components providing coarse positioning + var isTank = actor.Role == Role.Tank; + foreach (var p in Raid.WithoutSlot().Exclude(actor)) + { + var otherTank = p.Role == Role.Tank; + if (isTank && otherTank) + { + // tanks should stay near but not too near other tank + hints.AddForbiddenZone(_shape.Distance(p.Position, default), _activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(p.Position, _avoidBaitDistance), _activation); + } + else if (isTank != otherTank) + { + // tanks should avoid non-tanks and vice versa + hints.AddForbiddenZone(ShapeDistance.Circle(p.Position, _avoidBaitDistance), _activation); + } + // else: non-tanks don't care about non-tanks + } + } public override void OnStatusGain(Actor actor, ActorStatus status) { diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs index d97b16cbce..8c57af4535 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs @@ -104,20 +104,26 @@ class P1UtopianSkyAIInitial(BossModule module) : BossComponent(module) public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { + hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 18)); // stay on edge + var clockspot = _config.P1UtopianSkyInitialSpots[assignment]; - if (clockspot < 0) - return; // no assignment - var assignedDirection = (180 - 45 * clockspot).Degrees(); - if (assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.OT) + if (clockspot >= 0) { - // adjust slightly to neighbouring tank slot - var coTankSpot = _config.P1UtopianSkyInitialSpots[assignment == PartyRolesConfig.Assignment.MT ? PartyRolesConfig.Assignment.OT : PartyRolesConfig.Assignment.MT]; - if (coTankSpot == ((clockspot + 1) & 7)) - assignedDirection -= 4.Degrees(); - else if (coTankSpot == ((clockspot + 7) & 7)) - assignedDirection += 4.Degrees(); + // ... and in assigned cone + var assignedDirection = (180 - 45 * clockspot).Degrees(); + hints.AddForbiddenZone(ShapeDistance.InvertedCone(Module.Center, 50, assignedDirection, 5.Degrees()), DateTime.MaxValue); } - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + 18 * assignedDirection.ToDirection(), 1)); + + //if (assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.OT) + //{ + // // adjust slightly to neighbouring tank slot + // var coTankSpot = _config.P1UtopianSkyInitialSpots[assignment == PartyRolesConfig.Assignment.MT ? PartyRolesConfig.Assignment.OT : PartyRolesConfig.Assignment.MT]; + // if (coTankSpot == ((clockspot + 1) & 7)) + // assignedDirection -= 4.Degrees(); + // else if (coTankSpot == ((clockspot + 7) & 7)) + // assignedDirection += 4.Degrees(); + //} + //hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + 18 * assignedDirection.ToDirection(), 1)); } } From a778ba3f952748da68aa69cc79dee2e557c6b950 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 22 Dec 2024 22:54:43 +0000 Subject: [PATCH 06/12] FRU WIP (P4 start, ai) --- BossMod/Debug/MainDebugWindow.cs | 6 + BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs | 9 + .../Dawntrail/Ultimate/FRU/FRUConfig.cs | 7 +- .../Dawntrail/Ultimate/FRU/FRUEnums.cs | 30 +++- .../Dawntrail/Ultimate/FRU/FRUStates.cs | 62 ++++++- .../Dawntrail/Ultimate/FRU/P2DiamondDust.cs | 163 ++++++++++++++++-- .../Dawntrail/Ultimate/FRU/P3Apocalypse.cs | 2 +- .../Dawntrail/Ultimate/FRU/P4AhkRhai.cs | 16 ++ 8 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs diff --git a/BossMod/Debug/MainDebugWindow.cs b/BossMod/Debug/MainDebugWindow.cs index cb7a11d86e..6ff25ea3bc 100644 --- a/BossMod/Debug/MainDebugWindow.cs +++ b/BossMod/Debug/MainDebugWindow.cs @@ -180,6 +180,12 @@ private unsafe void DrawStatuses() var player = (Character*)GameObjectManager.Instance()->Objects.IndexSorted[0].Value; player->GetStatusManager()->SetStatus(20, 3909, 20.0f, 100, 0xE0000000, true); } + ImGui.SameLine(); + if (ImGui.Button("Add thin ice")) + { + var player = (Character*)GameObjectManager.Instance()->Objects.IndexSorted[0].Value; + player->GetStatusManager()->SetStatus(20, 911, 20.0f, 320, 0xE0000000, true); // param = distance * 10 + } foreach (var elem in ws.Actors) { diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs index 37cf674dfa..8f4170b331 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs @@ -4,6 +4,7 @@ class P2QuadrupleSlap(BossModule module) : Components.TankSwap(module, ActionID. class P2CrystalOfLight(BossModule module) : Components.Adds(module, (uint)OID.CrystalOfLight); class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Junction)); class P3BlackHalo(BossModule module) : Components.CastSharedTankbuster(module, ActionID.MakeSpell(AID.BlackHalo), new AOEShapeCone(60, 45.Degrees())); // TODO: verify angle +class P4EdgeOfOblivion(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.EdgeOfOblivion)); [ModuleInfo(BossModuleInfo.Maturity.WIP, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1006, NameID = 9707, PlanLevel = 100)] public class FRU(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)) @@ -11,11 +12,15 @@ class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.Ma private Actor? _bossP2; private Actor? _iceVeil; private Actor? _bossP3; + private Actor? _bossP4Usurper; + private Actor? _bossP4Oracle; public Actor? BossP1() => PrimaryActor; public Actor? BossP2() => _bossP2; public Actor? IceVeil() => _iceVeil; public Actor? BossP3() => _bossP3; + public Actor? BossP4Usurper() => _bossP4Usurper; + public Actor? BossP4Oracle() => _bossP4Oracle; protected override void UpdateModule() { @@ -24,6 +29,8 @@ protected override void UpdateModule() _bossP2 ??= StateMachine.ActivePhaseIndex == 1 ? Enemies(OID.BossP2).FirstOrDefault() : null; _iceVeil ??= StateMachine.ActivePhaseIndex == 1 ? Enemies(OID.IceVeil).FirstOrDefault() : null; _bossP3 ??= StateMachine.ActivePhaseIndex == 2 ? Enemies(OID.BossP3).FirstOrDefault() : null; + _bossP4Usurper ??= StateMachine.ActivePhaseIndex == 2 ? Enemies(OID.UsurperOfFrostP4).FirstOrDefault() : null; + _bossP4Oracle ??= StateMachine.ActivePhaseIndex == 2 ? Enemies(OID.OracleOfDarknessP4).FirstOrDefault() : null; } protected override void DrawEnemies(int pcSlot, Actor pc) @@ -32,5 +39,7 @@ protected override void DrawEnemies(int pcSlot, Actor pc) Arena.Actor(_bossP2, ArenaColor.Enemy); Arena.Actor(_iceVeil, ArenaColor.Enemy); Arena.Actor(_bossP3, ArenaColor.Enemy); + Arena.Actor(_bossP4Usurper, ArenaColor.Enemy); + Arena.Actor(_bossP4Oracle, ArenaColor.Enemy); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs index 81b14e43aa..3f6d563580 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs @@ -25,7 +25,8 @@ public class FRUConfig() : ConfigNode() [PropertyDisplay("P2 Diamond Dust: cardinal assignments")] [GroupDetails(["Support N", "Support E", "Support S", "Support W", "DD N", "DD E", "DD S", "DD W"])] - public GroupAssignmentUnique P2DiamondDustCardinals = GroupAssignmentUnique.DefaultRoles(); + [GroupPreset("Default", [0, 2, 3, 1, 7, 6, 4, 5])] + public GroupAssignmentUnique P2DiamondDustCardinals = new() { Assignments = [0, 2, 3, 1, 7, 6, 4, 5] }; [PropertyDisplay("P2 Diamond Dust: supports go to CCW intercardinal")] public bool P2DiamondDustSupportsCCW; @@ -33,6 +34,10 @@ public class FRUConfig() : ConfigNode() [PropertyDisplay("P2 Diamond Dust: DD go to CCW intercardinal")] public bool P2DiamondDustDDCCW; + [PropertyDisplay("P2 Diamond Dust: knockback groups")] + [GroupDetails(["G1 (CCW from N)", "G2 (CW from NE)"])] + public GroupAssignmentLightParties P2DiamondDustKnockbacks = GroupAssignmentLightParties.DefaultLightParties(); + [PropertyDisplay("P2 Light Rampant: conga spots (lower numbers to the west, assume CW rotation)")] [GroupDetails(["N1", "N2", "N3", "N4", "S1", "S2", "S3", "S4"])] [GroupPreset("HHTT/RRMM", [2, 3, 0, 1, 6, 7, 4, 5])] diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index cae417614d..aa011be6fb 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -21,11 +21,18 @@ public enum OID : uint Gaia = 0x45A6, // R1.000, x0 (spawn during fight) //_Gen_CrystalOfLight = 0x464F, // R1.000, x0 (spawn during fight) HiemalRayVoidzone = 0x1EA1CB, // R0.500, x0 (spawn during fight), EventObj type - //_Gen_EternalIceFragment = 0x1EBBF8, // R0.500, x0 (spawn during fight), EventObj type + EternalIceFragment = 0x1EBBF8, // R0.500, x0 (spawn during fight), EventObj type, spawns if dark crystals are killed BossP3 = 0x45A7, // R7.040, x0 (spawn during fight) DelightsHourglass = 0x45A8, // R1.000, x0 (spawn during fight) ApocalypseLight = 0x1EB0FF, // R0.500, x0 (spawn during fight), EventObj type + + UsurperOfFrostP4 = 0x45A9, // R6.125, x0 (spawn during fight) + OracleOfDarknessP4 = 0x45AB, // R7.040, x0 (spawn during fight) + VisionOfRyne = 0x45B4, // R0.750, x0 (spawn during fight) + VisionOfGaia = 0x45B5, // R1.500, x0 (spawn during fight) + FragmentOfFate = 0x45B1, // R3.500, x0 (spawn during fight) + GreatWyrm = 0x45AA, // R3.500, x0 (spawn during fight), Part type } public enum AID : uint @@ -151,6 +158,7 @@ public enum AID : uint HiemalStorm = 40255, // CrystalOfLight->self, no cast, single-target, visual (baited puddle) HiemalStormAOE = 40256, // Helper->location, 3.3s cast, range 7 circle HiemalRay = 40257, // Helper->player, no cast, range 4 circle + Icecrusher = 40121, // Gaia->IceVeil, no cast, single-target, 50% hp hit on large crystal // P3 Junction = 40226, // Helper->self, no cast, range 40 circle, raidwide @@ -189,7 +197,21 @@ public enum AID : uint DarkestDanceKnockback = 40183, // BossP3->self, no cast, range 40 circle, knockback 21 MemorysEnd = 40300, // BossP3->self, 10.0s cast, single-target, visual (enrage) - MemorysEndAOE = 40336, // Helper->self, no cast, range 100 circle, enrage + MemorysEndRaidwide = 40335, // Helper->self, no cast, range 100 circle, raidwide if boss is <20% + MemorysEndEnrage = 40336, // Helper->self, no cast, range 100 circle, enrage if boss is >20% + + // P4 + Materialization = 40246, // UsurperOfFrostP4->self, 3.0s cast, single-target, visual (create visions) + DrachenArmor = 40186, // Helper->self, no cast, single-target, visual (wings appear) + AutoAttackP4Wyrm = 40178, // GreatWyrm->player, no cast, single-target + AutoAttackP4Usurper = 40177, // UsurperOfFrostP4->player, no cast, single-target + EdgeOfOblivion = 40174, // FragmentOfFate->self, 5.0s cast, range 100 circle, raidwide + AkhRhai = 40237, // Helper->location, 2.5s cast, range 4 circle, visual (puddle) + AkhRhaiAOE = 40238, // Helper->location, no cast, range 4 circle, repeated puddle x10 + + DarklitDragonsongUsurper = 40239, // UsurperOfFrostP4->self, 5.0s cast, range 100 circle, raidwide + DarklitDragonsongOracle = 40301, // OracleOfDarknessP4->self, 5.0s cast, single-target, visual + //_Weaponskill_ThePathOfLight = 40187, // UsurperOfFrostP4->self, 8.0s cast, single-target } public enum SID : uint @@ -200,6 +222,7 @@ public enum SID : uint FatedBurnMark = 4165, // none->player, extra=0x0 FloatingFetters = 2304, // FatebreakersImage/BossP1->player, extra=0xC8 MarkOfMortality = 4372, // Helper->player, extra=0x1 + ThinIce = 911, // none->player, extra=0x140 ChainsOfEverlastingLight = 4157, // none->player, extra=0x0, light rampant first tether CurseOfEverlastingLight = 4158, // none->player, extra=0x0, light rampant second tether WeightOfLight = 4159, // none->player, extra=0x0, light rampant stack @@ -214,8 +237,6 @@ public enum SID : uint DelightsHourglassRotation = 2970, // none->DelightsHourglass, extra=0x10D (ccw)/0x15C (cw) Return = 2452, // none->player, extra=0x0 Stun = 4163, // none->player, extra=0x0 - //SpellInWaitingRefrain = 4373, // BossP3->BossP3, extra=0x0 - //_Gen_ = 2458, // none->player, extra=0x0 } public enum IconID : uint @@ -238,6 +259,7 @@ public enum TetherID : uint LightRampantCurse = 111, // player->player IntermissionGaia = 112, // Gaia->IceVeil IntermissionCrystal = 8, // CrystalOfLight/CrystalOfDarkness->IceVeil + IntermissionCrystalGaia = 37, // CrystalOfDarkness->Gaia/CrystalOfDarkness HiemalRay = 84, // CrystalOfLight->player UltimateRelativitySlow = 133, // DelightsHourglass->BossP3 UltimateRelativityQuicken = 134, // DelightsHourglass->BossP3 diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index ba1821f364..792a32e3d0 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -12,9 +12,9 @@ public FRUStates(FRU module) : base(module) SimplePhase(1, Phase2, "P2: Usurper of Frost") .SetHint(StateMachine.PhaseHint.StartWithDowntime) .Raw.Update = () => !Module.PrimaryActor.IsDead || (_module.BossP2()?.IsDeadOrDestroyed ?? false) || (_module.IceVeil()?.IsDeadOrDestroyed ?? false); - SimplePhase(2, Phase3, "P3: Oracle of Darkness") + SimplePhase(2, Phase34, "P3/4: Oracle of Darkness & Both") .SetHint(StateMachine.PhaseHint.StartWithDowntime) - .Raw.Update = () => !Module.PrimaryActor.IsDead || (_module.BossP2()?.IsDeadOrDestroyed ?? false) && (_module.BossP3()?.IsDeadOrDestroyed ?? true); + .Raw.Update = () => !Module.PrimaryActor.IsDead || (_module.BossP2()?.IsDeadOrDestroyed ?? false) && (_module.BossP3()?.IsDeadOrDestroyed ?? true) && (_module.BossP4Oracle()?.IsDeadOrDestroyed ?? true); } private void Phase1(uint id) @@ -42,13 +42,18 @@ private void Phase2(uint id) P2AbsoluteZero(id + 0x60000, 8.4f); } - private void Phase3(uint id) + private void Phase34(uint id) { P3JunctionHellsJudgment(id, 13.3f); P3UltimateRelativity(id + 0x10000, 4.3f); P3BlackHalo(id + 0x20000, 3.2f); P3Apocalypse(id + 0x30000, 7.2f); - ActorCast(id + 0x40000, _module.BossP3, AID.MemorysEnd, 3.7f, 10, true, "Enrage"); + P3Enrage(id + 0x40000, 3.7f); + + P4AkhRhai(id + 0x100000, 5.5f); + P4DarklitDragonsong(id + 0x110000, 1.9f); + + SimpleState(id + 0xFF0000, 100, "???"); } private void P1CyclonicBreakPowderMarkTrail(uint id, float delay) @@ -216,33 +221,43 @@ private void P2DiamondDust(uint id, float delay) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() + .ExecOnEnter(comp => comp.Risky = false) // it's fine to bait stuff into these aoes... .SetHint(StateMachine.StateHint.DowntimeStart); Condition(id + 0x30, 5.7f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "In/out") .DeactivateOnExit() .DeactivateOnExit(); ComponentCondition(id + 0x31, 0.8f, comp => comp.NumCasts > 0, "Proteans") + .ExecOnEnter(comp => comp.EnableHints = true) .DeactivateOnExit(); ComponentCondition(id + 0x32, 1.6f, comp => comp.NumCasts > 0, "Ice baits") .DeactivateOnExit() .DeactivateOnExit(); - ComponentCondition(id + 0x33, 0.4f, comp => comp.NumCasts > 0, "Ice circle 1"); + ComponentCondition(id + 0x33, 0.4f, comp => comp.NumCasts > 0, "Ice circle 1") + .ActivateOnEnter() // activate a bit early, so that it can observe first circle direction + .ExecOnEnter(comp => comp.Risky = true); ComponentCondition(id + 0x40, 3.9f, comp => comp.NumCasts > 0, "Knockback") .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() .ActivateOnEnter() // show the cone caster early, to simplify finding movement direction... + .ExecOnEnter(comp => comp.Risky = false) + .ExecOnEnter(comp => comp.Risky = false) .DeactivateOnExit(); ComponentCondition(id + 0x50, 2.8f, comp => comp.NumCasts > 0, "Stars") + .ActivateOnEnter() + .ExecOnEnter(comp => comp.Risky = true) + .ExecOnEnter(comp => comp.Risky = true) .DeactivateOnExit() .DeactivateOnExit(); - ComponentCondition(id + 0x60, 1.3f, comp => comp.NumCasts > 0); + ComponentCondition(id + 0x60, 1.3f, comp => comp.NumCasts > 0) + .ActivateOnEnter(); ComponentCondition(id + 0x70, 4.7f, comp => comp.NumCasts >= 4) .ActivateOnEnter() .ActivateOnEnter() .DeactivateOnExit() // last icicle explodes together with first stack + .DeactivateOnExit() .DeactivateOnExit(); ComponentCondition(id + 0x80, 3.7f, comp => comp.NumCasts > 0, "Gaze") + .ExecOnEnter(comp => comp.EnableAIHints = true) .DeactivateOnExit(); ComponentCondition(id + 0x90, 3.0f, comp => comp.AOEs.Count > 0); ComponentCondition(id + 0x91, 3.5f, comp => comp.NumCasts > 0, "Front/back"); @@ -462,4 +477,35 @@ private void P3Apocalypse(uint id, float delay) P3ShockwavePulsar(id + 0x1000, 0.3f); } + + private void P3Enrage(uint id, float delay) + { + ActorCast(id, _module.BossP3, AID.MemorysEnd, delay, 10, true, "Enrage"); + ActorTargetable(id + 0x10, _module.BossP3, false, 3.5f, "Boss disappears") + .SetHint(StateMachine.StateHint.DowntimeStart); + } + + private void P4AkhRhai(uint id, float delay) + { + ActorTargetable(id, _module.BossP4Usurper, true, delay, "Usurper appears") + .SetHint(StateMachine.StateHint.DowntimeEnd); + ActorCast(id + 0x10, _module.BossP4Usurper, AID.Materialization, 5.1f, 3, true); + ComponentCondition(id + 0x20, 11.2f, comp => comp.AOEs.Count > 0, "Puddle baits") + .ActivateOnEnter(); + ComponentCondition(id + 0x30, 2.6f, comp => comp.NumCasts > 0); + ComponentCondition(id + 0x40, 2.4f, comp => comp.NumCasts > 0, "Raidwide") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + ActorTargetable(id + 0x50, _module.BossP4Oracle, true, 1.2f, "Oracle appears"); + ComponentCondition(id + 0x60, 1.6f, comp => comp.NumCasts >= 10 * comp.AOEs.Count, "Puddle resolve") + .DeactivateOnExit(); + } + + private void P4DarklitDragonsong(uint id, float delay) + { + ActorCast(id, _module.BossP4Usurper, AID.DarklitDragonsongUsurper, delay, 5, true, "Raidwide (darklit)") + .SetHint(StateMachine.StateHint.Raidwide); + // TODO: more... + } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs index 6a2918841a..8c31217e50 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs @@ -10,8 +10,11 @@ class P2FrigidStone : Components.BaitAwayIcon { public P2FrigidStone(BossModule module) : base(module, new AOEShapeCircle(5), (uint)IconID.FrigidStone, ActionID.MakeSpell(AID.FrigidStone), 8.1f, true) { + EnableHints = false; IgnoreOtherBaits = true; } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } } class P2DiamondDustHouseOfLight(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.HouseOfLight)) @@ -46,6 +49,8 @@ public override void AddHints(int slot, Actor actor, TextHints hints) hints.Add("GTFO from baited cone!"); } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } + public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID is AID.AxeKick or AID.ScytheKick) @@ -68,12 +73,18 @@ class P2DiamondDustSafespots(BossModule module) : BossComponent(module) private bool? _out; private bool? _supportsBaitCones; private bool? _conesAtCardinals; - private readonly WPos[] _safespots = new WPos[PartyState.MaxPartySize]; + private readonly WDir[] _safeOffs = new WDir[PartyState.MaxPartySize]; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_safeOffs[slot] != default) + hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.Center + _safeOffs[slot], new WDir(0, 1), Module.Bounds.MapResolution, Module.Bounds.MapResolution, Module.Bounds.MapResolution)); + } public override void DrawArenaForeground(int pcSlot, Actor pc) { - if (_safespots[pcSlot] != default) - Arena.AddCircle(_safespots[pcSlot], 1, ArenaColor.Safe); + if (_safeOffs[pcSlot] != default) + Arena.AddCircle(Module.Center + _safeOffs[pcSlot], 1, ArenaColor.Safe); } public override void OnCastStarted(Actor caster, ActorCastInfo spell) @@ -81,8 +92,11 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) switch ((AID)spell.Action.ID) { case AID.IcicleImpact: - _conesAtCardinals ??= IsCardinal(caster.Position - Module.Center); - InitIfReady(); + if (_conesAtCardinals == null) + { + _conesAtCardinals = IsCardinal(caster.Position - Module.Center); + InitIfReady(); + } break; case AID.AxeKick: _out = true; @@ -95,11 +109,30 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) } } + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.AxeKick: + // out done => cone baiters go in, ice baiters stay + for (int i = 0; i < _safeOffs.Length; ++i) + if (_safeOffs[i] != default && Raid[i]?.Class.IsSupport() == _supportsBaitCones) + _safeOffs[i] = 4 * _safeOffs[i].Normalized(); + break; + case AID.ScytheKick: + // in done => cone baiters stay, ice baiters go out + for (int i = 0; i < _safeOffs.Length; ++i) + if (_safeOffs[i] != default && Raid[i]?.Class.IsSupport() != _supportsBaitCones) + _safeOffs[i] = 8 * _safeOffs[i].Normalized(); + break; + } + } + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) { - if (iconID == (uint)IconID.FrigidStone) + if (iconID == (uint)IconID.FrigidStone && _supportsBaitCones == null) { - _supportsBaitCones ??= actor.Class.IsDD(); + _supportsBaitCones = actor.Class.IsDD(); InitIfReady(); } } @@ -118,7 +151,7 @@ private void InitIfReady() var dir = 180.Degrees() - (group & 3) * 90.Degrees(); dir += support ? offsetTH : offsetDD; var radius = (_out.Value ? 16 : 0) + (baitCone ? 1 : 3); - _safespots[slot] = Module.Center + radius * dir.ToDirection(); + _safeOffs[slot] = radius * dir.ToDirection(); } } @@ -127,11 +160,40 @@ private void InitIfReady() class P2HeavenlyStrike(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.HeavenlyStrike)) { - private readonly DateTime _activation = module.WorldState.FutureTime(3.9f); + private readonly FRUConfig _config = Service.Config.Get(); + private readonly WDir[] _safeDirs = new WDir[PartyState.MaxPartySize]; + private DateTime _activation; public override IEnumerable Sources(int slot, Actor actor) { - yield return new(Module.Center, 12, _activation); + if (_activation != default) + yield return new(Module.Center, 12, _activation); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_safeDirs[slot] != default) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + 6 * _safeDirs[slot], 1), _activation); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + base.DrawArenaForeground(pcSlot, pc); + if (_safeDirs[pcSlot] != default) + Arena.AddCircle(Module.Center + 18 * _safeDirs[pcSlot], 1, ArenaColor.Safe); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.IcicleImpact && _activation == default) + { + _activation = WorldState.FutureTime(3.9f); + var safeDir = (caster.Position - Module.Center).Normalized(); + if (safeDir.X > 0.5f || safeDir.Z > 0.8f) + safeDir = -safeDir; // G1 + foreach (var (slot, group) in _config.P2DiamondDustKnockbacks.Resolve(Raid)) + _safeDirs[slot] = group == 1 ? -safeDir : safeDir; + } } } @@ -140,6 +202,8 @@ class P2SinboundHoly(BossModule module) : Components.UniformStackSpread(module, public int NumCasts; private DateTime _nextExplosion; + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // resolves automatically + public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.SinboundHoly) @@ -160,6 +224,40 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) class P2SinboundHolyVoidzone(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.SinboundHolyVoidzone).Where(z => z.EventState != 7)); +class P2SinboundHolyAIBait(BossModule module) : BossComponent(module) +{ + private WDir _finalDir; // if oracle jumps directly to one of the safespots, both groups run opposite in one (arbitrary, CW) direction, and the one that ends up behind boss slides across - in that case this is kept zeroed + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // stay on border + hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 18)); + + // and move towards safety (CW is arbitrary) + var preferredDir = _finalDir != default ? _finalDir : (actor.Position - Module.Center).Normalized().OrthoR(); + hints.AddForbiddenZone(ShapeDistance.HalfPlane(Module.Center - 2 * preferredDir, preferredDir)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + // note: we assume that when this component is activated, only last pair of icicles remain (orthogonal to original safe spots), and oracle is already at position + if ((AID)spell.Action.ID == AID.IcicleImpact && Module.Enemies(OID.OraclesReflection).FirstOrDefault() is var oracle && oracle != null && _finalDir == default) + { + var destDir = (caster.Position - Module.Center).Normalized(); + var idealDir = (Module.Center - oracle.Position).Normalized(); + var dot = destDir.Dot(idealDir); + if (dot < 0) + { + destDir = -destDir; + dot = -dot; + } + if (dot > 0.5f) + _finalDir = destDir; + // else: single direction mode + } + } +} + class P2ShiningArmor(BossModule module) : Components.GenericGaze(module, ActionID.MakeSpell(AID.ShiningArmor)) { private Actor? _source; @@ -183,19 +281,60 @@ public override void OnActorPlayActionTimelineEvent(Actor actor, ushort id) class P2TwinStillnessSilence(BossModule module) : Components.GenericAOEs(module) { - private readonly Actor? _source = module.Enemies(OID.OraclesReflection).FirstOrDefault(); public readonly List AOEs = []; + public bool EnableAIHints; + private readonly Actor? _source = module.Enemies(OID.OraclesReflection).FirstOrDefault(); + private BitMask _thinIce; private readonly AOEShapeCone _shapeFront = new(30, 135.Degrees()); private readonly AOEShapeCone _shapeBack = new(30, 45.Degrees()); public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs.Take(1); + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (!EnableAIHints || _source == null) + return; + + if (!_thinIce[slot]) + { + // preposition + // this is a bit hacky - we need to stay either far away from boss, or close (and slide over at the beginning of the ice) + hints.AddForbiddenZone(ShapeDistance.Rect(_source.Position, _source.Rotation, 25, 5, 20)); + return; + } + + // at this point, we have thin ice, so we can either stay or move fixed distance + hints.AddForbiddenZone(ShapeDistance.Donut(actor.Position, 1, 31)); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(actor.Position, 33)); + + if (AOEs.Count == 0) + { + // if we're behind boss, slide over + hints.AddForbiddenZone(ShapeDistance.Rect(_source.Position, _source.Rotation, 10, 20, 20)); + } + else + { + // otherwise just dodge next aoe + ref var nextAOE = ref AOEs.Ref(0); + hints.AddForbiddenZone(nextAOE.Shape.Distance(nextAOE.Origin, nextAOE.Rotation), nextAOE.Activation); + } + } + public override void DrawArenaForeground(int pcSlot, Actor pc) { Arena.Actor(_source, ArenaColor.Object, true); if (AOEs.Count > 0) - Arena.AddCircle(pc.Position, 32, ArenaColor.Safe); + { + Arena.AddCircle(pc.Position, 32, ArenaColor.Vulnerable); + Arena.AddLine(pc.Position, pc.Position - 32 * WorldState.Client.CameraAzimuth.ToDirection(), ArenaColor.Vulnerable); + } + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.ThinIce) + _thinIce.Set(Raid.FindSlot(actor.InstanceID)); } public override void OnCastStarted(Actor caster, ActorCastInfo spell) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs index 79e88c3d6a..ec6c2c8e5c 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs @@ -265,7 +265,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) } } -class P3DarkestDanceKnockback(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.DarkestDanceKnockback), true) // TODO: verify whether it ignores immunes +class P3DarkestDanceKnockback(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.DarkestDanceKnockback), true) { private Actor? _source; private DateTime _activation; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs new file mode 100644 index 0000000000..a03aed8ee4 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs @@ -0,0 +1,16 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P4AhkRhai(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.AkhRhaiAOE)) +{ + public readonly List AOEs = []; + + private static readonly AOEShapeCircle _shape = new(4); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.AkhRhai) + AOEs.Add(new(_shape, spell.LocXZ, default, Module.CastFinishAt(spell))); + } +} From 2c67eabd645b637d6734b5e37a14fa119b191084 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 22 Dec 2024 23:54:54 +0000 Subject: [PATCH 07/12] Apply target offset only if resolve was successful. --- BossMod/Autorotation/RotationModuleManager.cs | 4 +-- .../Extreme/Ex3Titan/Ex3TitanAI.cs | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/BossMod/Autorotation/RotationModuleManager.cs b/BossMod/Autorotation/RotationModuleManager.cs index 0931d4e977..fd4db19ac1 100644 --- a/BossMod/Autorotation/RotationModuleManager.cs +++ b/BossMod/Autorotation/RotationModuleManager.cs @@ -119,8 +119,8 @@ public void Update(float estimatedAnimLockDelay, bool isMoving) public WPos ResolveTargetLocation(StrategyTarget strategy, int param, float off1, float off2) => strategy switch { StrategyTarget.PointAbsolute => new(off1, off2), - StrategyTarget.PointCenter or StrategyTarget.Automatic => (Bossmods.ActiveModule?.Center ?? Player?.Position ?? default) + off1 * off2.Degrees().ToDirection(), - _ => (ResolveTargetOverride(strategy, param)?.Position ?? Player?.Position ?? default) + off1 * off2.Degrees().ToDirection(), + StrategyTarget.PointCenter or StrategyTarget.Automatic => (Bossmods.ActiveModule?.Center + off1 * off2.Degrees().ToDirection()) ?? Player?.Position ?? default, + _ => (ResolveTargetOverride(strategy, param)?.Position + off1 * off2.Degrees().ToDirection()) ?? Player?.Position ?? default, }; private Plan? CalculateExpectedPlan() diff --git a/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs b/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs index 9f04dedb93..f025f2000b 100644 --- a/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs +++ b/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs @@ -1,4 +1,36 @@ -namespace BossMod.RealmReborn.Extreme.Ex3Titan; +using BossMod.AI; +using BossMod.Autorotation; +using BossMod.Pathfinding; + +namespace BossMod.RealmReborn.Extreme.Ex3Titan; + +sealed class Ex3TitanAIRotation(RotationModuleManager manager, Actor player) : AIRotationModule(manager, player) +{ + public enum Track { Movement } + public enum MovementStrategy { None, Pathfind, Explicit } + + public static RotationModuleDefinition Definition() + { + var res = new RotationModuleDefinition("AI Experiment", "Experimental encounter-specific rotation", "Encounter AI", "veyn", RotationModuleQuality.WIP, new(~1ul), 1000, 1, typeof(Ex3Titan)); + res.Define(Track.Movement).As("Movement", "Movement") + .AddOption(MovementStrategy.None, "None", "No automatic movement") + .AddOption(MovementStrategy.Pathfind, "Pathfind", "Use standard pathfinding to move") + .AddOption(MovementStrategy.Explicit, "Explicit", "Move to specific point", supportedTargets: ActionTargets.Area); + return res; + } + + public override void Execute(StrategyValues strategy, Actor? primaryTarget, float estimatedAnimLockDelay, bool isMoving) + { + SetForcedMovement(CalculateDestination(strategy.Option(Track.Movement))); + } + + private WPos CalculateDestination(StrategyValues.OptionRef strategy) => strategy.As() switch + { + MovementStrategy.Pathfind => NavigationDecision.Build(NavigationContext, World, Hints, Player, Speed()).Destination ?? Player.Position, + MovementStrategy.Explicit => ResolveTargetLocation(strategy.Value), + _ => Player.Position + }; +} class Ex3TitanAI(BossModule module) : BossComponent(module) { From 5e63ce41c2c032345f30f93fe39a484f5da3da94 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Mon, 23 Dec 2024 00:45:34 +0000 Subject: [PATCH 08/12] Precise positioning support. --- .../Dawntrail/Ultimate/FRU/P1FallOfFaith.cs | 5 +---- .../Dawntrail/Ultimate/FRU/P1UtopianSky.cs | 11 ---------- .../Dawntrail/Ultimate/FRU/P2DiamondDust.cs | 12 ++++++++--- BossMod/Pathfinding/NavigationDecision.cs | 8 ++++++- BossMod/Util/ShapeDistance.cs | 21 +++++++++++++++++++ 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs index fe2e009ac7..8c32d2936b 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs @@ -52,10 +52,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme var dest = TetherSpot(baitOrder); if (_playerOrder[slot] != baitOrder) dest += BaitOffset(_playerOrder[slot], _fireTethers[baitOrder - 1]); - // note: the baits need to be very precise - var destCellSize = 0.5f * Module.Bounds.MapResolution; - var destCellCenter = Module.Center + ((dest - Module.Center) / Module.Bounds.MapResolution).Floor() * Module.Bounds.MapResolution + new WDir(destCellSize, destCellSize); - hints.AddForbiddenZone(p => p.AlmostEqual(destCellCenter, destCellSize) ? 1 : -1); + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(dest, new(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f)); } public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot, Actor player, ref uint customColor) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs index 8c57af4535..6dc8a6b9e6 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs @@ -113,17 +113,6 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme var assignedDirection = (180 - 45 * clockspot).Degrees(); hints.AddForbiddenZone(ShapeDistance.InvertedCone(Module.Center, 50, assignedDirection, 5.Degrees()), DateTime.MaxValue); } - - //if (assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.OT) - //{ - // // adjust slightly to neighbouring tank slot - // var coTankSpot = _config.P1UtopianSkyInitialSpots[assignment == PartyRolesConfig.Assignment.MT ? PartyRolesConfig.Assignment.OT : PartyRolesConfig.Assignment.MT]; - // if (coTankSpot == ((clockspot + 1) & 7)) - // assignedDirection -= 4.Degrees(); - // else if (coTankSpot == ((clockspot + 7) & 7)) - // assignedDirection += 4.Degrees(); - //} - //hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + 18 * assignedDirection.ToDirection(), 1)); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs index 8c31217e50..1695be3f29 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs @@ -34,14 +34,20 @@ public override void Update() public override void AddHints(int slot, Actor actor, TextHints hints) { + if (CurrentBaits.Count == 0) + return; + + var baitIndex = CurrentBaits.FindIndex(b => b.Target == actor); if (ForbiddenPlayers[slot]) { - if (ActiveBaitsOn(actor).Any()) + if (baitIndex >= 0) hints.Add("Stay farther away!"); } else { - if (ActiveBaitsOn(actor).Any(b => PlayersClippedBy(b).Any())) + if (baitIndex < 0) + hints.Add("Stay closer to bait!"); + else if (PlayersClippedBy(CurrentBaits[baitIndex]).Any()) hints.Add("Bait cone away from raid!"); } @@ -78,7 +84,7 @@ class P2DiamondDustSafespots(BossModule module) : BossComponent(module) public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { if (_safeOffs[slot] != default) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.Center + _safeOffs[slot], new WDir(0, 1), Module.Bounds.MapResolution, Module.Bounds.MapResolution, Module.Bounds.MapResolution)); + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(Module.Center + _safeOffs[slot], new WDir(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f)); } public override void DrawArenaForeground(int pcSlot, Actor pc) diff --git a/BossMod/Pathfinding/NavigationDecision.cs b/BossMod/Pathfinding/NavigationDecision.cs index 0dc7769464..2dd1e2dd96 100644 --- a/BossMod/Pathfinding/NavigationDecision.cs +++ b/BossMod/Pathfinding/NavigationDecision.cs @@ -196,7 +196,13 @@ private static (WPos? destination, float turn) GetFirstWaypoint(ThetaStar pf, Ma ref var node = ref pf.NodeByIndex(cell); if (pf.NodeByIndex(node.ParentIndex).GScore == 0) { - var dest = pf.CellCenter(cell); + //var dest = pf.CellCenter(cell); + // if destination coord matches player coord, do not move along that coordinate, this is used for precise positioning + var destCoord = map.IndexToGrid(cell); + var playerCoordFrac = map.WorldToGridFrac(startingPos); + var playerCoord = map.FracToGrid(playerCoordFrac); + var dest = map.GridToWorld(destCoord.x, destCoord.y, destCoord.x == playerCoord.x ? playerCoordFrac.X - playerCoord.x : 0.5f, destCoord.y == playerCoord.y ? playerCoordFrac.Y - playerCoord.y : 0.5f); + var next = pf.CellCenter(nextCell); return (dest, (dest - startingPos).OrthoL().Dot(next - dest)); } diff --git a/BossMod/Util/ShapeDistance.cs b/BossMod/Util/ShapeDistance.cs index 6b8f79afa8..650d8bece8 100644 --- a/BossMod/Util/ShapeDistance.cs +++ b/BossMod/Util/ShapeDistance.cs @@ -201,4 +201,25 @@ Func edge((WPos p1, WPos p2) e) public static Func Intersection(List> funcs, float offset = 0) => p => funcs.Max(e => e(p)) - offset; public static Func Union(List> funcs, float offset = 0) => p => funcs.Min(e => e(p)) - offset; + + // special distance function for precise positioning, finer than map resolution + // it's an inverted rect of a size equal to one grid cell, with a special adjustment if starting position is in the same cell, but farther than tolerance + public static Func PrecisePosition(WPos origin, WDir dir, float cellSize, WPos starting, float tolerance) + { + var delta = starting - origin; + var dparr = delta.Dot(dir); + if (dparr > tolerance && dparr <= cellSize) + origin -= cellSize * dir; + else if (dparr < -tolerance && dparr >= -cellSize) + origin += cellSize * dir; + + var normal = dir.OrthoL(); + var dortho = delta.Dot(normal); + if (dortho > tolerance && dortho <= cellSize) + origin -= cellSize * normal; + else if (dortho < -tolerance && dortho >= -cellSize) + origin += cellSize * normal; + + return InvertedRect(origin, dir, cellSize, cellSize, cellSize); + } } From c1a229f8f2c956a3c2a02a2b33dda64fe0c9adca Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Mon, 23 Dec 2024 22:40:06 +0000 Subject: [PATCH 09/12] FRU WIP --- BossMod/Data/Actor.cs | 1 + .../Dawntrail/Ultimate/FRU/FRUStates.cs | 12 +- .../Dawntrail/Ultimate/FRU/P2DiamondDust.cs | 204 +++++++++++++----- BossMod/Util/Angle.cs | 11 + BossMod/Util/ArcList.cs | 10 +- BossMod/Util/WPosDir.cs | 4 +- 6 files changed, 174 insertions(+), 68 deletions(-) diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index d3a31f83d0..2eabfd068a 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -105,6 +105,7 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public ClassCategory ClassCategory => Class.GetClassCategory(); public WPos Position => new(PosRot.X, PosRot.Z); public WPos PrevPosition => new(PrevPosRot.X, PrevPosRot.Z); + public WDir LastFrameMovement => Position - PrevPosition; public Angle Rotation => PosRot.W.Radians(); public bool Omnidirectional => Utils.CharacterIsOmnidirectional(OID); public bool IsDeadOrDestroyed => IsDead || IsDestroyed; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index 792a32e3d0..3bd601b685 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -221,7 +221,6 @@ private void P2DiamondDust(uint id, float delay) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ExecOnEnter(comp => comp.Risky = false) // it's fine to bait stuff into these aoes... .SetHint(StateMachine.StateHint.DowntimeStart); Condition(id + 0x30, 5.7f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "In/out") .DeactivateOnExit() @@ -232,10 +231,9 @@ private void P2DiamondDust(uint id, float delay) ComponentCondition(id + 0x32, 1.6f, comp => comp.NumCasts > 0, "Ice baits") .DeactivateOnExit() .DeactivateOnExit(); - ComponentCondition(id + 0x33, 0.4f, comp => comp.NumCasts > 0, "Ice circle 1") - .ActivateOnEnter() // activate a bit early, so that it can observe first circle direction - .ExecOnEnter(comp => comp.Risky = true); + ComponentCondition(id + 0x33, 0.4f, comp => comp.NumCasts > 0, "Ice circle 1"); ComponentCondition(id + 0x40, 3.9f, comp => comp.NumCasts > 0, "Knockback") + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() // show the cone caster early, to simplify finding movement direction... @@ -248,16 +246,14 @@ private void P2DiamondDust(uint id, float delay) .ExecOnEnter(comp => comp.Risky = true) .DeactivateOnExit() .DeactivateOnExit(); - ComponentCondition(id + 0x60, 1.3f, comp => comp.NumCasts > 0) - .ActivateOnEnter(); + ComponentCondition(id + 0x60, 1.3f, comp => comp.NumCasts > 0); ComponentCondition(id + 0x70, 4.7f, comp => comp.NumCasts >= 4) .ActivateOnEnter() .ActivateOnEnter() .DeactivateOnExit() // last icicle explodes together with first stack - .DeactivateOnExit() .DeactivateOnExit(); ComponentCondition(id + 0x80, 3.7f, comp => comp.NumCasts > 0, "Gaze") - .ExecOnEnter(comp => comp.EnableAIHints = true) + .ExecOnEnter(comp => comp.EnableAIHints()) .DeactivateOnExit(); ComponentCondition(id + 0x90, 3.0f, comp => comp.AOEs.Count > 0); ComponentCondition(id + 0x91, 3.5f, comp => comp.NumCasts > 0, "Front/back"); diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs index 1695be3f29..f25b59ae13 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs @@ -2,7 +2,49 @@ class P2AxeKick(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AxeKick), new AOEShapeCircle(16)); class P2ScytheKick(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ScytheKick), new AOEShapeDonut(4, 20)); -class P2IcicleImpact(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IcicleImpact), new AOEShapeCircle(10)); + +class P2IcicleImpact(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.IcicleImpact)) +{ + public readonly List AOEs = []; // note: we don't remove finished aoes, since we use them in other components to detect safespots + + private static readonly AOEShapeCircle _shape = new(10); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs.Skip(NumCasts); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == WatchedAction) + { + // initially all aoes start as non-risky + AOEs.Add(new(_shape, caster.Position, default, Module.CastFinishAt(spell), 0, false)); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.IcicleImpact: + ++NumCasts; + break; + case AID.HouseOfLight: + // after proteans are baited, first two aoes become risky; remaining are still not - stones are supposed to be baited into them + MarkAsRisky(0, Math.Min(2, AOEs.Count)); + break; + case AID.FrigidStone: + // after stones are baited, all aoes should be marked as risky + MarkAsRisky(2, AOEs.Count); + break; + } + } + + private void MarkAsRisky(int start, int end) + { + for (int i = start; i < end; ++i) + AOEs.Ref(i).Risky = true; + } +} + class P2FrigidNeedleCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FrigidNeedleCircle), new AOEShapeCircle(5)); class P2FrigidNeedleCross(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.FrigidNeedleCross), new AOEShapeCross(40, 2.5f)); @@ -166,20 +208,18 @@ private void InitIfReady() class P2HeavenlyStrike(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.HeavenlyStrike)) { - private readonly FRUConfig _config = Service.Config.Get(); - private readonly WDir[] _safeDirs = new WDir[PartyState.MaxPartySize]; - private DateTime _activation; + private readonly WDir[] _safeDirs = BuildSafeDirs(module); + private readonly DateTime _activation = module.WorldState.FutureTime(3.9f); public override IEnumerable Sources(int slot, Actor actor) { - if (_activation != default) - yield return new(Module.Center, 12, _activation); + yield return new(Module.Center, 12, _activation); } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { if (_safeDirs[slot] != default) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + 6 * _safeDirs[slot], 1), _activation); + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(Module.Center + 6 * _safeDirs[slot], new(1, 0), Module.Bounds.MapResolution, actor.Position, 0.25f), _activation); } public override void DrawArenaForeground(int pcSlot, Actor pc) @@ -189,17 +229,19 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) Arena.AddCircle(Module.Center + 18 * _safeDirs[pcSlot], 1, ArenaColor.Safe); } - public override void OnCastFinished(Actor caster, ActorCastInfo spell) + private static WDir[] BuildSafeDirs(BossModule module) { - if ((AID)spell.Action.ID == AID.IcicleImpact && _activation == default) + var res = new WDir[PartyState.MaxPartySize]; + var icicle = module.FindComponent(); + if (icicle?.AOEs.Count > 0) { - _activation = WorldState.FutureTime(3.9f); - var safeDir = (caster.Position - Module.Center).Normalized(); + var safeDir = (icicle.AOEs[0].Origin - module.Center).Normalized(); if (safeDir.X > 0.5f || safeDir.Z > 0.8f) safeDir = -safeDir; // G1 - foreach (var (slot, group) in _config.P2DiamondDustKnockbacks.Resolve(Raid)) - _safeDirs[slot] = group == 1 ? -safeDir : safeDir; + foreach (var (slot, group) in Service.Config.Get().P2DiamondDustKnockbacks.Resolve(module.Raid)) + res[slot] = group == 1 ? -safeDir : safeDir; } + return res; } } @@ -207,8 +249,54 @@ class P2SinboundHoly(BossModule module) : Components.UniformStackSpread(module, { public int NumCasts; private DateTime _nextExplosion; + private readonly WDir _destinationDir = CalculateDestination(module); - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // resolves automatically + private static WDir CalculateDestination(BossModule module) + { + // if oracle jumps directly to one of the initial safespots, both groups run opposite in one (arbitrary, CW) direction, and the one that ends up behind boss slides across - in that case we return zero destination + // note: we assume that when this is called oracle is already at position + var icicles = module.FindComponent(); + var oracle = module.Enemies(OID.OraclesReflection).FirstOrDefault(); + if (icicles == null || icicles.AOEs.Count == 0 || oracle == null) + return default; + + var idealDir = (module.Center - oracle.Position).Normalized(); // ideally we wanna stay as close as possible to across the oracle + var destDir = (icicles.AOEs[0].Origin - module.Center).Normalized().OrthoL(); // actual destination is one of the last icicles + return destDir.Dot(idealDir) switch + { + > 0.5f => destDir, + < -0.5f => -destDir, + _ => default, // fast movement mode + }; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var master = actor.Role != Role.Healer ? Stacks.MinBy(s => (s.Target.Position - actor.Position).LengthSq()).Target : null; + if (master != null && ((master.Position - actor.Position).LengthSq() > 100 || (master.Position - Module.Center).LengthSq() < 196)) + master = null; // our closest healer is too far away or too close to center, something is wrong (maybe kb didn't finish yet, or healer fucked up) + + if (master != null) + { + // non-healers should just stack with whatever closest healer is + var moveDir = master.LastFrameMovement.Normalized(); + var capsule = ShapeDistance.Capsule(master.Position + 2 * moveDir, moveDir, 4, 2); + hints.AddForbiddenZone(p => -capsule(p), DateTime.MaxValue); + } + + // note: other hints have to be 'later' than immediate (to make getting out of voidzones higher prio), but 'earlier' than stack-with-healer: + // healer's position is often overlapped by new voidzones, if healer is moving slowly - in that case we still need to dodge in correct direction + var hintTime = WorldState.FutureTime(50); + + // stay near border + hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 16), hintTime); + + // prefer moving towards safety (CW is arbitrary) + var moveQuickly = _destinationDir == default; + var preferredDir = !moveQuickly ? _destinationDir : (actor.Position - Module.Center).Normalized().OrthoR(); + var planeOffset = moveQuickly && master == null && NumCasts > 0 ? 2 : -2; // if we're moving quickly, mark our current spot as forbidden (don't bother if we have master or waiting for first cast, though) + hints.AddForbiddenZone(ShapeDistance.HalfPlane(Module.Center + planeOffset * preferredDir, preferredDir), hintTime); + } public override void OnCastStarted(Actor caster, ActorCastInfo spell) { @@ -230,40 +318,6 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) class P2SinboundHolyVoidzone(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.SinboundHolyVoidzone).Where(z => z.EventState != 7)); -class P2SinboundHolyAIBait(BossModule module) : BossComponent(module) -{ - private WDir _finalDir; // if oracle jumps directly to one of the safespots, both groups run opposite in one (arbitrary, CW) direction, and the one that ends up behind boss slides across - in that case this is kept zeroed - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - // stay on border - hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 18)); - - // and move towards safety (CW is arbitrary) - var preferredDir = _finalDir != default ? _finalDir : (actor.Position - Module.Center).Normalized().OrthoR(); - hints.AddForbiddenZone(ShapeDistance.HalfPlane(Module.Center - 2 * preferredDir, preferredDir)); - } - - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - // note: we assume that when this component is activated, only last pair of icicles remain (orthogonal to original safe spots), and oracle is already at position - if ((AID)spell.Action.ID == AID.IcicleImpact && Module.Enemies(OID.OraclesReflection).FirstOrDefault() is var oracle && oracle != null && _finalDir == default) - { - var destDir = (caster.Position - Module.Center).Normalized(); - var idealDir = (Module.Center - oracle.Position).Normalized(); - var dot = destDir.Dot(idealDir); - if (dot < 0) - { - destDir = -destDir; - dot = -dot; - } - if (dot > 0.5f) - _finalDir = destDir; - // else: single direction mode - } - } -} - class P2ShiningArmor(BossModule module) : Components.GenericGaze(module, ActionID.MakeSpell(AID.ShiningArmor)) { private Actor? _source; @@ -288,25 +342,56 @@ public override void OnActorPlayActionTimelineEvent(Actor actor, ushort id) class P2TwinStillnessSilence(BossModule module) : Components.GenericAOEs(module) { public readonly List AOEs = []; - public bool EnableAIHints; private readonly Actor? _source = module.Enemies(OID.OraclesReflection).FirstOrDefault(); private BitMask _thinIce; + private P2SinboundHolyVoidzone? _voidzones; // used for hints only + private const float SlideDistance = 32; private readonly AOEShapeCone _shapeFront = new(30, 135.Degrees()); private readonly AOEShapeCone _shapeBack = new(30, 45.Degrees()); + public void EnableAIHints() + { + _voidzones = Module.FindComponent(); + } + public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs.Take(1); public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (!EnableAIHints || _source == null) + if (_voidzones == null || _source == null) return; if (!_thinIce[slot]) { // preposition // this is a bit hacky - we need to stay either far away from boss, or close (and slide over at the beginning of the ice) - hints.AddForbiddenZone(ShapeDistance.Rect(_source.Position, _source.Rotation, 25, 5, 20)); + // the actual shape is quite complicated ('primary' shape is a set of points at distance X from a cone behind boss, 'secondary' is a set of points at distance X from primary), so we use a rough approximation + + // first, find a set of allowed angles along the border + var zoneList = new ArcList(Module.Center, 17); + foreach (var z in _voidzones.Sources(Module)) + zoneList.ForbidCircle(z.Position, _voidzones.Shape.Radius); + + // now find closest allowed zone + var actorDir = Angle.FromDirection(actor.Position - Module.Center); + var closest = zoneList.Allowed(5.Degrees()).MinBy(z => actorDir.DistanceToRange(z.min, z.max).Abs().Rad); + if (closest != default) + { + var desiredDir = (closest.min + closest.max) * 0.5f; + var halfWidth = (closest.max - closest.min) * 0.5f; + if (halfWidth.Deg > 5) + { + // destination is very wide, narrow it down a bit to be in line with the boss + halfWidth = 5.Degrees(); + var sourceDir = Angle.FromDirection(_source.Position - Module.Center); + var sourceDist = sourceDir.DistanceToRange(closest.min + halfWidth, closest.max - halfWidth); + var oppositeDist = (sourceDir + 180.Degrees()).DistanceToRange(closest.min + halfWidth, closest.max - halfWidth); + desiredDir = oppositeDist.Abs().Rad < sourceDist.Abs().Rad ? (sourceDir + 180.Degrees() + oppositeDist) : (sourceDir + sourceDist); + } + hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 16), WorldState.FutureTime(50)); + hints.AddForbiddenZone(ShapeDistance.InvertedCone(Module.Center, 100, desiredDir, halfWidth), DateTime.MaxValue); + } return; } @@ -317,7 +402,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (AOEs.Count == 0) { // if we're behind boss, slide over - hints.AddForbiddenZone(ShapeDistance.Rect(_source.Position, _source.Rotation, 10, 20, 20)); + hints.AddForbiddenZone(ShapeDistance.Rect(_source.Position, _source.Rotation, 20, 20, 20), DateTime.MaxValue); } else { @@ -325,15 +410,24 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme ref var nextAOE = ref AOEs.Ref(0); hints.AddForbiddenZone(nextAOE.Shape.Distance(nextAOE.Origin, nextAOE.Rotation), nextAOE.Activation); } + + // ensure we don't slide over voidzones + foreach (var z in _voidzones.Sources(Module)) + { + var offset = z.Position - actor.Position; + var dist = offset.Length(); + if (dist > _voidzones.Shape.Radius) + hints.AddForbiddenZone(ShapeDistance.Cone(actor.Position, 100, Angle.FromDirection(offset), Angle.Asin(dist / _voidzones.Shape.Radius))); + } } public override void DrawArenaForeground(int pcSlot, Actor pc) { Arena.Actor(_source, ArenaColor.Object, true); - if (AOEs.Count > 0) + if (_thinIce[pcSlot]) { - Arena.AddCircle(pc.Position, 32, ArenaColor.Vulnerable); - Arena.AddLine(pc.Position, pc.Position - 32 * WorldState.Client.CameraAzimuth.ToDirection(), ArenaColor.Vulnerable); + Arena.AddCircle(pc.Position, SlideDistance, ArenaColor.Vulnerable); + Arena.AddLine(pc.Position, pc.Position - SlideDistance * WorldState.Client.CameraAzimuth.ToDirection(), ArenaColor.Vulnerable); } } diff --git a/BossMod/Util/Angle.cs b/BossMod/Util/Angle.cs index bc08d265fb..c30cf436fc 100644 --- a/BossMod/Util/Angle.cs +++ b/BossMod/Util/Angle.cs @@ -37,6 +37,17 @@ public readonly Angle Normalized() public readonly bool AlmostEqual(Angle other, float epsRad) => Math.Abs((this - other).Normalized().Rad) <= epsRad; + // closest distance to move from this angle to destination (== 0 if equal, >0 if moving in positive/CCW dir, <0 if moving in negative/CW dir) + public readonly Angle DistanceToAngle(Angle other) => (other - this).Normalized(); + + // returns 0 if angle is within range, positive value if min is closest, negative if max is closest + public readonly Angle DistanceToRange(Angle min, Angle max) + { + var width = (max - min) * 0.5f; + var midDist = DistanceToAngle((min + max) * 0.5f); + return midDist.Rad > width.Rad ? midDist - width : midDist.Rad < -width.Rad ? midDist + width : default; + } + public override readonly string ToString() => Deg.ToString("f0"); } diff --git a/BossMod/Util/ArcList.cs b/BossMod/Util/ArcList.cs index bfce988716..fae65a9630 100644 --- a/BossMod/Util/ArcList.cs +++ b/BossMod/Util/ArcList.cs @@ -40,8 +40,12 @@ public void ForbidCircle(WPos origin, float radius) { var oo = origin - Center; var center = Angle.FromDirection(oo); - var halfWidth = MathF.Acos((oo.LengthSq() + Radius * Radius - radius * radius) / (2 * oo.Length() * Radius)); - ForbidArcByLength(center, halfWidth.Radians()); + var cos = (oo.LengthSq() + Radius * Radius - radius * radius) / (2 * oo.Length() * Radius); + if (cos is <= 1 and >= -1) + { + var halfWidth = MathF.Acos(cos); + ForbidArcByLength(center, halfWidth.Radians()); + } } public void ForbidInfiniteRect(WPos origin, Angle dir, float halfWidth) @@ -55,7 +59,7 @@ public void ForbidInfiniteRect(WPos origin, Angle dir, float halfWidth) ForbidArc(i2.Item2, i1.Item2); } - public IEnumerable<(Angle, Angle)> Allowed(Angle cushion) + public IEnumerable<(Angle min, Angle max)> Allowed(Angle cushion) { if (Forbidden.Segments.Count == 0) { diff --git a/BossMod/Util/WPosDir.cs b/BossMod/Util/WPosDir.cs index e1745a7dca..ec8a055606 100644 --- a/BossMod/Util/WPosDir.cs +++ b/BossMod/Util/WPosDir.cs @@ -27,8 +27,8 @@ public WDir(Vector2 v) : this(v.X, v.Y) { } public readonly WDir Rotate(Angle dir) => Rotate(dir.ToDirection()); public readonly float LengthSq() => X * X + Z * Z; public readonly float Length() => MathF.Sqrt(LengthSq()); - public static WDir Normalize(WDir a) => a / a.Length(); - public readonly WDir Normalized() => this / Length(); + public static WDir Normalize(WDir a, float zeroThreshold = 0) => a.Length() is var len && len > zeroThreshold ? a / len : default; + public readonly WDir Normalized(float zeroThreshold = 0) => Normalize(this, zeroThreshold); public static bool AlmostZero(WDir a, float eps) => Math.Abs(a.X) <= eps && Math.Abs(a.Z) <= eps; public readonly bool AlmostZero(float eps) => AlmostZero(this, eps); public static bool AlmostEqual(WDir a, WDir b, float eps) => AlmostZero(a - b, eps); From 82618b0299ebf1024a72f19eb46dc2bd9051a965 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Thu, 26 Dec 2024 16:39:02 +0000 Subject: [PATCH 10/12] FRU & chaotic WIP --- .../ActivePivotParticleBeam.cs | 24 ++ .../Ch01CloudOfDarkness/BladeOfDarkness.cs | 29 +++ .../Chaotic/Ch01CloudOfDarkness/Break.cs | 20 ++ .../Ch01CloudOfDarkness.cs | 189 ++++++++++++++ .../Ch01CloudOfDarknessEnums.cs | 199 +++++++++++++++ .../ChaosCondensedParticleBeam.cs | 21 ++ .../Ch01CloudOfDarkness/CurseOfDarkness.cs | 31 +++ .../DiffusiveForceParticleBeam.cs | 18 ++ .../Chaotic/Ch01CloudOfDarkness/Enaero.cs | 109 ++++++++ .../Chaotic/Ch01CloudOfDarkness/Endeath.cs | 116 +++++++++ .../Ch01CloudOfDarkness/GrimEmbrace.cs | 68 +++++ .../Chaotic/Ch01CloudOfDarkness/Phaser.cs | 15 ++ .../RapidSequenceParticleBeam.cs | 35 +++ .../RazingVolleyParticleBeam.cs | 16 ++ .../Ch01CloudOfDarkness/ThirdArtOfDarkness.cs | 105 ++++++++ .../Ch01CloudOfDarkness/UnholyDarkness.cs | 14 + BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs | 2 + .../Dawntrail/Ultimate/FRU/FRUConfig.cs | 5 + .../Dawntrail/Ultimate/FRU/FRUEnums.cs | 31 ++- .../Dawntrail/Ultimate/FRU/FRUStates.cs | 78 +++++- .../Dawntrail/Ultimate/FRU/P1Blastburn.cs | 7 + .../Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs | 9 +- .../Dawntrail/Ultimate/FRU/P4AkhMorn.cs | 18 ++ .../FRU/{P4AhkRhai.cs => P4AkhRhai.cs} | 2 +- .../Ultimate/FRU/P4CrystallizeTime.cs | 6 + .../Ultimate/FRU/P4DarklitDragonsong.cs | 241 ++++++++++++++++++ .../Dawntrail/Ultimate/FRU/P4MornAfah.cs | 35 +++ 27 files changed, 1432 insertions(+), 11 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/BladeOfDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ChaosCondensedParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/GrimEmbrace.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Phaser.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RapidSequenceParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RazingVolleyParticleBeam.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ThirdArtOfDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/UnholyDarkness.cs create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs rename BossMod/Modules/Dawntrail/Ultimate/FRU/{P4AhkRhai.cs => P4AkhRhai.cs} (88%) create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs new file mode 100644 index 0000000000..6052f981bf --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs @@ -0,0 +1,24 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class ActivePivotParticleBeam(BossModule module) : Components.GenericRotatingAOE(module) +{ + private static readonly AOEShapeRect _shape = new(40, 9, 40); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + var rotation = (AID)spell.Action.ID switch + { + AID.ActivePivotParticleBeamCW => -22.5f.Degrees(), + AID.ActivePivotParticleBeamCCW => 22.5f.Degrees(), + _ => default + }; + if (rotation != default) + Sequences.Add(new(_shape, caster.Position, spell.Rotation, rotation, Module.CastFinishAt(spell, 0.6f), 1.6f, 5)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.ActivePivotParticleBeamAOE && Sequences.Count > 0) + AdvanceSequence(0, WorldState.CurrentTime); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/BladeOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/BladeOfDarkness.cs new file mode 100644 index 0000000000..7f651ce03e --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/BladeOfDarkness.cs @@ -0,0 +1,29 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class BladeOfDarkness(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + + private static readonly AOEShapeDonutSector _shapeIn = new(12, 60, 75.Degrees()); + private static readonly AOEShapeCone _shapeOut = new(30, 90.Degrees()); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + AOEShape? shape = (AID)spell.Action.ID switch + { + AID.BladeOfDarknessLAOE or AID.BladeOfDarknessRAOE => _shapeIn, + AID.BladeOfDarknessCAOE => _shapeOut, + _ => null + }; + if (shape != null) + _aoe = new(shape, spell.LocXZ, spell.Rotation, Module.CastFinishAt(spell)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.BladeOfDarknessLAOE or AID.BladeOfDarknessRAOE or AID.BladeOfDarknessCAOE) + _aoe = null; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs new file mode 100644 index 0000000000..0de99df6c8 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs @@ -0,0 +1,20 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class Break(BossModule module) : Components.GenericGaze(module) +{ + private readonly List _eyes = []; + + public override IEnumerable ActiveEyes(int slot, Actor actor) => _eyes;//_casters.Where(c => c.CastInfo?.TargetID != actor.InstanceID).Select(c => new Eye(EyePosition(c), Module.CastFinishAt(c.CastInfo), Range: range)); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.BreakBoss or AID.BreakEye) + _eyes.Add(new(caster.Position, Module.CastFinishAt(spell, 0.9f))); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.BreakBossAOE or AID.BreakEyeAOE) + _eyes.RemoveAll(eye => eye.Position.AlmostEqual(caster.Position, 1)); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs new file mode 100644 index 0000000000..1b8a0cfc19 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs @@ -0,0 +1,189 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class DelugeOfDarkness1(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DelugeOfDarkness1)); +class Flare(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(25), (uint)IconID.Flare, ActionID.MakeSpell(AID.FlareAOE), 8.1f, true); +class FloodOfDarkness(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.FloodOfDarkness)); +class DelugeOfDarkness2(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DelugeOfDarkness2)); +class StygianShadow(BossModule module) : Components.Adds(module, (uint)OID.StygianShadow); +class Atomos(BossModule module) : Components.Adds(module, (uint)OID.Atomos); +class DarkDominion(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.DarkDominion)); +class GhastlyGloomCross(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GhastlyGloomCross), new AOEShapeCross(40, 15)); +class GhastlyGloomDonut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GhastlyGloomDonut), new AOEShapeDonut(21, 40)); +class FloodOfDarknessAdd(BossModule module) : Components.CastInterruptHint(module, ActionID.MakeSpell(AID.FloodOfDarknessAdd)); // TODO: only if add is player's?.. +class Excruciate(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.Excruciate), new AOEShapeCircle(4), true); +class LoomingChaos(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.LoomingChaosAOE), "Raidwide + swap positions"); + +class Ch01CloudOfDarknessStates : StateMachineBuilder +{ + public Ch01CloudOfDarknessStates(BossModule module) : base(module) + { + DeathPhase(0, SinglePhase); + } + + private void SinglePhase(uint id) + { + SimpleState(id + 0xFF0000, 10000, "???") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +// TODO: mechanic phase bounds +// TODO: flood bounds & squares +// TODO: particle concentration towers +// TODO: evil seed +// TODO: chaser beam +// TODO: tankswap hints? +[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624)] +public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultCenter, InitialBounds) +{ + public static readonly WPos DefaultCenter = new(100, 100); + public static readonly ArenaBoundsCircle InitialBounds = new(40); + public static readonly ArenaBoundsCustom Phase1Bounds = new(InitialBounds.Radius, new(BuildPhase1BoundsContour())); + public static readonly ArenaBoundsCustom Phase2Bounds = new(InitialBounds.Radius, BuildPhase2BoundsPoly()); + public static readonly WPos Phase1Midpoint = DefaultCenter + Phase1Bounds.Poly.Parts[0].Vertices[1] + Phase1Bounds.Poly.Parts[0].Vertices[3]; + + public static List BuildPhase1BoundsContour() + { + // north 'diagonal' is at [+/-15, -37] (it almost intersects the initial circle - at x=15 z is ~37.08) + // the main diagonal is 20, rotated by 45 degrees, which means that side corners are at x=+/- 40/sqrt(2), z = -37 + 40/sqrt(2) - 15 + var nz = -37; + var nx = 15; + var halfDiag = 40 / MathF.Sqrt(2); + var cz = nz + halfDiag - nx; + return [new(nx, nz), new(halfDiag, cz), new(0, cz + halfDiag), new(-halfDiag, cz), new(-nx, nz)]; + } + + public static RelSimplifiedComplexPolygon BuildPhase2BoundsPoly() + { + // mid is union of 4 rects + var midHalfWidth = 3; + var midHalfLength = 24; + var midOffset = 15; + var op1 = new PolygonClipper.Operand(); + var op2 = new PolygonClipper.Operand(); + op1.AddContour(CurveApprox.Rect(new WDir(0, +midOffset), new(1, 0), midHalfWidth, midHalfLength)); + op1.AddContour(CurveApprox.Rect(new WDir(0, -midOffset), new(1, 0), midHalfWidth, midHalfLength)); + op2.AddContour(CurveApprox.Rect(new WDir(+midOffset, 0), new(0, 1), midHalfWidth, midHalfLength)); + op2.AddContour(CurveApprox.Rect(new WDir(-midOffset, 0), new(0, 1), midHalfWidth, midHalfLength)); + var mid = InitialBounds.Clipper.Union(op1, op2); + + // sides is union of two platforms and the outside ring + var sideHalfWidth = 7.5f; + var sideHalfLength = 10; + var sideOffset = 19 + sideHalfLength; + var sideRingWidth = 6; + op1.Clear(); + op2.Clear(); + op1.AddContour(CurveApprox.Rect(new WDir(+sideOffset, 0), new(1, 0), sideHalfWidth, sideHalfLength)); + op1.AddContour(CurveApprox.Rect(new WDir(-sideOffset, 0), new(1, 0), sideHalfWidth, sideHalfLength)); + op2.AddContour(CurveApprox.Circle(InitialBounds.Radius, 0.1f)); + op2.AddContour(CurveApprox.Circle(InitialBounds.Radius - sideRingWidth, 0.1f)); + var side = InitialBounds.Clipper.Union(op1, op2); + + op1.Clear(); + op2.Clear(); + op1.AddPolygon(mid); + op2.AddPolygon(side); + return InitialBounds.Clipper.Union(op1, op2); + } +} + +// envcontrols: +// 00 = main bounds telegraph +// - 00200010 - phase 1 +// - 00020001 - phase 2 +// - 00040004 - remove telegraph (note that actual bounds are controlled by something else!) +// 02 = outer ring +// - 00020001 - become dangerous +// - 00080004 - restore to normal +// 03-1E = mid squares +// - 08000001 - init +// - 00200010 - become occupied +// - 02000001 - become free +// - 00800040 - player is standing for too long, will break soon +// - 00080004 - break +// - 00020001 - repair +// - arrangement: +// 04 0B +// 03 05 06 07 0E 0D 0C 0A +// 08 0F +// 09 10 +// 17 1E +// 16 1D +// 11 13 14 15 1C 1B 1A 18 +// 12 19 +// 1F-2E = 1-man towers +// - 00020001 - appear +// - 00200010 - occupied +// - 00080004 - disappear +// - 08000001 - ? (spot animation) +// - arrangement: +// 25 26 +// 21 xx 1F xx xx 20 xx 22 +// 23 24 +// xx xx +// xx xx +// 2B 2C +// 29 xx 27 xx xx 28 xx 2A +// 2D 2E +// 2F-3E = 2-man towers +// - 00020001 - appear +// - 00200010 - occupied by 1 +// - 00800040 - occupied by 2 +// - 00080004 - disappear +// - 08000001 - ? (spot animations) +// - arrangement (also covers intersecting square): +// 35 36 +// 31 xx 2F xx xx 30 xx 32 +// 33 34 +// xx xx +// xx xx +// 3B 3C +// 39 xx 37 xx xx 38 xx 3A +// 3D 3E +// 3F-46 = 3-man towers +// - 00020001 - appear +// - 00200010 - occupied by 1 +// - 00800040 - occupied by 2 +// - 02000100 - occupied by 3 +// - 00080004 - disappear +// - 08000001 - ? (spot animations) +// - arrangement: +// 3F 43 +// 42 40 44 46 +// 41 45 +// 47-56 = 1-man tower falling orb +// 57-66 = 2-man tower falling orb +// 67-6E = 3-man tower falling orb + diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs new file mode 100644 index 0000000000..617023a819 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs @@ -0,0 +1,199 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +public enum OID : uint +{ + Boss = 0x461E, // R23.000, x1 + Helper = 0x233C, // R0.500, x24, Helper type + StygianShadow = 0x461F, // R4.000, x0 (spawn during fight), big add + Atomos = 0x4620, // R2.800, x0 (spawn during fight), small add + DeathsHand = 0x4621, // R2.000, x6, grim embrace hand + StygianTendrils = 0x4622, // R1.200, x0 (spawn during fight), evil seed + CloudletOfDarkness = 0x4623, // R3.000, x0 (spawn during fight), criss-cross source + BallOfNaught = 0x4624, // R1.500, x1, (en)death sphere + //_Gen_DreadGale = 0x4625, // R1.200, x1 + SinisterEye = 0x4626, // R2.800, x2, break gaze source + AtomosSpawnPoint = 0x1EBD7B, // R0.500, x0 (spawn during fight), EventObj type + EvilSeed = 0x1E9B3B, // R0.500, x0 (spawn during fight), EventObj type + //_Gen_Actor1e8536 = 0x1E8536, // R2.000, x1, EventObj type + //_Gen_Exit = 0x1E850B, // R0.500, x1, EventObj type +} + +public enum AID : uint +{ + AutoAttackNormalPrimary = 40441, // Boss->player, no cast, single-target + AutoAttackNormalSecondary = 40442, // Helper->player, no cast, single-target + AutoAttackVulnPrimary = 41348, // Boss->player, no cast, single-target + AutoAttackVulnSecondary = 41349, // Helper->player, no cast, single-target + Teleport1 = 40534, // Boss->location, no cast, single-target + BladeOfDarknessL = 40443, // Boss->self, 7.0+0.7s cast, single-target, visual (left hand safe) + BladeOfDarknessLAOE = 40444, // Helper->self, 7.7s cast, range 12-60 150-degree cone donut + BladeOfDarknessR = 40445, // Boss->self, 7.0+0.7s cast, single-target, visual (right hand safe) + BladeOfDarknessRAOE = 40446, // Helper->self, 7.7s cast, range 12-60 150-degree cone donut + BladeOfDarknessC = 40447, // Boss->self, 7.0+0.7s cast, single-target, visual (out safe) + BladeOfDarknessCAOE = 40448, // Helper->self, 7.7s cast, range 30 180-degree cone + + DelugeOfDarkness1 = 40509, // Boss->location, 8.0s cast, range 60 circle, raidwide + arena transition + GrimEmbraceForward = 40505, // Boss->self, 5.0s cast, tethered players spawn aoe forward + GrimEmbraceBackward = 40506, // Boss->self, 5.0s cast, tethered players spawn aoe backward + GrimEmbraceVisual = 40507, // DeathsHand->self, no cast, single-target, visual (hand spawn) + GrimEmbraceAOE = 40508, // Helper->self, 1.0s cast, range 8 width 8 rect + RazingVolleyParticleBeam = 40511, // CloudletOfDarkness->self, 8.0s cast, range 45 width 8 rect, criss-cross + + RapidSequenceParticleBeam = 40512, // Boss->self, 7.0+0.7s cast, single-target, visual (party line stacks) + RapidSequenceParticleBeamRepeat = 40513, // Boss->self, no cast, single-target, visual (repeat) + RapidSequenceParticleBeamAOE = 40514, // Helper->self, no cast, range 50 width 6 rect + Death = 40515, // Boss->self, 5.6+0.7s cast, single-target, visual (attract to out/in) + DeathVortex = 40516, // Helper->self, 2.0s cast, range 40 circle, attract 15 + DeathAOE1 = 40517, // Helper->self, 4.0s cast, range 6 circle + DeathAOE2 = 40518, // BallOfNaught->self, 6.0s cast, range 6-40 donut + Endeath = 40531, // Boss->self, 5.0s cast, single-target, visual (delayed attract) + EndeathVortex = 40519, // Helper->self, 1.0s cast, range 40 circle, attract 15 + EndeathAOE1 = 40520, // Helper->self, 3.0s cast, range 6 circle + EndeathAOE2 = 40521, // BallOfNaught->self, 5.0s cast, range 6-40 donut + Aero = 40524, // Boss->self, 5.6+0.7s cast, single-target, visual (knockback) + AeroKnockback = 40522, // Helper->self, 2.0s cast, range 40 circle, knockback 15 + AeroAOE = 40523, // 4625->self, 2.0s cast, range 8 circle + Enaero = 40532, // Boss->self, 5.0s cast, single-target, visual (delayed knockback) + EnaeroKnockback = 40525, // Helper->self, 1.0s cast, range 40 circle, knockback 15 + EnaeroAOE = 40526, // 4625->self, 1.0s cast, range 8 circle + BreakBoss = 40527, // Boss->self, 5.0+1.0s cast, single-target, visual (gazes) + BreakBossAOE = 40528, // Helper->self, 6.0s cast, range 60 circle, gaze + BreakEye = 40529, // SinisterEye->self, 3.0+1.0s cast, single-target, visual (gaze) + BreakEyeAOE = 40530, // Helper->self, 4.0s cast, range 60 circle, gaze + Flare = 40536, // Boss->self, 4.0s cast, single-target, visual (flares) + FlareAOE = 40537, // Helper->players, no cast, range 60 circle with 25 falloff + UnholyDarkness = 41261, // Boss->self, 5.0s cast, single-target, visual (4-man stacks on healers) + UnholyDarknessAOE = 41262, // Helper->players, no cast, range 6 circle, 4-man stack + + FloodOfDarkness = 40510, // Boss->location, 7.0s cast, range 60 circle, raidwide + arena transition to normal + DelugeOfDarkness2 = 40449, // Boss->location, 8.0s cast, range 60 circle, raidwide + arena transition + Teleport2 = 40450, // Boss->location, no cast, single-target + AutoAttackAdd = 40501, // StygianShadow->player, no cast, single-target + DarkDominion = 40456, // Boss->self, 5.0s cast, range 60 circle, raidwide + TeleportAdd = 40494, // StygianShadow->location, no cast, single-target + FloodOfDarknessAdd = 40503, // StygianShadow->self, 6.0s cast, range 40 circle, interruptible raidwide + + ThirdArtOfDarknessR = 40480, // StygianShadow->self, 10.0+0.4s cast, single-target, visual (right first) + ThirdArtOfDarknessL = 40483, // StygianShadow->self, 10.0+0.4s cast, single-target, visual (left first) + ArtOfDarknessNextR = 40481, // StygianShadow->self, no cast, single-target, visual (next right) + ArtOfDarknessAOER = 40482, // Helper->self, no cast, range 15 180-degree cone + ArtOfDarknessNextL = 40484, // StygianShadow->self, no cast, single-target, visual (next left) + ArtOfDarknessAOEL = 40485, // Helper->self, no cast, range 15 ?-degree cone + HyperFocusedParticleBeamNext = 40486, // StygianShadow->self, no cast, single-target, visual (next spread) + HyperFocusedParticleBeamAOE = 40487, // Helper->self, no cast, range 22 width 5 rect protean + MultiProngedParticleBeamNext = 40488, // StygianShadow->self, no cast, single-target, visual (next pairs) + MultiProngedParticleBeamAOE = 40489, // Helper->players, no cast, range 3 circle, 2-man stack + + ParticleConcentration = 40472, // Boss->self, 6.0s cast, single-target, visual (towers) + ParticleBeam1 = 40474, // Helper->location, no cast, range 3 circle, 1-man tower + ParticleBeam2 = 40475, // Helper->location, no cast, range 3 circle, 2-man tower + ParticleBeam3 = 40476, // Helper->location, no cast, range 3 circle, 3-man tower + ParticleBeam1Fail = 40473, // Helper->self, no cast, range 80 circle, raidwide if 1-man tower is not soaked + ParticleBeam2Fail = 41346, // Helper->self, no cast, range 80 circle, raidwide if 2-man tower is not soaked + ParticleBeam3Fail = 41347, // Helper->self, no cast, range 80 circle, raidwide if 3-man tower is not soaked + + GhastlyGloomCross = 40457, // Boss->self, 7.8+0.7s cast, single-target, visual (cross) + GhastlyGloomCrossAOE = 40458, // Helper->self, 8.5s cast, range 40 width 30 cross + GhastlyGloomDonut = 40459, // Boss->self, 7.8+0.7s cast, single-target, visual (donut) + GhastlyGloomDonutAOE = 40460, // Helper->self, 8.5s cast, range 21-40 donut + + CurseOfDarkness = 40498, // StygianShadow->self, 2.0s cast, single-target, visual (raidwide with debuff that causes dark energy particle beam on expire) + CurseOfDarknessAOE = 40499, // Helper->self, 3.0s cast, range 40 circle, raidwide + DarkEnergyParticleBeam = 40500, // Helper->self, no cast, range 25 15?-degree cone + + EvilSeed = 40490, // StygianShadow->self, 7.0s cast, single-target, visual (seeds plant) + EvilSeedAOE = 40491, // Helper->location, 5.0s cast, range 5 circle, puddle when seed is planted + ThornyVine = 40492, // StygianShadow->self, 8.0s cast, single-target, visual (seeds tether) + ThornyVineAOE = 40493, // Helper->self, no cast, ??? (if tethers weren't broken?) + + ChaosCondensedParticleBeam = 40461, // Boss->self, 8.0+0.7s cast, single-target, visual (wild charges) + ChaosCondensedParticleBeamAOE1 = 40462, // Helper->self, no cast, range 50 width 6 rect, 6-man wild charge on platforms + ChaosCondensedParticleBeamAOE2 = 40463, // Helper->self, no cast, range 50 width 6 rect, 3-man wild charge on mid + DiffusiveForceParticleBeam = 40464, // Boss->self, 8.0+0.7s cast, single-target, visual (spread) + DiffusiveForceParticleBeamAOE1 = 40465, // Helper->players, no cast, range 7 circle, first wave (any specific targeting?) + DiffusiveForceParticleBeamAOE2 = 40466, // Helper->players, no cast, range 5 circle, second wave + + LateralCorePhaser = 40495, // StygianShadow->self, 6.0+2.0s cast, single-target, visual (sides > front) + CoreLateralPhaser = 40496, // StygianShadow->self, 6.0+2.0s cast, single-target, visual (front > sides) + Phaser = 40497, // Helper->self, 8.0s cast, range 23 ?-degree cone + + ActivePivotParticleBeamCW = 40467, // Boss->self, 14.0+0.5s cast, single-target, visual (cw rotation) + ActivePivotParticleBeamCWRepeat = 40468, // Boss->self, no cast, single-target + ActivePivotParticleBeamCCW = 40469, // Boss->self, 14.0+0.5s cast, single-target, visual (ccw rotation) + ActivePivotParticleBeamCCWRepeat = 40470, // Boss->self, no cast, single-target + ActivePivotParticleBeamAOE = 40471, // Helper->self, no cast, range 80 width 18 rect + + Excruciate = 40502, // StygianShadow->player, 5.0s cast, range 4 circle, tankbuster + LoomingChaosAdd = 41673, // StygianShadow->self, 7.0s cast, single-target, visual (position swaps) + LoomingChaosBoss = 41674, // Boss->self, 7.0s cast, single-target, visual (position swaps) + LoomingChaosAOE = 41675, // Helper->self, 7.7s cast, range 50 circle, raidwide + position swaps + + //_Weaponskill_FeintParticleBeam = 40477, // Boss->self, 6.0+0.7s cast, single-target +} + +public enum SID : uint +{ + //_Gen_ArcaneDesign = 4180, // Boss->Boss, extra=0x0 + //_Gen_LightningResistanceDown = 4386, // Helper/Boss->player, extra=0x1/0x2/0x3/0x4/0x5/0x6/0x7/0x8/0x9/0xA/0xB/0xC/0xD/0xE/0xF/0x10 + DeadlyEmbrace = 4181, // none->player, extra=0x0 + //_Gen_ = 2970, // none->player, extra=0x358 + //_Gen_Heavy = 1595, // none->player, extra=0x4B + AbyssalEdge = 4182, // Boss->Boss, extra=0x0 (endeath/enaero stored) + //_Gen_Doom = 3364, // Helper/BallOfNaught->player, extra=0x0 + //_Gen_MagicVulnerabilityUp = 2941, // Helper->player, extra=0x0 + //_Gen_BrinkOfDeath = 44, // none->player, extra=0x0 + //_Gen_Petrification = 3007, // Helper->player, extra=0x0 + //_Gen_VeilOfDarkness = 4179, // Boss->Boss, extra=0x0 + //_Gen_CloyingCondensation = 2532, // none->player, extra=0x0 + //_Gen_ = 4388, // none->StygianShadow, extra=0x1052 + //_Gen_ = 4387, // none->Boss, extra=0x1051 + //_Gen_InnerDarkness = 4177, // none->player, extra=0x0 + //_Gen_OuterDarkness = 4178, // none->player, extra=0x0 + //_Gen_VulnerabilityDown = 2198, // none->StygianShadow/Boss, extra=0x0 + //_Gen_DamageUp = 3129, // none->StygianShadow/Boss, extra=0x0 + //_Gen_Rehabilitation = 4191, // none->Boss, extra=0x1/0x4/0x3/0x2 + //_Gen_LifeDrain = 1377, // none->player, extra=0x0 + //_Gen_CraftersGrace = 45, // player->player, extra=0x50 + //_Gen_VulnerabilityUp = 4375, // none->player, extra=0x10/0x2/0xA/0x7/0x6/0x1/0x3/0x5/0x4/0x9/0x8/0xF + //_Gen_Terror = 66, // Helper->player, extra=0x0 + CurseOfDarkness = 2387, // none->player, extra=0x0 + //_Gen_SustainedDamage = 4149, // Helper/StygianTendrils/StygianShadow->player, extra=0x1/0x2/0x3 + //_Gen_StabWound = 3061, // none->player, extra=0x0 + //_Gen_StabWound = 3062, // none->player, extra=0x0 + //_Gen_ThornyVine = 445, // none->player, extra=0x0 + //_Gen_ForwardWithThee = 2240, // none->player, extra=0x33F + //_Gen_Stun = 149, // none->player, extra=0x0 + //_Gen_BackWithThee = 2241, // none->player, extra=0x340 + //_Gen_LeftWithThee = 2242, // none->player, extra=0x341 + //_Gen_Stun = 2656, // none->player, extra=0x0 + //_Gen_RightWithThee = 2243, // none->player, extra=0x342 + //_Gen_Bleeding = 3077, // none->player, extra=0x0 +} + +public enum IconID : uint +{ + GrimEmbraceCountdown = 552, // player->self + Flare = 346, // player->self + UnholyDarkness = 100, // player->self + ThirdArtOfDarknessLeft = 239, // StygianShadow->self + ThirdArtOfDarknessRight = 240, // StygianShadow->self + ThirdArtOfDarknessStack = 241, // StygianShadow->self + ThirdArtOfDarknessSpread = 242, // StygianShadow->self + EvilSeed = 551, // player->self + ThornyVine = 569, // StygianTendrils->self + //_Gen_Icon_12 = 12, // player->self + RotateCW = 564, // Boss->self + RotateCCW = 565, // Boss->self + Excruciate = 342, // player->self +} + +public enum TetherID : uint +{ + GrimEmbraceForward = 300, // player->Boss + GrimEmbraceBackward = 301, // player->Boss + //_Gen_Tether_14 = 14, // Boss/StygianShadow->StygianShadow/Boss + //_Gen_Tether_165 = 165, // 4620->player + ThornyVine = 18, // StygianTendrils/player->player + //_Gen_Tether_38 = 38, // player->player + //_Gen_Tether_1 = 1, // StygianTendrils->StygianTendrils +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ChaosCondensedParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ChaosCondensedParticleBeam.cs new file mode 100644 index 0000000000..e5b3611d9a --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ChaosCondensedParticleBeam.cs @@ -0,0 +1,21 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class ChaosCondensedParticleBeam(BossModule module) : Components.GenericWildCharge(module, 3, default, 50) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.ChaosCondensedParticleBeam) + { + Source = caster; + Activation = Module.CastFinishAt(spell, 0.7f); + foreach (var (i, p) in Raid.WithSlot(true)) + PlayerRoles[i] = p.Role == Role.Tank ? PlayerRole.Target : PlayerRole.ShareNotFirst; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.ChaosCondensedParticleBeamAOE1 or AID.ChaosCondensedParticleBeamAOE2) + Source = null; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs new file mode 100644 index 0000000000..c1073a13d0 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs @@ -0,0 +1,31 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class CurseOfDarkness(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.CurseOfDarknessAOE), "Raidwide + bait debuffs"); + +class DarkEnergyParticleBeam(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.DarkEnergyParticleBeam)) +{ + private readonly DateTime[] _activation = new DateTime[PartyState.MaxAllianceSize]; + + private static readonly AOEShapeCone _shape = new(25, 7.5f.Degrees()); + + public override void Update() + { + CurrentBaits.Clear(); + var deadline = WorldState.FutureTime(7); + foreach (var (i, p) in Raid.WithSlot()) + if (_activation[i] != default && _activation[i] < deadline) + CurrentBaits.Add(new(p, p, _shape, _activation[i])); + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.CurseOfDarkness && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + _activation[slot] = status.ExpireAt; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.CurseOfDarkness && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + _activation[slot] = default; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs new file mode 100644 index 0000000000..9e92c4d428 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs @@ -0,0 +1,18 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +// TODO: who gets radius 7 and who gets radius 5? +// TODO: show for second wave too... +class DiffusiveForceParticleBeam(BossModule module) : Components.UniformStackSpread(module, 0, 7) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.DiffusiveForceParticleBeam) + AddSpreads(Raid.WithoutSlot(), Module.CastFinishAt(spell, 0.7f)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.DiffusiveForceParticleBeamAOE1 or AID.DiffusiveForceParticleBeamAOE2) + Spreads.Clear(); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs new file mode 100644 index 0000000000..7828a9c80a --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs @@ -0,0 +1,109 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class EnaeroKnockback(BossModule module) : Components.Knockback(module) +{ + private Source? _source; + private bool _delayed; + + public override IEnumerable Sources(int slot, Actor actor) => Utils.ZeroOrOne(_source); + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!_delayed) + base.AddHints(slot, actor, hints); + } + + public override void AddGlobalHints(GlobalHints hints) + { + if (_delayed) + hints.Add("Delayed knockback"); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Aero: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Enaero: + _delayed = true; + break; + case AID.AeroKnockback: + case AID.EnaeroKnockback: + if (_source == null || !_source.Value.Origin.AlmostEqual(caster.Position, 1)) + ReportError("Aero knockback mispredicted"); + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(Module.CastFinishAt(spell, 2.2f)); + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.AeroKnockback: + case AID.EnaeroKnockback: + _source = null; + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + _delayed = false; + break; + } + } + + private void Start(DateTime activation) => _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation); +} + +class EnaeroAOE(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + private bool _delayed; + + private static readonly AOEShapeCircle _shape = new(8); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Aero: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Enaero: + _delayed = true; + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.AeroAOE: + case AID.EnaeroAOE: + _aoe = null; + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(WorldState.FutureTime(2.2f)); + break; + } + } + + private void Start(DateTime activation) + { + _aoe = new(_shape, Ch01CloudOfDarkness.Phase1Midpoint, default, activation); + _delayed = false; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs new file mode 100644 index 0000000000..35f468ddf5 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs @@ -0,0 +1,116 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class EndeathVortex(BossModule module) : Components.Knockback(module) +{ + private Source? _source; + private bool _delayed; + + public override IEnumerable Sources(int slot, Actor actor) => Utils.ZeroOrOne(_source); + + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => (pos - Ch01CloudOfDarkness.Phase1Midpoint).LengthSq() <= 36; + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!_delayed) + base.AddHints(slot, actor, hints); + } + + public override void AddGlobalHints(GlobalHints hints) + { + if (_delayed) + hints.Add("Delayed attract"); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Death: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Endeath: + _delayed = true; + break; + case AID.DeathVortex: + case AID.EndeathVortex: + if (_source == null || !_source.Value.Origin.AlmostEqual(caster.Position, 1)) + ReportError("Death vortex mispredicted"); + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(Module.CastFinishAt(spell, 2.2f)); + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.DeathVortex: + case AID.EndeathVortex: + _source = null; + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + _delayed = false; + break; + } + } + + private void Start(DateTime activation) => _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation, Kind: Kind.TowardsOrigin); +} + +class EndeathAOE(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + private bool _delayed; + + private static readonly AOEShapeCircle _shapeOut = new(6); + private static readonly AOEShapeDonut _shapeIn = new(6, 40); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes.Take(1); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Death: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Endeath: + _delayed = true; + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.DeathAOE1: + case AID.DeathAOE2: + case AID.EndeathAOE1: + case AID.EndeathAOE2: + if (_aoes.Count > 0) + _aoes.RemoveAt(0); + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(WorldState.FutureTime(2.2f)); + break; + } + } + + private void Start(DateTime activation) + { + _aoes.Add(new(_shapeOut, Ch01CloudOfDarkness.Phase1Midpoint, default, activation.AddSeconds(2))); + _aoes.Add(new(_shapeIn, Ch01CloudOfDarkness.Phase1Midpoint, default, activation.AddSeconds(4))); + _delayed = false; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/GrimEmbrace.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/GrimEmbrace.cs new file mode 100644 index 0000000000..ce186ca23f --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/GrimEmbrace.cs @@ -0,0 +1,68 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class GrimEmbraceBait(BossModule module) : Components.GenericBaitAway(module) +{ + public struct PlayerState + { + public AOEShapeRect? Shape; + public DateTime Activation; + } + + private readonly PlayerState[] _states = new PlayerState[PartyState.MaxAllianceSize]; + + private static readonly AOEShapeRect _shapeForward = new(8, 4); + private static readonly AOEShapeRect _shapeBackward = new(0, 4, 8); + + public override void Update() + { + CurrentBaits.Clear(); + var deadline = WorldState.FutureTime(7); + foreach (var (i, p) in Raid.WithSlot()) + { + ref var s = ref _states[i]; + if (s.Shape != null && s.Activation != default && s.Activation < deadline) + CurrentBaits.Add(new(p, p, s.Shape, s.Activation)); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + ref var s = ref _states[slot]; + if (s.Shape != null && s.Activation != default) + hints.Add($"Dodge {(s.Shape == _shapeForward ? "backward" : "forward")} in {Math.Max(0, (s.Activation - WorldState.CurrentTime).TotalSeconds):f1}s", false); + base.AddHints(slot, actor, hints); + } + + //public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + //{ + // var s = _states[slot]; + // if (s.Shape != null) + // hints.AddSpecialMode(AIHints.SpecialMode.Pyretic, s.Activation); // TODO: reconsider? i want to ensure character won't turn last moment... + //} + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + var shape = (TetherID)tether.ID switch + { + TetherID.GrimEmbraceForward => _shapeForward, + TetherID.GrimEmbraceBackward => _shapeBackward, + _ => null + }; + if (shape != null && Raid.FindSlot(source.InstanceID) is var slot && slot >= 0 && slot < _states.Length) + _states[slot].Shape = shape; + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.DeadlyEmbrace && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0 && slot < _states.Length) + _states[slot].Activation = status.ExpireAt; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.DeadlyEmbrace && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0 && slot < _states.Length) + _states[slot] = default; + } +} + +class GrimEmbraceAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GrimEmbraceAOE), new AOEShapeRect(8, 4)); diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Phaser.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Phaser.cs new file mode 100644 index 0000000000..2eb927dc20 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Phaser.cs @@ -0,0 +1,15 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class Phaser(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Phaser), new AOEShapeCone(23, 30.Degrees())) // TODO: verify angle +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var deadline = Module.CastFinishAt(Casters.Count > 0 ? Casters[0].CastInfo : null, 1); + foreach (var c in Casters) + { + var activation = Module.CastFinishAt(c.CastInfo); + if (activation < deadline) + yield return new(Shape, c.Position, c.CastInfo?.Rotation ?? c.Rotation, activation); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RapidSequenceParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RapidSequenceParticleBeam.cs new file mode 100644 index 0000000000..e6d28c7360 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RapidSequenceParticleBeam.cs @@ -0,0 +1,35 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class RapidSequenceParticleBeam(BossModule module) : Components.GenericWildCharge(module, 3, ActionID.MakeSpell(AID.RapidSequenceParticleBeamAOE), 50) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + Source = null; // just in case, if mechanic was not finished properly, reset on next cast start + if ((AID)spell.Action.ID == AID.RapidSequenceParticleBeam) + { + NumCasts = 0; + Source = caster; + Activation = Module.CastFinishAt(spell, 0.8f); + // TODO: not sure how targets are selected, assume it's first healer of each alliance + BitMask selectedTargetsInAlliance = default; + foreach (var (i, p) in Raid.WithSlot()) + { + if (p.Role == Role.Healer && !selectedTargetsInAlliance[i >> 3]) + { + PlayerRoles[i] = PlayerRole.TargetNotFirst; + selectedTargetsInAlliance.Set(i >> 3); + } + else + { + PlayerRoles[i] = p.Role == Role.Tank ? PlayerRole.Share : PlayerRole.ShareNotFirst; + } + } + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.RapidSequenceParticleBeamAOE && ++NumCasts >= 12) + Source = null; + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RazingVolleyParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RazingVolleyParticleBeam.cs new file mode 100644 index 0000000000..0f9cf5ddbe --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/RazingVolleyParticleBeam.cs @@ -0,0 +1,16 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class RazingVolleyParticleBeam(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RazingVolleyParticleBeam), new AOEShapeRect(45, 4)) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var deadline = Module.CastFinishAt(Casters.Count > 0 ? Casters[0].CastInfo : null, 3); + foreach (var c in Casters) + { + var activation = Module.CastFinishAt(c.CastInfo); + if (activation > deadline) + break; + yield return new(Shape, c.Position, c.CastInfo?.Rotation ?? c.Rotation, activation); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ThirdArtOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ThirdArtOfDarkness.cs new file mode 100644 index 0000000000..d97daec199 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ThirdArtOfDarkness.cs @@ -0,0 +1,105 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class ThirdArtOfDarknessCleave(BossModule module) : Components.GenericAOEs(module) +{ + public enum Mechanic { None, Left, Right, Stack, Spread } + + public readonly Dictionary> Mechanics = []; + + private static readonly AOEShapeCone _shape = new(15, 90.Degrees()); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var (caster, m) in Mechanics) + { + var dir = m.Count == 0 ? default : m[0].mechanic switch + { + Mechanic.Left => 90.Degrees(), + Mechanic.Right => -90.Degrees(), + _ => default + }; + if (dir != default) + yield return new(_shape, caster.Position, caster.Rotation + dir, m[0].activation); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + var (a, m) = Mechanics.FirstOrDefault(kv => kv.Key.InstanceID == actor.TargetID); + if (a != null && m.Count > 0) + hints.Add($"Order: {string.Join(" > ", m.Select(m => m.mechanic))}", false); + base.AddHints(slot, actor, hints); + } + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if ((OID)actor.OID == OID.StygianShadow) + { + var mechanic = (IconID)iconID switch + { + IconID.ThirdArtOfDarknessLeft => Mechanic.Left, + IconID.ThirdArtOfDarknessRight => Mechanic.Right, + IconID.ThirdArtOfDarknessStack => Mechanic.Stack, + IconID.ThirdArtOfDarknessSpread => Mechanic.Spread, + _ => Mechanic.None + }; + if (mechanic != Mechanic.None) + Mechanics.GetOrAdd(actor).Add((mechanic, WorldState.FutureTime(9.5f))); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var mechanic = (AID)spell.Action.ID switch + { + AID.ArtOfDarknessAOEL => Mechanic.Left, + AID.ArtOfDarknessAOER => Mechanic.Right, + AID.HyperFocusedParticleBeamAOE => Mechanic.Spread, + AID.MultiProngedParticleBeamAOE => Mechanic.Stack, + _ => Mechanic.None + }; + if (mechanic != Mechanic.None) + { + var (a, m) = Mechanics.FirstOrDefault(kv => kv.Key.Position.AlmostEqual(caster.Position, 1) && kv.Value.Count > 0 && kv.Value[0].mechanic == mechanic); + if (a != null) + { + m.RemoveAt(0); + if (m.Count == 0) + Mechanics.Remove(a); + } + } + } +} + +class ThirdArtOfDarknessHyperFocusedParticleBeam(BossModule module) : Components.GenericBaitAway(module) +{ + private readonly ThirdArtOfDarknessCleave? _main = module.FindComponent(); + + private static readonly AOEShapeRect _shape = new(22, 2.5f); + + public override void Update() + { + CurrentBaits.Clear(); + if (_main != null) + foreach (var (a, m) in _main.Mechanics) + if (m.Count > 0 && m[0].mechanic == ThirdArtOfDarknessCleave.Mechanic.Spread) + foreach (var p in Raid.WithoutSlot().SortedByRange(a.Position).Take(6)) + CurrentBaits.Add(new(a, p, _shape, m[0].activation)); + } +} + +class ThirdArtOfDarknessMultiProngedParticleBeam(BossModule module) : Components.UniformStackSpread(module, 3, 0, 2) +{ + private readonly ThirdArtOfDarknessCleave? _main = module.FindComponent(); + + public override void Update() + { + Stacks.Clear(); + if (_main != null) + foreach (var (a, m) in _main.Mechanics) + if (m.Count > 0 && m[0].mechanic == ThirdArtOfDarknessCleave.Mechanic.Stack) + foreach (var p in Raid.WithoutSlot().SortedByRange(a.Position).Take(3)) + AddStack(p, m[0].activation); + base.Update(); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/UnholyDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/UnholyDarkness.cs new file mode 100644 index 0000000000..0d2cdfa9de --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/UnholyDarkness.cs @@ -0,0 +1,14 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class UnholyDarkness(BossModule module) : Components.StackWithIcon(module, (uint)IconID.UnholyDarkness, ActionID.MakeSpell(AID.UnholyDarknessAOE), 6, 8.1f, 4) +{ + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action == StackAction) + { + Stacks.Clear(); // if one of the target dies, it won't get hit + ++NumFinishedStacks; + } + } +} + diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs index 8f4170b331..82c98a5bd9 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs @@ -5,6 +5,8 @@ class P2CrystalOfLight(BossModule module) : Components.Adds(module, (uint)OID.Cr class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Junction)); class P3BlackHalo(BossModule module) : Components.CastSharedTankbuster(module, ActionID.MakeSpell(AID.BlackHalo), new AOEShapeCone(60, 45.Degrees())); // TODO: verify angle class P4EdgeOfOblivion(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.EdgeOfOblivion)); +class P4HallowedWingsL(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HallowedWingsL), new AOEShapeRect(80, 20, 0, 90.Degrees())); +class P4HallowedWingsR(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HallowedWingsR), new AOEShapeRect(80, 20, 0, -90.Degrees())); [ModuleInfo(BossModuleInfo.Maturity.WIP, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1006, NameID = 9707, PlanLevel = 100)] public class FRU(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs index 3f6d563580..14d0779a56 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs @@ -57,6 +57,11 @@ public class FRUConfig() : ConfigNode() [PropertyDisplay("P3 Apocalypse: uptime swaps (only consider swaps within prio 1/2 and 3/4, assuming these are melee and ranged)", separator: true)] public bool P3ApocalypseUptime; + [PropertyDisplay("P4 Darklit Dragonsong: assignments (lower prio stays more clockwise, lowest prio support takes N tower)")] + [GroupDetails(["Support prio1", "Support prio2", "Support prio3", "Support prio4", "DD prio1", "DD prio2", "DD prio3", "DD prio4"])] + [GroupPreset("Default (healer N)", [2, 3, 0, 1, 4, 5, 6, 7])] + public GroupAssignmentUnique P4DarklitDragonsongAssignments = new() { Assignments = [2, 3, 0, 1, 4, 5, 6, 7] }; + // ai-only settings [PropertyDisplay("P1 Cyclonic Break (proteans): bait clock spots (supports should be near dd to resolve pairs)", tooltip: "Only used by AI")] [GroupDetails(["N", "NE", "E", "SE", "S", "SW", "W", "NW"])] diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index aa011be6fb..c2a466f76e 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -28,11 +28,12 @@ public enum OID : uint ApocalypseLight = 0x1EB0FF, // R0.500, x0 (spawn during fight), EventObj type UsurperOfFrostP4 = 0x45A9, // R6.125, x0 (spawn during fight) + GreatWyrm = 0x45AA, // R3.500, x0 (spawn during fight), Part type OracleOfDarknessP4 = 0x45AB, // R7.040, x0 (spawn during fight) + SorrowsHourglass = 0x45AD, // R1.000, x0 (spawn during fight) + FragmentOfFate = 0x45B1, // R3.500, x0 (spawn during fight) VisionOfRyne = 0x45B4, // R0.750, x0 (spawn during fight) VisionOfGaia = 0x45B5, // R1.500, x0 (spawn during fight) - FragmentOfFate = 0x45B1, // R3.500, x0 (spawn during fight) - GreatWyrm = 0x45AA, // R3.500, x0 (spawn during fight), Part type } public enum AID : uint @@ -211,7 +212,23 @@ public enum AID : uint DarklitDragonsongUsurper = 40239, // UsurperOfFrostP4->self, 5.0s cast, range 100 circle, raidwide DarklitDragonsongOracle = 40301, // OracleOfDarknessP4->self, 5.0s cast, single-target, visual - //_Weaponskill_ThePathOfLight = 40187, // UsurperOfFrostP4->self, 8.0s cast, single-target + PathOfLight = 40187, // UsurperOfFrostP4->self, 8.0s cast, single-target, visual (towers + proteans) + PathOfLightAOE = 40190, // Helper->self, no cast, range 60 60?-degree cone protean on 4 closest targets + HallowedWingsL = 40227, // UsurperOfFrostP4->self, 5.0s cast, range 80 width 40 rect, side cleave + HallowedWingsR = 40228, // UsurperOfFrostP4->self, 5.0s cast, range 80 width 40 rect, side cleave + SomberDance = 40283, // OracleOfDarknessP4->self, 5.0s cast, single-target, visual (baited tankbusters) + SomberDanceAOE1 = 40284, // OracleOfDarknessP4->player, no cast, range 8 circle, tankbuster on farthest + SomberDanceAOE2 = 40285, // OracleOfDarknessP4->players, no cast, range 8 circle, tankbuster on closest + AkhMornUsurper = 40247, // UsurperOfFrostP4->self, 4.0s cast, single-target, visual (4-hit party stacks) + AkhMornOracle = 40302, // OracleOfDarknessP4->self, 4.0s cast, single-target, visual (4-hit party stacks) + AkhMornAOEUsurper = 40248, // Helper->players, no cast, range 4 circle, 4-hit 4-man stack + AkhMornAOEOracle = 40303, // Helper->players, no cast, range 4 circle, 4-hit 4-man stack + MornAfahUsurper = 40249, // UsurperOfFrostP4->self, 6.0s cast, single-target, visual (full raid stack, lethal if hp difference is large) + MornAfahOracle = 40304, // OracleOfDarknessP4->self, 6.0s cast, single-target, visual (full raid stack, lethal if hp difference is large) + MornAfahAOE = 40250, // Helper->players, no cast, range 4 circle, wipe if hp difference check fails ? + + CrystallizeTimeUsurper = 40240, // UsurperOfFrostP4->self, 10.0s cast, single-target, visual + CrystallizeTimeOracle = 40298, // OracleOfDarknessP4->self, 10.0s cast, range 100 circle, raidwide } public enum SID : uint @@ -227,6 +244,7 @@ public enum SID : uint CurseOfEverlastingLight = 4158, // none->player, extra=0x0, light rampant second tether WeightOfLight = 4159, // none->player, extra=0x0, light rampant stack Lightsteeped = 2257, // Helper/HolyLight->player, extra=0x1/0x2/0x3/0x4/0x5 + Invincibility = 775, // none->IceVeil, extra=0x0 SpellInWaitingUnholyDarkness = 2454, // none->player, extra=0x0, stack SpellInWaitingDarkFire = 2455, // none->player, extra=0x0, large spread SpellInWaitingShadoweye = 2456, // none->player, extra=0x0, delayed gaze @@ -237,6 +255,12 @@ public enum SID : uint DelightsHourglassRotation = 2970, // none->DelightsHourglass, extra=0x10D (ccw)/0x15C (cw) Return = 2452, // none->player, extra=0x0 Stun = 4163, // none->player, extra=0x0 + Wyrmclaw = 3263, // none->player, extra=0x0 + Wyrmfang = 3264, // none->player, extra=0x0 + SpellInWaitingQuietus = 4174, // none->player, extra=0x0 + SpellInWaitingDarkAero = 2463, // none->player, extra=0x0 + //SpellInWaitingReturn = 4208, // none->player, extra=0x0 + //SpellInWaitingReturnII = 4171, // Helper->UsurperOfFrostP4, extra=0x0 } public enum IconID : uint @@ -263,4 +287,5 @@ public enum TetherID : uint HiemalRay = 84, // CrystalOfLight->player UltimateRelativitySlow = 133, // DelightsHourglass->BossP3 UltimateRelativityQuicken = 134, // DelightsHourglass->BossP3 + MornAfahHPCheck = 1, // UsurperOfFrostP4->OracleOfDarknessP4 } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index 3bd601b685..8e02df8555 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -52,6 +52,8 @@ private void Phase34(uint id) P4AkhRhai(id + 0x100000, 5.5f); P4DarklitDragonsong(id + 0x110000, 1.9f); + P4AkhMornMornAfah(id + 0x120000, 2.4f); + P4CrystallizeTime(id + 0x130000, 4.6f); SimpleState(id + 0xFF0000, 100, "???"); } @@ -147,6 +149,7 @@ private void P1TurnOfTheHeavensBoundOfFaith(uint id, float delay) ComponentCondition(id + 0x20, 2.1f, comp => comp.NumCasts > 0, "Circles") .ActivateOnEnter() .ExecOnEnter(comp => comp.Risky = true) + .ExecOnEnter(comp => comp.EnableHints = true) .DeactivateOnExit() .DeactivateOnExit(); ComponentCondition(id + 0x30, 4, comp => !comp.Active, "Stacks") // note: won't happen if both targets die, but that's a wipe anyway @@ -486,22 +489,85 @@ private void P4AkhRhai(uint id, float delay) ActorTargetable(id, _module.BossP4Usurper, true, delay, "Usurper appears") .SetHint(StateMachine.StateHint.DowntimeEnd); ActorCast(id + 0x10, _module.BossP4Usurper, AID.Materialization, 5.1f, 3, true); - ComponentCondition(id + 0x20, 11.2f, comp => comp.AOEs.Count > 0, "Puddle baits") - .ActivateOnEnter(); - ComponentCondition(id + 0x30, 2.6f, comp => comp.NumCasts > 0); + ComponentCondition(id + 0x20, 11.2f, comp => comp.AOEs.Count > 0, "Puddle baits") + .ActivateOnEnter(); + ComponentCondition(id + 0x30, 2.6f, comp => comp.NumCasts > 0); ComponentCondition(id + 0x40, 2.4f, comp => comp.NumCasts > 0, "Raidwide") .ActivateOnEnter() .DeactivateOnExit() .SetHint(StateMachine.StateHint.Raidwide); ActorTargetable(id + 0x50, _module.BossP4Oracle, true, 1.2f, "Oracle appears"); - ComponentCondition(id + 0x60, 1.6f, comp => comp.NumCasts >= 10 * comp.AOEs.Count, "Puddle resolve") - .DeactivateOnExit(); + ComponentCondition(id + 0x60, 1.6f, comp => comp.NumCasts >= 10 * comp.AOEs.Count, "Puddle resolve") + .ActivateOnEnter() + .DeactivateOnExit(); } private void P4DarklitDragonsong(uint id, float delay) { ActorCast(id, _module.BossP4Usurper, AID.DarklitDragonsongUsurper, delay, 5, true, "Raidwide (darklit)") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .SetHint(StateMachine.StateHint.Raidwide); - // TODO: more... + ActorCast(id + 0x10, _module.BossP4Usurper, AID.PathOfLight, 3.2f, 8); + ActorCastStart(id + 0x20, _module.BossP4Oracle, AID.SpiritTaker, 0.1f, true, "Towers") // towers resolve right as cast starts + .DeactivateOnExit(); + ComponentCondition(id + 0x21, 0.8f, comp => comp.NumCasts > 0, "Proteans") + .DeactivateOnExit(); + ActorCastEnd(id + 0x22, _module.BossP4Oracle, 2.2f, true) + .ActivateOnEnter(); + ActorCastStartMulti(id + 0x23, _module.BossP4Usurper, [AID.HallowedWingsL, AID.HallowedWingsR], 0.1f, true); + ComponentCondition(id + 0x24, 0.3f, comp => comp.Spreads.Count == 0, "Jump") + .DeactivateOnExit(); + ActorCastStart(id + 0x25, _module.BossP4Oracle, AID.SomberDance, 2.8f) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + ComponentCondition(id + 0x26, 1.7f, comp => comp.Stacks.Count == 0, "Stacks") + .DeactivateOnExit(); + ActorCastEnd(id + 0x27, _module.BossP4Usurper, 0.2f, false, "Side cleave") + .ActivateOnEnter() + .DeactivateOnExit() + .DeactivateOnExit() + .DeactivateOnExit(); + ActorCastEnd(id + 0x28, _module.BossP4Oracle, 3.1f, true); + ComponentCondition(id + 0x29, 0.4f, comp => comp.NumCasts > 0, "Tankbuster 1") + .SetHint(StateMachine.StateHint.Tankbuster); + ComponentCondition(id + 0x2A, 3.2f, comp => comp.NumCasts > 1, "Tankbuster 2") + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Tankbuster); + + ComponentCondition(id + 0x30, 3.4f, comp => comp.NumCasts > 0, "Raidwide") + .ActivateOnEnter() + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void P4AkhMornMornAfah(uint id, float delay) + { + ActorCast(id, _module.BossP4Usurper, AID.AkhMornUsurper, delay, 4, true) + .ActivateOnEnter(); + ComponentCondition(id + 0x10, 0.9f, comp => comp.NumCasts >= 1, "Party stack 1"); + ComponentCondition(id + 0x11, 1.1f, comp => comp.NumCasts >= 2, "Party stack 2"); + ComponentCondition(id + 0x12, 1.1f, comp => comp.NumCasts >= 3, "Party stack 3"); + ComponentCondition(id + 0x13, 1.1f, comp => comp.NumCasts >= 4, "Party stack 4") + .DeactivateOnExit(); + + ActorCast(id + 0x1000, _module.BossP4Usurper, AID.MornAfahUsurper, 0.1f, 6, true) + .ActivateOnEnter(); + ComponentCondition(id + 0x1010, 0.9f, comp => comp.Stacks.Count == 0, "Raid stack + HP check") + .DeactivateOnExit(); + } + + private void P4CrystallizeTime(uint id, float delay) + { + ActorCast(id, _module.BossP4Oracle, AID.CrystallizeTimeOracle, delay, 10, true, "Raidwide (crystallize)") + .SetHint(StateMachine.StateHint.Raidwide); + ActorTargetable(id + 0x10, _module.BossP4Usurper, false, 3.1f, "Usurper disappears") + .ActivateOnEnter(); + ActorTargetable(id + 0x11, _module.BossP4Oracle, false, 1.1f, "Oracle disappears") + .SetHint(StateMachine.StateHint.DowntimeStart); + ActorCast(id + 0x20, _module.BossP4Oracle, AID.UltimateRelativitySpeed, 0.1f, 5.5f, true); + // TODO: ... } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs index 3b3d9afa1d..411aadc9bc 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Blastburn.cs @@ -15,6 +15,13 @@ public override IEnumerable Sources(int slot, Actor actor) } } + public override void AddHints(int slot, Actor actor, TextHints hints) + { + // don't show kb hints until aoe is done + if (_aoeDone) + base.AddHints(slot, actor, hints); + } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { if (_caster != null) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs index 9c96c8d56a..7103790f59 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs @@ -9,6 +9,7 @@ // TODO: fixed tethers strat variant (tether target with clone on safe side goes S, other goes N, if any group has 5 players prio1 adjusts) class P1BoundOfFaith(BossModule module) : Components.UniformStackSpread(module, 6, 0, 4, 4) { + public bool EnableHints; public WDir SafeSide; public DateTime Activation; public readonly int[] AssignedGroups = new int[PartyState.MaxPartySize]; @@ -17,6 +18,12 @@ class P1BoundOfFaith(BossModule module) : Components.UniformStackSpread(module, public WDir AssignedLane(int slot) => new(0, AssignedGroups[slot] * 5.4f); + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (EnableHints) + base.AddHints(slot, actor, hints); + } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // we have dedicated components for this public override void DrawArenaForeground(int pcSlot, Actor pc) @@ -140,7 +147,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme var targetSlot = Raid.FindSlot(s.Target.InstanceID); var targetGroup = targetSlot >= 0 ? _comp.AssignedGroups[targetSlot] : 0; if (targetGroup == _comp.AssignedGroups[slot]) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(s.Target.Position, 6), _comp.Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(s.Target.Position, 4), _comp.Activation); // stay a bit closer to the target to avoid spooking people else hints.AddForbiddenZone(ShapeDistance.Circle(s.Target.Position, 6), _comp.Activation); } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs new file mode 100644 index 0000000000..feea449556 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs @@ -0,0 +1,18 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P4AkhMorn(BossModule module) : Components.UniformStackSpread(module, 4, 0, 4) +{ + public int NumCasts; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.AkhMornOracle) + AddStacks(Raid.WithoutSlot(true).Where(p => p.Role == Role.Tank), Module.CastFinishAt(spell, 0.9f)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.AkhMornAOEOracle) + ++NumCasts; + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhRhai.cs similarity index 88% rename from BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs rename to BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhRhai.cs index a03aed8ee4..ab80db2686 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AhkRhai.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhRhai.cs @@ -1,6 +1,6 @@ namespace BossMod.Dawntrail.Ultimate.FRU; -class P4AhkRhai(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.AkhRhaiAOE)) +class P4AkhRhai(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.AkhRhaiAOE)) { public readonly List AOEs = []; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs new file mode 100644 index 0000000000..74c5253297 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs @@ -0,0 +1,6 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P4CrystallizeTime(BossModule module) : BossComponent(module) +{ + +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs new file mode 100644 index 0000000000..9114fb9f4a --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs @@ -0,0 +1,241 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +// tethers & general assignments +class P4DarklitDragonsong(BossModule module) : BossComponent(module) +{ + public BitMask Stacks; + public BitMask TowerSoakers; + public BitMask AssignS; + public BitMask AssignE; + private readonly List<(Actor from, Actor to)> _tethers = []; + + public override void AddGlobalHints(GlobalHints hints) + { + var southTower = TowerSoakers & AssignS; + if (southTower.Any()) + hints.Add($"Water in {((southTower & Stacks).Any() ? "S" : "N")} tower"); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var t in _tethers) + Arena.AddLine(t.from.Position, t.to.Position, ArenaColor.Safe); // TODO: min/max break distance + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.SpellInWaitingDarkWater) + Stacks.Set(Raid.FindSlot(actor.InstanceID)); + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.LightRampantChains) + { + TowerSoakers.Set(Raid.FindSlot(source.InstanceID)); + var target = WorldState.Actors.Find(tether.Target); + if (target != null) + _tethers.Add((source, target)); + if (_tethers.Count == 4) + InitAssignments(); + } + } + + private void InitAssignments() + { + Span ccwOrderSlots = [-1, -1, -1, -1, -1, -1, -1, -1]; + int[] playerPrios = [-1, -1, -1, -1, -1, -1, -1, -1]; + foreach (var (slot, group) in Service.Config.Get().P4DarklitDragonsongAssignments.Resolve(Raid)) + { + ccwOrderSlots[group] = slot; + playerPrios[slot] = group; + } + if (ccwOrderSlots.Contains(-1)) + return; // assignments are not valid, bail + + // find the anchor (tethered player with lowest prio), players tethered to him on both sides will take S tower + var anchorSlot = TowerSoakers.SetBits().MinBy(i => playerPrios[i]); + var anchorPlayer = Raid[anchorSlot]; + foreach (var t in _tethers) + { + if (t.from == anchorPlayer) + AssignS.Set(Raid.FindSlot(t.to.InstanceID)); + else if (t.to == anchorPlayer) + AssignS.Set(Raid.FindSlot(t.from.InstanceID)); + } + + // remaining assignments (N/S for baits, E/W for everyone) in prio order + int numAssignedSoakers = 0, numAssignedBaits = 0; + foreach (var slot in ccwOrderSlots) + { + if (TowerSoakers[slot]) + { + // first and last prio go E + AssignE[slot] = numAssignedSoakers++ is 0 or 3; + } + else + { + // first and last go N, last two go E + AssignS[slot] = numAssignedBaits is 1 or 2; + AssignE[slot] = numAssignedBaits >= 2; + ++numAssignedBaits; + } + } + + // finally, if both stacks are on the same N/S side, bait needs to swap with other bait on same E/W side + if ((AssignS & Stacks).NumSetBits() != 1) + { + var flexStack = Stacks & ~TowerSoakers; // mask containing one set bit, corresponding to non-soaker stack - he will need to flex + var flexE = AssignE[flexStack.LowestSetBit()]; + var flexMask = (AssignE ^ new BitMask(flexE ? 0u : 0xFF)) & ~TowerSoakers; // mask containing both flexers + if (flexStack.NumSetBits() != 1 || flexMask.NumSetBits() != 2 || (flexStack & flexMask) != flexStack) + ReportError("Some error with swap logic, investigate"); + AssignS ^= flexMask; + } + } +} + +class P4DarklitDragonsongBrightHunger(BossModule module) : Components.GenericTowers(module, ActionID.MakeSpell(AID.BrightHunger)) +{ + private int _numTethers; + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.LightRampantChains && ++_numTethers == 4) + { + var assignments = Module.FindComponent(); + if (assignments != null) + { + var allowedN = assignments.TowerSoakers & ~assignments.AssignS; + var allowedS = assignments.TowerSoakers & assignments.AssignS; + if (assignments.AssignS.None()) + allowedN = allowedS = assignments.TowerSoakers; // no assignments, just mark both towers as good + + var towerOffset = new WDir(0, 8); + Towers.Add(new(Module.Center - towerOffset, 4, 4, 4, new BitMask(0xFF) ^ allowedN, WorldState.FutureTime(10.4f))); + Towers.Add(new(Module.Center + towerOffset, 4, 4, 4, new BitMask(0xFF) ^ allowedS, WorldState.FutureTime(10.4f))); + } + } + } +} + +class P4DarklitDragonsongPathOfLight(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.PathOfLightAOE)) +{ + private Actor? _source; + private DateTime _activation; + + private static readonly AOEShapeCone _shape = new(60, 30.Degrees()); + + public override void Update() + { + CurrentBaits.Clear(); + if (_source != null && ForbiddenPlayers.Any()) + foreach (var p in Raid.WithoutSlot().SortedByRange(_source.Position).Take(4)) + CurrentBaits.Add(new(_source, p, _shape, _activation)); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (CurrentBaits.Count == 0) + return; + + var baitIndex = CurrentBaits.FindIndex(b => b.Target == actor); + if (ForbiddenPlayers[slot]) + { + if (baitIndex >= 0) + hints.Add("Stay farther away!"); + } + else + { + if (baitIndex < 0) + hints.Add("Stay closer to bait!"); + else if (PlayersClippedBy(CurrentBaits[baitIndex]).Any()) + hints.Add("Bait cone away from raid!"); + } + + if (ActiveBaitsNotOn(actor).Any(b => IsClippedBy(actor, b))) + hints.Add("GTFO from baited cone!"); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.DarklitDragonsongUsurper) + { + _source = caster; + _activation = Module.CastFinishAt(spell, 12); + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.LightRampantChains) + ForbiddenPlayers.Set(Raid.FindSlot(source.InstanceID)); + } +} + +class P4DarklitDragonsongDarkWater(BossModule module) : Components.UniformStackSpread(module, 6, 0, 4, includeDeadTargets: true) +{ + private readonly P4DarklitDragonsong? _assignments = module.FindComponent(); + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.SpellInWaitingDarkWater) + { + BitMask forbidden = default; + if (_assignments != null && _assignments.AssignS.Any()) + { + var isSouth = _assignments.AssignS[Raid.FindSlot(actor.InstanceID)]; + forbidden = _assignments.AssignS ^ new BitMask(isSouth ? 0xFF : 0u); + } + AddStack(actor, status.ExpireAt, forbidden); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.DarkWater) + Stacks.Clear(); + } +} + +class P4SomberDance(BossModule module) : Components.GenericBaitAway(module, centerAtTarget: true) +{ + private Actor? _source; + private DateTime _activation; + + private static readonly AOEShapeCircle _shape = new(8); + + public override void Update() + { + CurrentBaits.Clear(); + if (_source == null) + return; + var targets = Raid.WithoutSlot(excludeNPCs: true); + var target = NumCasts == 0 ? targets.Farthest(_source.Position) : targets.Closest(_source.Position); + if (target != null) + CurrentBaits.Add(new(_source, target, _shape, _activation)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.SomberDance) + { + ForbiddenPlayers = Raid.WithSlot(true).WhereActor(p => p.Role != Role.Tank).Mask(); + _source = caster; + _activation = Module.CastFinishAt(spell, 0.4f); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.SomberDanceAOE1 or AID.SomberDanceAOE2) + { + ++NumCasts; + _activation = WorldState.FutureTime(3.2f); + foreach (var t in spell.Targets) + ForbiddenPlayers.Set(Raid.FindSlot(t.ID)); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs new file mode 100644 index 0000000000..1ec3b55c60 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs @@ -0,0 +1,35 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P4MornAfah(BossModule module) : Components.UniformStackSpread(module, 4, 0, 8) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.MornAfahOracle) + { + // note: target is random?.. + var target = WorldState.Actors.Find(caster.TargetID); + if (target != null) + AddStack(target, Module.CastFinishAt(spell, 0.9f)); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.MornAfahAOE) // TODO: proper spell... + Stacks.Clear(); + } +} + +class P4MornAfahHPCheck(BossModule module) : BossComponent(module) +{ + public override void AddGlobalHints(GlobalHints hints) + { + var usurper = Module.Enemies(OID.UsurperOfFrostP4).FirstOrDefault(); + var oracle = Module.Enemies(OID.OracleOfDarknessP4).FirstOrDefault(); + if (usurper != null && oracle != null) + { + var diff = (int)(usurper.HPMP.CurHP - oracle.HPMP.CurHP) * 100.0f / usurper.HPMP.MaxHP; + hints.Add($"Usurper HP: {(diff > 0 ? "+" : "")}{diff:f1}%"); + } + } +} From 3923cc4422048a3d91ab69da708acdec4b498637 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Fri, 27 Dec 2024 02:53:05 +0100 Subject: [PATCH 11/12] fix Sphene setting default --- BossMod/AI/AIConfig.cs | 4 ++-- .../Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs | 6 +++--- .../Dawntrail/Extreme/Ex3QueenEternal/VirtualShiftIce.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/BossMod/AI/AIConfig.cs b/BossMod/AI/AIConfig.cs index 96911d4883..c86106bad3 100644 --- a/BossMod/AI/AIConfig.cs +++ b/BossMod/AI/AIConfig.cs @@ -7,10 +7,10 @@ sealed class AIConfig : ConfigNode public bool ShowDTR = false; [PropertyDisplay("Show AI interface")] - public bool DrawUI = true; + public bool DrawUI = false; [PropertyDisplay("Focus target master")] - public bool FocusTargetLeader = true; + public bool FocusTargetLeader = false; [PropertyDisplay("Broadcast keypresses to other windows")] public bool BroadcastToSlaves = false; diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs b/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs index 0a3d2c1243..fe43c19c2e 100644 --- a/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs +++ b/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs @@ -6,7 +6,7 @@ class Ex3QueenEternalConfig() : ConfigNode() [PropertyDisplay("Absolute Authority: ignore flares, stack together")] public bool AbsoluteAuthorityIgnoreFlares = true; - [PropertyDisplay("Fixed bridge tether spots", tooltip: "Side tethers stretch without crossing is typically used on EU/NA partyfinder, the other option is usually used by JP")] - [PropertyCombo("Side tethers stretch without crossing", "Side tethers stretch cross (JP)")] - public bool SideTethersNoCrossing = true; + [PropertyDisplay("Fixed bridge spots for West/East tethers", tooltip: "West/East stretch tethers without crossing is typically used on EU/NA partyfinder, the other option is usually used by JP")] + [PropertyCombo("No crossing", "Cross (JP)")] + public bool SideTethersCrossStrategy = false; } diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/VirtualShiftIce.cs b/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/VirtualShiftIce.cs index 1084b7715a..92bbce1c32 100644 --- a/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/VirtualShiftIce.cs +++ b/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/VirtualShiftIce.cs @@ -157,7 +157,7 @@ private static WPos SafeSpot(Actor source, Ex3QueenEternalConfig config) { // second order var central = source.Position.Z < 96; - var strat = !config.SideTethersNoCrossing ? (central ? -2 : 9) : (central ? 9 : -9); + var strat = !config.SideTethersCrossStrategy ? (central ? -2 : 9) : (central ? 9 : -9); return center + new WDir(safeSide * 15, strat); } } From 1a7ba899ac8f1a888b4a3b29b66d5ea78e00ca26 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Fri, 27 Dec 2024 07:16:43 +0100 Subject: [PATCH 12/12] chaotic stuff and merge fixes --- BossMod/BossModule/BossModuleInfo.cs | 1 + BossMod/Config/ModuleViewer.cs | 1 + .../Alliance/A11Prishe/ArenaChanges.cs | 9 +-- .../Ch01CloudOfDarkness/ArenaChanges.cs | 73 +++++++++++++++++++ .../Ch01CloudOfDarkness.cs | 67 ++++------------- .../Ch01CloudOfDarknessEnums.cs | 7 +- .../Chaotic/Ch01CloudOfDarkness/Enaero.cs | 4 +- .../Chaotic/Ch01CloudOfDarkness/Endeath.cs | 8 +- .../D092OverseerKanilokka.cs | 14 ++-- .../Modules/Dawntrail/Ultimate/FRU/FRUAI.cs | 7 -- .../Dawntrail/Ultimate/FRU/P2DiamondDust.cs | 21 ++---- .../Ultimate/FRU/P4DarklitDragonsong.cs | 2 +- .../Extreme/Ex3Titan/Ex3TitanAI.cs | 5 +- BossMod/Util/Angle.cs | 11 --- 14 files changed, 120 insertions(+), 110 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ArenaChanges.cs diff --git a/BossMod/BossModule/BossModuleInfo.cs b/BossMod/BossModule/BossModuleInfo.cs index 210c23caa8..3c07066cf1 100644 --- a/BossMod/BossModule/BossModuleInfo.cs +++ b/BossMod/BossModule/BossModuleInfo.cs @@ -38,6 +38,7 @@ public enum Category Ultimate, Unreal, Alliance, + Chaotic, Foray, VariantCriterion, DeepDungeon, diff --git a/BossMod/Config/ModuleViewer.cs b/BossMod/Config/ModuleViewer.cs index 6470090336..82c01419a0 100644 --- a/BossMod/Config/ModuleViewer.cs +++ b/BossMod/Config/ModuleViewer.cs @@ -58,6 +58,7 @@ public ModuleViewer(PlanDatabase? planDB, WorldState ws) Customize(BossModuleInfo.Category.DeepDungeon, contentType.GetRow(21)); Customize(BossModuleInfo.Category.Ultimate, contentType.GetRow(28)); Customize(BossModuleInfo.Category.VariantCriterion, contentType.GetRow(30)); + Customize(BossModuleInfo.Category.Chaotic, contentType.GetRow(37)); var playStyle = Service.LuminaSheet()!; Customize(BossModuleInfo.Category.Foray, playStyle.GetRow(6)); diff --git a/BossMod/Modules/Dawntrail/Alliance/A11Prishe/ArenaChanges.cs b/BossMod/Modules/Dawntrail/Alliance/A11Prishe/ArenaChanges.cs index 612bcc7703..99562d8788 100644 --- a/BossMod/Modules/Dawntrail/Alliance/A11Prishe/ArenaChanges.cs +++ b/BossMod/Modules/Dawntrail/Alliance/A11Prishe/ArenaChanges.cs @@ -37,17 +37,12 @@ public override void OnEventEnvControl(byte index, uint state) SetArena(ArenaENVC02000100); break; case 0x00080004 or 0x00800004: - SetDefaultArena(); + Arena.Bounds = A11Prishe.DefaultBounds; + Arena.Center = A11Prishe.ArenaCenter; break; } } - private void SetDefaultArena() - { - Arena.Bounds = A11Prishe.DefaultBounds; - Arena.Center = A11Prishe.ArenaCenter; - } - private void SetArena(ArenaBoundsComplex bounds) { Arena.Bounds = bounds; diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ArenaChanges.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ArenaChanges.cs new file mode 100644 index 0000000000..ab9b1434eb --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ArenaChanges.cs @@ -0,0 +1,73 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +class ArenaChanges(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + private static readonly Square[] DefaultPolygon = [new(Ch01CloudOfDarkness.DefaultCenter, 40)]; + private static readonly AOEShapeCustom P1Transition = new(DefaultPolygon, Ch01CloudOfDarkness.Diamond); + private static readonly AOEShapeCustom P2Transition = new(DefaultPolygon, Ch01CloudOfDarkness.Phase2ShapesWD); + private static readonly AOEShapeDonut donut = new(34, 40); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 0x00) + switch (state) + { + case 0x00200010: + SetAOE(P1Transition); + break; + case 0x00020001: + SetAOE(P2Transition); + break; + } + else if (index == 0x02) + switch (state) + { + case 0x00020001: + SetArena(Ch01CloudOfDarkness.Phase2BoundsND); + break; + case 0x00080004: + SetArena(Ch01CloudOfDarkness.Phase2BoundsWD); + break; + } + } + + public override void OnEventDirectorUpdate(uint updateID, uint param1, uint param2, uint param3, uint param4) + { + if (updateID != 0x8000000D) + return; + switch (param1) + { + case 0x10000000: // default arena + Arena.Bounds = Ch01CloudOfDarkness.DefaultArena; + Arena.Center = Ch01CloudOfDarkness.DefaultCenter; + break; + case 0x20000000: // (phase 2) + SetArena(Ch01CloudOfDarkness.Phase2BoundsWD); + break; + case 0x40000000: // diamond arena (phase 1) + SetArena(Ch01CloudOfDarkness.Phase1Bounds); + break; + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.DarkDominion) + SetAOE(donut); + } + + private void SetArena(ArenaBoundsComplex bounds) + { + Arena.Bounds = bounds; + Arena.Center = bounds.Center; + _aoe = null; + } + + private void SetAOE(AOEShape shape) + { + _aoe = new(shape, Arena.Center, default, WorldState.FutureTime(9)); + } +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs index 1b8a0cfc19..02af1473bf 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs @@ -17,7 +17,8 @@ class Ch01CloudOfDarknessStates : StateMachineBuilder { public Ch01CloudOfDarknessStates(BossModule module) : base(module) { - DeathPhase(0, SinglePhase); + DeathPhase(0, SinglePhase) + .ActivateOnEnter(); } private void SinglePhase(uint id) @@ -58,65 +59,25 @@ private void SinglePhase(uint id) } } -// TODO: mechanic phase bounds // TODO: flood bounds & squares // TODO: particle concentration towers // TODO: evil seed // TODO: chaser beam // TODO: tankswap hints? -[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624)] -public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultCenter, InitialBounds) +[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624)] +public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultCenter, DefaultArena) { public static readonly WPos DefaultCenter = new(100, 100); - public static readonly ArenaBoundsCircle InitialBounds = new(40); - public static readonly ArenaBoundsCustom Phase1Bounds = new(InitialBounds.Radius, new(BuildPhase1BoundsContour())); - public static readonly ArenaBoundsCustom Phase2Bounds = new(InitialBounds.Radius, BuildPhase2BoundsPoly()); - public static readonly WPos Phase1Midpoint = DefaultCenter + Phase1Bounds.Poly.Parts[0].Vertices[1] + Phase1Bounds.Poly.Parts[0].Vertices[3]; - - public static List BuildPhase1BoundsContour() - { - // north 'diagonal' is at [+/-15, -37] (it almost intersects the initial circle - at x=15 z is ~37.08) - // the main diagonal is 20, rotated by 45 degrees, which means that side corners are at x=+/- 40/sqrt(2), z = -37 + 40/sqrt(2) - 15 - var nz = -37; - var nx = 15; - var halfDiag = 40 / MathF.Sqrt(2); - var cz = nz + halfDiag - nx; - return [new(nx, nz), new(halfDiag, cz), new(0, cz + halfDiag), new(-halfDiag, cz), new(-nx, nz)]; - } - - public static RelSimplifiedComplexPolygon BuildPhase2BoundsPoly() - { - // mid is union of 4 rects - var midHalfWidth = 3; - var midHalfLength = 24; - var midOffset = 15; - var op1 = new PolygonClipper.Operand(); - var op2 = new PolygonClipper.Operand(); - op1.AddContour(CurveApprox.Rect(new WDir(0, +midOffset), new(1, 0), midHalfWidth, midHalfLength)); - op1.AddContour(CurveApprox.Rect(new WDir(0, -midOffset), new(1, 0), midHalfWidth, midHalfLength)); - op2.AddContour(CurveApprox.Rect(new WDir(+midOffset, 0), new(0, 1), midHalfWidth, midHalfLength)); - op2.AddContour(CurveApprox.Rect(new WDir(-midOffset, 0), new(0, 1), midHalfWidth, midHalfLength)); - var mid = InitialBounds.Clipper.Union(op1, op2); - - // sides is union of two platforms and the outside ring - var sideHalfWidth = 7.5f; - var sideHalfLength = 10; - var sideOffset = 19 + sideHalfLength; - var sideRingWidth = 6; - op1.Clear(); - op2.Clear(); - op1.AddContour(CurveApprox.Rect(new WDir(+sideOffset, 0), new(1, 0), sideHalfWidth, sideHalfLength)); - op1.AddContour(CurveApprox.Rect(new WDir(-sideOffset, 0), new(1, 0), sideHalfWidth, sideHalfLength)); - op2.AddContour(CurveApprox.Circle(InitialBounds.Radius, 0.1f)); - op2.AddContour(CurveApprox.Circle(InitialBounds.Radius - sideRingWidth, 0.1f)); - var side = InitialBounds.Clipper.Union(op1, op2); - - op1.Clear(); - op2.Clear(); - op1.AddPolygon(mid); - op2.AddPolygon(side); - return InitialBounds.Clipper.Union(op1, op2); - } + public static readonly WPos Phase1BoundsCenter = new(100, 76.28427f); + public static readonly PolygonCustom[] Diamond = [new([new(115, 63), new(128.28427f, 76.28427f), new(100, 104.56854f), new(71.71573f, 76.28427f), new(85, 63)])]; + private static readonly DonutV[] donut = [new(DefaultCenter, 34, 40, 80)]; + public static readonly Shape[] Phase2ShapesND = [new Rectangle(new(100, 115), 24, 3), new Rectangle(new(100, 85), 24, 3), new Rectangle(new(115, 100), 3, 24), + new Rectangle(new(85, 100), 3, 24), new Square(new(126.5f, 100), 7.5f), new Square(new(73.5f, 100), 7.5f)]; + public static readonly Shape[] Phase2ShapesWD = [.. donut, .. Phase2ShapesND]; + public static readonly ArenaBoundsCircle DefaultArena = new(40); + public static readonly ArenaBoundsComplex Phase1Bounds = new(Diamond); + public static readonly ArenaBoundsComplex Phase2BoundsWD = new(Phase2ShapesWD); + public static readonly ArenaBoundsComplex Phase2BoundsND = new(Phase2ShapesND, donut); } // envcontrols: diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs index 617023a819..9628060f00 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs @@ -128,7 +128,12 @@ public enum AID : uint LoomingChaosBoss = 41674, // Boss->self, 7.0s cast, single-target, visual (position swaps) LoomingChaosAOE = 41675, // Helper->self, 7.7s cast, range 50 circle, raidwide + position swaps - //_Weaponskill_FeintParticleBeam = 40477, // Boss->self, 6.0+0.7s cast, single-target + FeintParticleBeamVisual = 40477, // Boss->self, 6.0+0,7s cast, single-target, chasing AOE + FeintParticleBeamFirst = 40478, // Helper->location, 4.0s cast, range 3 circle + FeintParticleBeamRest = 40479, // Helper->location, no cast, range 3 circle + FloodOfDarkness2 = 40455, // Boss->location, 7.0s cast, range 60 circle + Evaporation = 40454, // StygianShadow->Boss, 2.0s cast, single-target + DelugeOfDarknessEnrage = 40533, // Boss->location, 12.0s cast, range 100 circle } public enum SID : uint diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs index 7828a9c80a..897f864a87 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs @@ -59,7 +59,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } } - private void Start(DateTime activation) => _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation); + private void Start(DateTime activation) => _source = new(Ch01CloudOfDarkness.Phase1BoundsCenter, 15, activation); } class EnaeroAOE(BossModule module) : Components.GenericAOEs(module) @@ -103,7 +103,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) private void Start(DateTime activation) { - _aoe = new(_shape, Ch01CloudOfDarkness.Phase1Midpoint, default, activation); + _aoe = new(_shape, Ch01CloudOfDarkness.Phase1BoundsCenter, default, activation); _delayed = false; } } diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs index 35f468ddf5..17887d8f75 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs @@ -7,7 +7,7 @@ class EndeathVortex(BossModule module) : Components.Knockback(module) public override IEnumerable Sources(int slot, Actor actor) => Utils.ZeroOrOne(_source); - public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => (pos - Ch01CloudOfDarkness.Phase1Midpoint).LengthSq() <= 36; + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => (pos - Ch01CloudOfDarkness.Phase1BoundsCenter).LengthSq() <= 36; public override void AddHints(int slot, Actor actor, TextHints hints) { @@ -61,7 +61,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } } - private void Start(DateTime activation) => _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation, Kind: Kind.TowardsOrigin); + private void Start(DateTime activation) => _source = new(Ch01CloudOfDarkness.Phase1BoundsCenter, 15, activation, Kind: Kind.TowardsOrigin); } class EndeathAOE(BossModule module) : Components.GenericAOEs(module) @@ -109,8 +109,8 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) private void Start(DateTime activation) { - _aoes.Add(new(_shapeOut, Ch01CloudOfDarkness.Phase1Midpoint, default, activation.AddSeconds(2))); - _aoes.Add(new(_shapeIn, Ch01CloudOfDarkness.Phase1Midpoint, default, activation.AddSeconds(4))); + _aoes.Add(new(_shapeOut, Ch01CloudOfDarkness.Phase1BoundsCenter, default, activation.AddSeconds(2))); + _aoes.Add(new(_shapeIn, Ch01CloudOfDarkness.Phase1BoundsCenter, default, activation.AddSeconds(4))); _delayed = false; } } diff --git a/BossMod/Modules/Dawntrail/Dungeon/D09YuweyawataFieldStation/D092OverseerKanilokka.cs b/BossMod/Modules/Dawntrail/Dungeon/D09YuweyawataFieldStation/D092OverseerKanilokka.cs index 2d15fed2ca..242790320e 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D09YuweyawataFieldStation/D092OverseerKanilokka.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D09YuweyawataFieldStation/D092OverseerKanilokka.cs @@ -56,28 +56,28 @@ public override void OnEventEnvControl(byte index, uint state) switch (state) { case 0x00020001: - SetArena(D092OverseerKanilokka.DefaultArena, D092OverseerKanilokka.DefaultArena.Center); + SetArena(D092OverseerKanilokka.DefaultArena); break; case 0x00200010: - SetArena(D092OverseerKanilokka.TinyArena, D092OverseerKanilokka.TinyArena.Center); + SetArena(D092OverseerKanilokka.TinyArena); break; case 0x00800040: - SetArena(D092OverseerKanilokka.ArenaENVC00800040, D092OverseerKanilokka.ArenaENVC00800040.Center); + SetArena(D092OverseerKanilokka.ArenaENVC00800040); break; case 0x02000100: - SetArena(D092OverseerKanilokka.ArenaENVC02000100, D092OverseerKanilokka.ArenaENVC02000100.Center); + SetArena(D092OverseerKanilokka.ArenaENVC02000100); break; case 0x00080004: - SetArena(D092OverseerKanilokka.StartingBounds, D092OverseerKanilokka.StartingBounds.Center); + SetArena(D092OverseerKanilokka.StartingBounds); break; } _aoe = null; } - private void SetArena(ArenaBounds bounds, WPos center) + private void SetArena(ArenaBoundsComplex bounds) { Arena.Bounds = bounds; - Arena.Center = center; + Arena.Center = bounds.Center; } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs index a60cb161f3..8b34ba8422 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs @@ -37,13 +37,6 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa _ => Player.Position }; - // TODO: account for leeway for casters - private WPos PathfindPosition(Actor? meleeGreedTarget) - { - var res = NavigationDecision.Build(NavigationContext, World, Hints, Player, Speed()); - return meleeGreedTarget != null && res.Destination != null ? ClosestInMelee(res.Destination.Value, meleeGreedTarget) : (res.Destination ?? Player.Position); - } - // assumption: pull range is 12; hitbox is 5, so maxmelee is 8, meaning we have approx 4m to move during pull - with sprint, speed is 7.8, accel is 30 => over 0.26s accel period we move 1.014m, then need another 0.38s to reach boss (but it also moves) private WPos PrepullPosition(FRU module, PartyRolesConfig.Assignment assignment) { diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs index dcb423cdad..eccbd4212b 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs @@ -219,14 +219,14 @@ public override IEnumerable Sources(int slot, Actor actor) public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { if (_safeDirs[slot] != default) - hints.AddForbiddenZone(ShapeDistance.PrecisePosition(Module.Center + 6 * _safeDirs[slot], new(1, 0), Module.Bounds.MapResolution, actor.Position, 0.25f), _activation); + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(Arena.Center + 6 * _safeDirs[slot], new(1, 0), Arena.Bounds.MapResolution, actor.Position, 0.25f), _activation); } public override void DrawArenaForeground(int pcSlot, Actor pc) { base.DrawArenaForeground(pcSlot, pc); if (_safeDirs[pcSlot] != default) - Arena.AddCircle(Module.Center + 18 * _safeDirs[pcSlot], 1, ArenaColor.Safe); + Arena.AddCircle(Arena.Center + 18 * _safeDirs[pcSlot], 1, Colors.Safe); } private static WDir[] BuildSafeDirs(BossModule module) @@ -346,7 +346,6 @@ class P2TwinStillnessSilence(BossModule module) : Components.GenericAOEs(module) private BitMask _thinIce; private P2SinboundHolyVoidzone? _voidzones; // used for hints only - private const float SlideDistance = 32; private readonly AOEShapeCone _shapeFront = new(30, 135.Degrees()); private readonly AOEShapeCone _shapeBack = new(30, 45.Degrees()); @@ -371,7 +370,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme // first, find a set of allowed angles along the border var zoneList = new ArcList(Arena.Center, 17); foreach (var z in _voidzones.Sources(Module)) - zoneList.ForbidCircle(z.Position, _voidzones.Shape.Radius); + zoneList.ForbidCircle(z.Position, 6); // now find closest allowed zone var actorDir = Angle.FromDirection(actor.Position - Module.Center); @@ -384,21 +383,17 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { // destination is very wide, narrow it down a bit to be in line with the boss halfWidth = 5.Degrees(); - var sourceDir = Angle.FromDirection(_source.Position - Module.Center); + var sourceDir = Angle.FromDirection(_source.Position - Arena.Center); var sourceDist = sourceDir.DistanceToRange(closest.min + halfWidth, closest.max - halfWidth); var oppositeDist = (sourceDir + 180.Degrees()).DistanceToRange(closest.min + halfWidth, closest.max - halfWidth); desiredDir = oppositeDist.Abs().Rad < sourceDist.Abs().Rad ? (sourceDir + 180.Degrees() + oppositeDist) : (sourceDir + sourceDist); } - hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 16), WorldState.FutureTime(50)); - hints.AddForbiddenZone(ShapeDistance.InvertedCone(Module.Center, 100, desiredDir, halfWidth), DateTime.MaxValue); + hints.AddForbiddenZone(ShapeDistance.Circle(Arena.Center, 16), WorldState.FutureTime(50)); + hints.AddForbiddenZone(ShapeDistance.InvertedCone(Arena.Center, 100, desiredDir, halfWidth), DateTime.MaxValue); } return; } - // at this point, we have thin ice, so we can either stay or move fixed distance - hints.AddForbiddenZone(ShapeDistance.Donut(actor.Position, 1, 31)); - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(actor.Position, 33)); - if (AOEs.Count == 0) { // if we're behind boss, slide over @@ -416,8 +411,8 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { var offset = z.Position - actor.Position; var dist = offset.Length(); - if (dist > _voidzones.Shape.Radius) - hints.AddForbiddenZone(ShapeDistance.Cone(actor.Position, 100, Angle.FromDirection(offset), Angle.Asin(dist / _voidzones.Shape.Radius))); + if (dist > 6) + hints.AddForbiddenZone(ShapeDistance.Cone(actor.Position, 100, Angle.FromDirection(offset), Angle.Asin(dist / 6))); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs index 9114fb9f4a..d0999d9fcd 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs @@ -19,7 +19,7 @@ public override void AddGlobalHints(GlobalHints hints) public override void DrawArenaForeground(int pcSlot, Actor pc) { foreach (var t in _tethers) - Arena.AddLine(t.from.Position, t.to.Position, ArenaColor.Safe); // TODO: min/max break distance + Arena.AddLine(t.from.Position, t.to.Position, Colors.Safe); // TODO: min/max break distance } public override void OnStatusGain(Actor actor, ActorStatus status) diff --git a/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs b/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs index 60107d646f..dcda2b676f 100644 --- a/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs +++ b/BossMod/Modules/RealmReborn/Extreme/Ex3Titan/Ex3TitanAI.cs @@ -1,20 +1,18 @@ using BossMod.AI; using BossMod.Autorotation; -using BossMod.Pathfinding; namespace BossMod.RealmReborn.Extreme.Ex3Titan; sealed class Ex3TitanAIRotation(RotationModuleManager manager, Actor player) : AIRotationModule(manager, player) { public enum Track { Movement } - public enum MovementStrategy { None, Pathfind, Explicit } + public enum MovementStrategy { None, Explicit } public static RotationModuleDefinition Definition() { var res = new RotationModuleDefinition("AI Experiment", "Experimental encounter-specific rotation", "Encounter AI", "veyn", RotationModuleQuality.WIP, new(~1ul), 1000, 1, typeof(Ex3Titan)); res.Define(Track.Movement).As("Movement", "Movement") .AddOption(MovementStrategy.None, "None", "No automatic movement") - .AddOption(MovementStrategy.Pathfind, "Pathfind", "Use standard pathfinding to move") .AddOption(MovementStrategy.Explicit, "Explicit", "Move to specific point", supportedTargets: ActionTargets.Area); return res; } @@ -26,7 +24,6 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa private WPos CalculateDestination(StrategyValues.OptionRef strategy) => strategy.As() switch { - MovementStrategy.Pathfind => NavigationDecision.Build(NavigationContext, World, Hints, Player, Speed()).Destination ?? Player.Position, MovementStrategy.Explicit => ResolveTargetLocation(strategy.Value), _ => Player.Position }; diff --git a/BossMod/Util/Angle.cs b/BossMod/Util/Angle.cs index 8127312f07..5f4b9880ee 100644 --- a/BossMod/Util/Angle.cs +++ b/BossMod/Util/Angle.cs @@ -62,17 +62,6 @@ public readonly Angle DistanceToRange(Angle min, Angle max) return midDist.Rad > width.Rad ? midDist - width : midDist.Rad < -width.Rad ? midDist + width : default; } - // closest distance to move from this angle to destination (== 0 if equal, >0 if moving in positive/CCW dir, <0 if moving in negative/CW dir) - public readonly Angle DistanceToAngle(Angle other) => (other - this).Normalized(); - - // returns 0 if angle is within range, positive value if min is closest, negative if max is closest - public readonly Angle DistanceToRange(Angle min, Angle max) - { - var width = (max - min) * 0.5f; - var midDist = DistanceToAngle((min + max) * 0.5f); - return midDist.Rad > width.Rad ? midDist - width : midDist.Rad < -width.Rad ? midDist + width : default; - } - public override readonly string ToString() => Deg.ToString("f3"); }