From 2a2001d9671ba037134b41221070415e092696ce Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:28:03 +0200 Subject: [PATCH] Tower of Babil modules --- BossMod/AI/AIManager.cs | 8 +- BossMod/Autorotation/CommonActions.cs | 2 +- BossMod/BossModule/AOEShapes.cs | 126 +++++++---- BossMod/BossModule/BossModule.cs | 12 +- BossMod/BossModule/Shapes.cs | 118 +++++++++- BossMod/Components/BaitAway.cs | 11 +- BossMod/Components/ChasingAOEs.cs | 11 + BossMod/Components/Knockback.cs | 13 +- BossMod/Components/StackSpread.cs | 116 +++++++++- BossMod/Components/Tethers.cs | 210 +++++++++++++++++- BossMod/Data/Actor.cs | 1 + BossMod/Data/PartyState.cs | 2 - .../Endwalker/Alliance/A10Lions/A10Lions.cs | 2 +- .../Endwalker/Alliance/A13Azeyma/A13Azeyma.cs | 4 +- .../Alliance/A13Azeyma/WildfireWard.cs | 23 +- .../Alliance/A32Llymlaen/SurgingWave.cs | 11 +- .../DeepDungeon/EurekaOrthos/DD70Aeturna.cs | 96 +------- .../Dungeon/D02TowerOfBabil/D021Barnabas.cs | 196 +++++++++++++--- .../Dungeon/D02TowerOfBabil/D022Lugae.cs | 83 ++++++- .../Dungeon/D02TowerOfBabil/D023Anima.cs | 163 ++++++++++---- .../Dungeon/D03Vanaspati/D033Svarbhanu.cs | 2 +- .../Endwalker/Dungeon/D06DeadEnds/D063Rala.cs | 16 +- .../D11LapisManalis/D112GalateaMagna.cs | 4 +- .../Dungeon/D11LapisManalis/D113Cagnazzo.cs | 4 +- .../Dungeon/D12Aetherfont/D121Lyngbakr.cs | 2 +- .../Dungeon/D12Aetherfont/D122Arkas.cs | 89 +++++--- .../Dungeon/D13LunarSubterrane/D133Durante.cs | 44 +--- .../GymnasiouMegakantha.cs | 3 +- .../Modules/Global/Quest/FF15Collab/Garuda.cs | 48 +--- .../D063LahabreaIgeyorhm.cs | 57 +---- .../D06Aetherochemical/D064AscianPrime.cs | 71 ++---- .../Dungeon/D01Holminster/D013Philia.cs | 42 +--- .../Dungeon/D03QitanaRavel/D033Eros.cs | 147 +----------- .../D05MtGulg/D055ForgivenObscenity.cs | 40 ++-- .../Stormblood/Trial/T09Seiryu/BlueBolt.cs | 43 ---- .../Trial/T09Seiryu/ForbiddenArts.cs | 49 ---- .../Stormblood/Trial/T09Seiryu/T09Seiryu.cs | 31 ++- .../Trial/T09Seiryu/T09SeiryuEnums.cs | 14 +- BossMod/Replay/Analysis/AbilityInfo.cs | 11 +- BossMod/Replay/Analysis/ParticipantInfo.cs | 2 +- BossMod/Replay/Analysis/StatusInfo.cs | 3 +- .../Visualization/ColumnEnemiesCastEvents.cs | 4 +- .../Visualization/ColumnEnemiesDetails.cs | 2 +- BossMod/Replay/Visualization/ColumnUtils.cs | 2 +- BossMod/Replay/Visualization/EventList.cs | 4 +- BossMod/Replay/Visualization/OpList.cs | 14 +- .../Visualization/ReplayDetailsWindow.cs | 2 +- BossMod/Util/ShapeDistance.cs | 19 ++ 48 files changed, 1135 insertions(+), 842 deletions(-) delete mode 100644 BossMod/Modules/Stormblood/Trial/T09Seiryu/BlueBolt.cs delete mode 100644 BossMod/Modules/Stormblood/Trial/T09Seiryu/ForbiddenArts.cs diff --git a/BossMod/AI/AIManager.cs b/BossMod/AI/AIManager.cs index 56efb7bb44..3a691712b6 100644 --- a/BossMod/AI/AIManager.cs +++ b/BossMod/AI/AIManager.cs @@ -7,6 +7,7 @@ namespace BossMod.AI; sealed class AIManager : IDisposable { + public static AIManager? Instance { get; private set; } public readonly Autorotation Autorot; public readonly AIController Controller; private readonly AIConfig _config; @@ -17,6 +18,7 @@ sealed class AIManager : IDisposable public AIManager(Autorotation autorot) { + Instance = this; _wndAI = new AIManagementWindow(this); Autorot = autorot; Controller = new(); @@ -292,9 +294,7 @@ private bool ToggleFollowCombat() _config.FollowDuringActiveBossModule = false; } else - { _config.FollowDuringCombat = true; - } Service.Log($"[AI] Follow during combat is now {(_config.FollowDuringCombat ? "enabled" : "disabled")}"); Service.Log($"[AI] Follow during active boss module is now {(_config.FollowDuringActiveBossModule ? "enabled" : "disabled")}"); return true; @@ -303,9 +303,7 @@ private bool ToggleFollowCombat() private bool ToggleFollowModule() { if (_config.FollowDuringActiveBossModule) - { _config.FollowDuringActiveBossModule = false; - } else { _config.FollowDuringActiveBossModule = true; @@ -319,9 +317,7 @@ private bool ToggleFollowModule() private bool ToggleFollowTarget(string[] messageData) { if (messageData.Length == 1) - { _config.FollowTarget = !_config.FollowTarget; - } else { switch (messageData[1].ToUpperInvariant()) diff --git a/BossMod/Autorotation/CommonActions.cs b/BossMod/Autorotation/CommonActions.cs index 3505626a4f..7b5edbd6f6 100644 --- a/BossMod/Autorotation/CommonActions.cs +++ b/BossMod/Autorotation/CommonActions.cs @@ -383,7 +383,7 @@ protected void FillStrategyPositionals(CommonRotation.Strategy strategy, (Positi // smart targeting utility: return target (if friendly) or mouseover (if friendly) or null (otherwise) protected Actor? SmartTargetFriendly(Actor? primaryTarget) - => primaryTarget?.Type is ActorType.Player or ActorType.Chocobo ? primaryTarget : Autorot.SecondaryTarget?.Type is ActorType.Player or ActorType.Chocobo ? Autorot.SecondaryTarget : null; + => primaryTarget?.Type is ActorType.Player or ActorType.Chocobo or ActorType.Buddy ? primaryTarget : Autorot.SecondaryTarget?.Type is ActorType.Player or ActorType.Chocobo or ActorType.Buddy ? Autorot.SecondaryTarget : null; // smart targeting utility: return mouseover (if hostile and allowed) or target (otherwise) protected Actor? SmartTargetHostile(Actor? primaryTarget) diff --git a/BossMod/BossModule/AOEShapes.cs b/BossMod/BossModule/AOEShapes.cs index a4514e9a66..bbdf4b408a 100644 --- a/BossMod/BossModule/AOEShapes.cs +++ b/BossMod/BossModule/AOEShapes.cs @@ -25,27 +25,37 @@ public void Outline(MiniArena arena, Actor? origin, uint color = ArenaColor.Dang } } -public sealed record class AOEShapeCone(float Radius, Angle HalfAngle, Angle DirectionOffset = default) : AOEShape +public sealed record class AOEShapeCone(float Radius, Angle HalfAngle, Angle DirectionOffset = default, bool InvertForbiddenZone = false) : AOEShape { - public override string ToString() => $"Cone: r={Radius:f3}, angle={HalfAngle * 2}, off={DirectionOffset}"; + public override string ToString() => $"Cone: r={Radius:f3}, angle={HalfAngle * 2}, off={DirectionOffset}, ivz={InvertForbiddenZone}"; public override bool Check(WPos position, WPos origin, Angle rotation) => position.InCircleCone(origin, Radius, rotation + DirectionOffset, HalfAngle); public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) => arena.ZoneCone(origin, 0, Radius, rotation + DirectionOffset, HalfAngle, color); public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.Danger) => arena.AddCone(origin, Radius, rotation + DirectionOffset, HalfAngle, color); - public override Func Distance(WPos origin, Angle rotation) => ShapeDistance.Cone(origin, Radius, rotation + DirectionOffset, HalfAngle); + public override Func Distance(WPos origin, Angle rotation) + { + return !InvertForbiddenZone + ? ShapeDistance.Cone(origin, Radius, rotation + DirectionOffset, HalfAngle) + : ShapeDistance.InvertedCone(origin, Radius, rotation + DirectionOffset, HalfAngle); + } } -public sealed record class AOEShapeCircle(float Radius) : AOEShape +public sealed record class AOEShapeCircle(float Radius, bool InvertForbiddenZone = false) : AOEShape { - public override string ToString() => $"Circle: r={Radius:f3}"; + public override string ToString() => $"Circle: r={Radius:f3}, ivz={InvertForbiddenZone}"; public override bool Check(WPos position, WPos origin, Angle rotation = new()) => position.InCircle(origin, Radius); public override void Draw(MiniArena arena, WPos origin, Angle rotation = new(), uint color = ArenaColor.AOE) => arena.ZoneCircle(origin, Radius, color); public override void Outline(MiniArena arena, WPos origin, Angle rotation = new(), uint color = ArenaColor.Danger) => arena.AddCircle(origin, Radius, color); - public override Func Distance(WPos origin, Angle rotation) => ShapeDistance.Circle(origin, Radius); + public override Func Distance(WPos origin, Angle rotation) + { + return !InvertForbiddenZone + ? ShapeDistance.Circle(origin, Radius) + : ShapeDistance.InvertedCircle(origin, Radius); + } } -public sealed record class AOEShapeDonut(float InnerRadius, float OuterRadius) : AOEShape +public sealed record class AOEShapeDonut(float InnerRadius, float OuterRadius, bool InvertForbiddenZone = false) : AOEShape { - public override string ToString() => $"Donut: r={InnerRadius:f3}-{OuterRadius:f3}"; + public override string ToString() => $"Donut: r={InnerRadius:f3}-{OuterRadius:f3}, ivz={InvertForbiddenZone}"; public override bool Check(WPos position, WPos origin, Angle rotation = new()) => position.InDonut(origin, InnerRadius, OuterRadius); public override void Draw(MiniArena arena, WPos origin, Angle rotation = new(), uint color = ArenaColor.AOE) => arena.ZoneDonut(origin, InnerRadius, OuterRadius, color); public override void Outline(MiniArena arena, WPos origin, Angle rotation = new(), uint color = ArenaColor.Danger) @@ -53,30 +63,45 @@ public sealed record class AOEShapeDonut(float InnerRadius, float OuterRadius) : arena.AddCircle(origin, InnerRadius, color); arena.AddCircle(origin, OuterRadius, color); } - public override Func Distance(WPos origin, Angle rotation) => ShapeDistance.Donut(origin, InnerRadius, OuterRadius); + public override Func Distance(WPos origin, Angle rotation) + { + return !InvertForbiddenZone + ? ShapeDistance.Donut(origin, InnerRadius, OuterRadius) + : ShapeDistance.InvertedDonut(origin, InnerRadius, OuterRadius); + } } -public sealed record class AOEShapeDonutSector(float InnerRadius, float OuterRadius, Angle HalfAngle, Angle DirectionOffset = default) : AOEShape +public sealed record class AOEShapeDonutSector(float InnerRadius, float OuterRadius, Angle HalfAngle, Angle DirectionOffset = default, bool InvertForbiddenZone = false) : AOEShape { - public override string ToString() => $"Donut sector: r={InnerRadius:f3}-{OuterRadius:f3}, angle={HalfAngle * 2}, off={DirectionOffset}"; + public override string ToString() => $"Donut sector: r={InnerRadius:f3}-{OuterRadius:f3}, angle={HalfAngle * 2}, off={DirectionOffset}, ivz={InvertForbiddenZone}"; public override bool Check(WPos position, WPos origin, Angle rotation) => position.InDonutCone(origin, InnerRadius, OuterRadius, rotation + DirectionOffset, HalfAngle); public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) => arena.ZoneCone(origin, InnerRadius, OuterRadius, rotation + DirectionOffset, HalfAngle, color); public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.Danger) => arena.AddDonutCone(origin, InnerRadius, OuterRadius, rotation + DirectionOffset, HalfAngle, color); - public override Func Distance(WPos origin, Angle rotation) => ShapeDistance.DonutSector(origin, InnerRadius, OuterRadius, rotation + DirectionOffset, HalfAngle); + public override Func Distance(WPos origin, Angle rotation) + { + return !InvertForbiddenZone + ? ShapeDistance.DonutSector(origin, InnerRadius, OuterRadius, rotation + DirectionOffset, HalfAngle) + : ShapeDistance.InvertedDonutSector(origin, InnerRadius, OuterRadius, rotation + DirectionOffset, HalfAngle); + } } -public sealed record class AOEShapeRect(float LengthFront, float HalfWidth, float LengthBack = 0, Angle DirectionOffset = default) : AOEShape +public sealed record class AOEShapeRect(float LengthFront, float HalfWidth, float LengthBack = 0, Angle DirectionOffset = default, bool InvertForbiddenZone = false) : AOEShape { - public override string ToString() => $"Rect: l={LengthFront:f3}+{LengthBack:f3}, w={HalfWidth * 2}, off={DirectionOffset}"; + public override string ToString() => $"Rect: l={LengthFront:f3}+{LengthBack:f3}, w={HalfWidth * 2}, off={DirectionOffset}, ivz={InvertForbiddenZone}"; public override bool Check(WPos position, WPos origin, Angle rotation) => position.InRect(origin, rotation + DirectionOffset, LengthFront, LengthBack, HalfWidth); public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) => arena.ZoneRect(origin, rotation + DirectionOffset, LengthFront, LengthBack, HalfWidth, color); public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.Danger) => arena.AddRect(origin, (rotation + DirectionOffset).ToDirection(), LengthFront, LengthBack, HalfWidth, color); - public override Func Distance(WPos origin, Angle rotation) => ShapeDistance.Rect(origin, rotation + DirectionOffset, LengthFront, LengthBack, HalfWidth); + public override Func Distance(WPos origin, Angle rotation) + { + return !InvertForbiddenZone + ? ShapeDistance.Rect(origin, rotation + DirectionOffset, LengthFront, LengthBack, HalfWidth) + : ShapeDistance.InvertedRect(origin, rotation + DirectionOffset, LengthFront, LengthBack, HalfWidth); + } } -public sealed record class AOEShapeCross(float Length, float HalfWidth, Angle DirectionOffset = default) : AOEShape +public sealed record class AOEShapeCross(float Length, float HalfWidth, Angle DirectionOffset = default, bool InvertForbiddenZone = false) : AOEShape { - public override string ToString() => $"Cross: l={Length:f3}, w={HalfWidth * 2}, off={DirectionOffset}"; + public override string ToString() => $"Cross: l={Length:f3}, w={HalfWidth * 2}, off={DirectionOffset}, ivz={InvertForbiddenZone}"; public override bool Check(WPos position, WPos origin, Angle rotation) => position.InRect(origin, rotation + DirectionOffset, Length, Length, HalfWidth) || position.InRect(origin, rotation + DirectionOffset, HalfWidth, HalfWidth, Length); public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) => arena.ZonePoly((GetType(), origin, rotation + DirectionOffset, Length, HalfWidth), ContourPoints(origin, rotation), color); public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.Danger) @@ -86,8 +111,6 @@ public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint MiniArena.PathStroke(true, color); } - public override Func Distance(WPos origin, Angle rotation) => ShapeDistance.Cross(origin, rotation + DirectionOffset, Length, HalfWidth); - private IEnumerable ContourPoints(WPos origin, Angle rotation, float offset = 0) { var dx = (rotation + DirectionOffset).ToDirection(); @@ -109,29 +132,43 @@ private IEnumerable ContourPoints(WPos origin, Angle rotation, float offse yield return origin + dx2 + dy2; yield return origin + dx1 + dy2; } + + public override Func Distance(WPos origin, Angle rotation) + { + return !InvertForbiddenZone + ? ShapeDistance.Cross(origin, rotation + DirectionOffset, Length, HalfWidth) + : ShapeDistance.InvertedCross(origin, rotation + DirectionOffset, Length, HalfWidth); + } } // note: it's very rare, not sure it needs to be a common utility - it's an isosceles triangle, a cone with flat base -public sealed record class AOEShapeTriCone(float SideLength, Angle HalfAngle, Angle DirectionOffset = default) : AOEShape +public sealed record class AOEShapeTriCone(float SideLength, Angle HalfAngle, Angle DirectionOffset = default, bool InvertForbiddenZone = false) : AOEShape { - public override string ToString() => $"TriCone: side={SideLength:f3}, angle={HalfAngle * 2}, off={DirectionOffset}"; - public override bool Check(WPos position, WPos origin, Angle rotation) => position.InTri(origin, origin + SideLength * (rotation + HalfAngle).ToDirection(), origin + SideLength * (rotation - HalfAngle).ToDirection()); - public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) => arena.ZoneTri(origin, origin + SideLength * (rotation + HalfAngle).ToDirection(), origin + SideLength * (rotation - HalfAngle).ToDirection(), color); - public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.Danger) => arena.AddTriangle(origin, origin + SideLength * (rotation + HalfAngle).ToDirection(), origin + SideLength * (rotation - HalfAngle).ToDirection(), color); - public override Func Distance(WPos origin, Angle rotation) => ShapeDistance.Tri(origin, new(default, SideLength * (rotation + HalfAngle).ToDirection(), SideLength * (rotation - HalfAngle).ToDirection())); + public override string ToString() => $"TriCone: side={SideLength:f3}, angle={HalfAngle * 2}, off={DirectionOffset}, ivz={InvertForbiddenZone}"; + public override bool Check(WPos position, WPos origin, Angle rotation) => position.InTri(origin, origin + SideLength * (rotation + DirectionOffset + HalfAngle).ToDirection(), origin + SideLength * (rotation + DirectionOffset - HalfAngle).ToDirection()); + public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) => arena.ZoneTri(origin, origin + SideLength * (rotation + DirectionOffset + HalfAngle).ToDirection(), origin + SideLength * (rotation + DirectionOffset - HalfAngle).ToDirection(), color); + public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.Danger) => arena.AddTriangle(origin, origin + SideLength * (rotation + DirectionOffset + HalfAngle).ToDirection(), origin + SideLength * (rotation + DirectionOffset - HalfAngle).ToDirection(), color); + + public override Func Distance(WPos origin, Angle rotation) + { + var direction1 = SideLength * (rotation + DirectionOffset + HalfAngle).ToDirection(); + var direction2 = SideLength * (rotation + DirectionOffset - HalfAngle).ToDirection(); + var shape = new RelTriangle(default, direction1, direction2); + return !InvertForbiddenZone ? ShapeDistance.Tri(origin, shape) : ShapeDistance.InvertedTri(origin, shape); + } } public sealed record class AOEShapeCustom(IEnumerable UnionShapes, IEnumerable? DifferenceShapes = null, bool InvertForbiddenZone = false) : AOEShape { - private static readonly Dictionary _polygonCacheStatic = []; - private readonly Dictionary _polygonCache = []; - private static readonly Dictionary<(string, WPos, Angle), Func> _distanceFuncCache = []; + private static readonly Dictionary<(string, bool), RelSimplifiedComplexPolygon> _polygonCache = []; + private readonly Dictionary<(string, WPos, WPos, Angle, bool), bool> _checkCache = []; + private static readonly Dictionary<(string, WPos, Angle, bool), Func> _distanceFuncCache = []; private RelSimplifiedComplexPolygon GetCombinedPolygon(WPos origin) { - var cacheKey = CreateCacheKey(UnionShapes, DifferenceShapes ?? []); - if (_polygonCacheStatic.TryGetValue(cacheKey, out var cachedResult)) - return (RelSimplifiedComplexPolygon)cachedResult; + var cacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes ?? []), InvertForbiddenZone); + if (_polygonCache.TryGetValue(cacheKey, out var cachedResult)) + return cachedResult; var unionOperands = new PolygonClipper.Operand(); foreach (var shape in UnionShapes) @@ -144,19 +181,19 @@ private RelSimplifiedComplexPolygon GetCombinedPolygon(WPos origin) var clipper = new PolygonClipper(); var finalResult = clipper.Difference(unionOperands, differenceOperands); - _polygonCacheStatic[cacheKey] = finalResult; + _polygonCache[cacheKey] = finalResult; return finalResult; } public override bool Check(WPos position, WPos origin, Angle rotation) { - var cacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes ?? []), position, origin, rotation); - if (_polygonCache.TryGetValue(cacheKey, out var cachedResult)) - return (bool)cachedResult; + var cacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes ?? []), position, origin, rotation, InvertForbiddenZone); + if (_checkCache.TryGetValue(cacheKey, out var cachedResult)) + return cachedResult; var combinedPolygon = GetCombinedPolygon(origin); var relativePosition = position - origin; var result = combinedPolygon.Contains(new WDir(relativePosition.X, relativePosition.Z)); - _polygonCache[cacheKey] = result; + _checkCache[cacheKey] = result; return result; } @@ -175,18 +212,25 @@ public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint var combinedPolygon = GetCombinedPolygon(origin); foreach (var part in combinedPolygon.Parts) { - foreach (var (start, end) in part.ExteriorEdges) + var exteriorEdges = part.ExteriorEdges.ToList(); + for (var i = 0; i < exteriorEdges.Count; i++) { + var (start, end) = exteriorEdges[i]; arena.PathLineTo(origin + start); - arena.PathLineTo(origin + end); + if (i != exteriorEdges.Count - 1) + arena.PathLineTo(origin + end); } MiniArena.PathStroke(true, color); + foreach (var holeIndex in part.Holes) { - foreach (var (start, end) in part.InteriorEdges(holeIndex)) + var interiorEdges = part.InteriorEdges(holeIndex).ToList(); + for (var i = 0; i < interiorEdges.Count; i++) { + var (start, end) = interiorEdges[i]; arena.PathLineTo(origin + start); - arena.PathLineTo(origin + end); + if (i != interiorEdges.Count - 1) + arena.PathLineTo(origin + end); } MiniArena.PathStroke(true, color); } @@ -196,7 +240,7 @@ public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint public override Func Distance(WPos origin, Angle rotation) { // TODO: Distance maps should probably be cloned instead of being saved in a dictionary - var cacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes ?? []), origin, rotation); + var cacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes ?? []), origin, rotation, InvertForbiddenZone); if (_distanceFuncCache.TryGetValue(cacheKey, out var cachedFunc)) return cachedFunc; var unionDistanceFuncs = UnionShapes.Select(shape => shape.Distance()).ToList(); diff --git a/BossMod/BossModule/BossModule.cs b/BossMod/BossModule/BossModule.cs index 5f1b52e567..560ece8cc7 100644 --- a/BossMod/BossModule/BossModule.cs +++ b/BossMod/BossModule/BossModule.cs @@ -54,7 +54,7 @@ public void ActivateComponent() where T : BossComponent // execute callbacks for existing state foreach (var actor in WorldState.Actors) { - bool nonPlayer = actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo; + bool nonPlayer = actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo and not ActorType.Buddy; if (nonPlayer) { comp.OnActorCreated(actor); @@ -357,7 +357,7 @@ private void DrawPartyMembers(int pcSlot, Actor pc) private void OnActorCreated(Actor actor) { _relevantEnemies.GetValueOrDefault(actor.OID)?.Add(actor); - if (actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo) + if (actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo and not ActorType.Buddy) foreach (var comp in _components) comp.OnActorCreated(actor); } @@ -365,21 +365,21 @@ private void OnActorCreated(Actor actor) private void OnActorDestroyed(Actor actor) { _relevantEnemies.GetValueOrDefault(actor.OID)?.Remove(actor); - if (actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo) + if (actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo and not ActorType.Buddy) foreach (var comp in _components) comp.OnActorDestroyed(actor); } private void OnActorCastStarted(Actor actor) { - if ((actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo) && (actor.CastInfo?.IsSpell() ?? false)) + if (actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo and not ActorType.Buddy && (actor.CastInfo?.IsSpell() ?? false)) foreach (var comp in _components) comp.OnCastStarted(actor, actor.CastInfo); } private void OnActorCastFinished(Actor actor) { - if ((actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo) && (actor.CastInfo?.IsSpell() ?? false)) + if (actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo and not ActorType.Buddy && (actor.CastInfo?.IsSpell() ?? false)) foreach (var comp in _components) comp.OnCastFinished(actor, actor.CastInfo); } @@ -416,7 +416,7 @@ private void OnActorIcon(Actor actor, uint iconID) private void OnActorCastEvent(Actor actor, ActorCastEvent cast) { - if ((actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo) && cast.IsSpell()) + if (actor.Type is not ActorType.Player and not ActorType.Pet and not ActorType.Chocobo and not ActorType.Buddy && cast.IsSpell()) foreach (var comp in _components) comp.OnEventCast(actor, cast); } diff --git a/BossMod/BossModule/Shapes.cs b/BossMod/BossModule/Shapes.cs index 53ddba6dd8..f593b42517 100644 --- a/BossMod/BossModule/Shapes.cs +++ b/BossMod/BossModule/Shapes.cs @@ -53,16 +53,67 @@ public override Func Distance() public override string ComputeHash() => ComputeSHA512($"{nameof(Circle)}:{Center.X},{Center.Z},{Radius}"); } -// for custom polygons defined by a list of vertices +// for custom polygons, automatically checking if convex or concave public record class PolygonCustom(IEnumerable Vertices) : Shape { + private static readonly Dictionary propertyCache = []; + public override List Contour(WPos center) => GetOrCreateContour(center, () => Vertices.Select(v => v - center).ToList()); public override RelSimplifiedComplexPolygon ToPolygon(WPos center) => GetOrCreatePolygon(center, () => new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(Contour(center))])); - public override Func Distance() => ShapeDistance.ConcavePolygon(Vertices); + private bool IsConvex() + { + var hash = ComputeHash() + "IsConvex"; + if (propertyCache.TryGetValue(hash, out var isConvex)) + return isConvex; + + var vertices = Vertices.ToList(); + var n = vertices.Count; + isConvex = true; + for (var i = 0; i < n; i++) + { + var p0 = vertices[i]; + var p1 = vertices[(i + 1) % n]; + var p2 = vertices[(i + 2) % n]; + + var crossProduct = (p1.X - p0.X) * (p2.Z - p1.Z) - (p1.Z - p0.Z) * (p2.X - p1.X); + if (i == 0) + isConvex = crossProduct > 0; + else + if ((crossProduct > 0) != isConvex) + return propertyCache[hash] = false; + } + propertyCache[hash] = isConvex; + return isConvex; + } + + private bool IsCounterClockwise() + { + var hash = ComputeHash() + "IsCounterClockwise"; + if (propertyCache.TryGetValue(hash, out var isCounterClockwise)) + return isCounterClockwise; + + var vertices = Vertices.ToList(); + float area = 0; + for (var i = 0; i < vertices.Count; i++) + { + var p0 = vertices[i]; + var p1 = vertices[(i + 1) % vertices.Count]; + area += (p1.X - p0.X) * (p1.Z + p0.Z); + } + isCounterClockwise = area > 0; + propertyCache[hash] = isCounterClockwise; + return isCounterClockwise; + } + + public override Func Distance() + { + return IsConvex() ? IsCounterClockwise() ? ShapeDistance.ConvexPolygon(Vertices, false) : ShapeDistance.ConvexPolygon(Vertices, true) + : ShapeDistance.ConcavePolygon(Vertices); + } public override string ComputeHash() { @@ -185,7 +236,25 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) => GetOrCreatePolygon(center, () => new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(Contour(center))])); public override Func Distance() - => ShapeDistance.Tri(Center, new RelTriangle(new WDir(-Radius, 0), new WDir(Radius, 0), new WDir(0, -Radius))); + { + var sqrt3 = MathF.Sqrt(3); + var halfSide = Radius; + var height = halfSide * sqrt3; + var a = new WDir(-halfSide, height / 3); + var b = new WDir(halfSide, height / 3); + var c = new WDir(0, -2 * height / 3); + + var cos = MathF.Cos(Rotation.Rad); + var sin = MathF.Sin(Rotation.Rad); + + var rotatedA = new WDir(a.X * cos - a.Z * sin, a.X * sin + a.Z * cos); + var rotatedB = new WDir(b.X * cos - b.Z * sin, b.X * sin + b.Z * cos); + var rotatedC = new WDir(c.X * cos - c.Z * sin, c.X * sin + c.Z * cos); + + var relTriangle = new RelTriangle(rotatedA, rotatedB, rotatedC); + + return ShapeDistance.Tri(Center, relTriangle); + } public override string ComputeHash() => ComputeSHA512($"{nameof(TriangleE)}:{Center.X},{Center.Z},{Radius},{Rotation.Rad}"); } @@ -223,7 +292,28 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) => GetOrCreatePolygon(center, () => new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(Contour(center))])); public override Func Distance() - => ShapeDistance.Tri(Center, new RelTriangle(new WDir(-SideA / 2, 0), new WDir(SideA / 2, 0), new WDir(0, -SideB))); + { + var sides = new[] { SideA, SideB, SideC }.OrderByDescending(s => s).ToArray(); + var a = sides[0]; + var b = sides[1]; + var c = sides[2]; + var vertex1 = new WDir(0, 0); + var vertex2 = new WDir(a, 0); + var cosC = (b * b + a * a - c * c) / (2 * a * b); + var sinC = MathF.Sqrt(1 - cosC * cosC); + var vertex3 = new WDir(b * cosC, b * sinC); + + var cos = MathF.Cos(Rotation.Rad); + var sin = MathF.Sin(Rotation.Rad); + + var rotatedVertex1 = new WDir(vertex1.X * cos - vertex1.Z * sin, vertex1.X * sin + vertex1.Z * cos); + var rotatedVertex2 = new WDir(vertex2.X * cos - vertex2.Z * sin, vertex2.X * sin + vertex2.Z * cos); + var rotatedVertex3 = new WDir(vertex3.X * cos - vertex3.Z * sin, vertex3.X * sin + vertex3.Z * cos); + + var relTriangle = new RelTriangle(rotatedVertex1, rotatedVertex2, rotatedVertex3); + + return ShapeDistance.Tri(Center, relTriangle); + } public override string ComputeHash() => ComputeSHA512($"{nameof(TriangleS)}:{Center.X},{Center.Z},{SideA},{SideB},{SideC},{Rotation.Rad}"); } @@ -258,7 +348,25 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) => GetOrCreatePolygon(center, () => new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(Contour(center))])); public override Func Distance() - => ShapeDistance.Tri(Center, new RelTriangle(new WDir(-BaseLength / 2, 0), new WDir(BaseLength / 2, 0), new WDir(0, -BaseLength / 2 / MathF.Tan(ApexAngle.Rad / 2)))); + { + var apexAngleRad = ApexAngle.Rad; + var height = BaseLength / 2 / MathF.Tan(apexAngleRad / 2); + var halfBase = BaseLength / 2; + var vertex1 = new WDir(-halfBase, 0); + var vertex2 = new WDir(halfBase, 0); + var vertex3 = new WDir(0, -height); + + var cos = MathF.Cos(Rotation.Rad); + var sin = MathF.Sin(Rotation.Rad); + + var rotatedVertex1 = new WDir(vertex1.X * cos - vertex1.Z * sin, vertex1.X * sin + vertex1.Z * cos); + var rotatedVertex2 = new WDir(vertex2.X * cos - vertex2.Z * sin, vertex2.X * sin + vertex2.Z * cos); + var rotatedVertex3 = new WDir(vertex3.X * cos - vertex3.Z * sin, vertex3.X * sin + vertex3.Z * cos); + + var relTriangle = new RelTriangle(rotatedVertex1, rotatedVertex2, rotatedVertex3); + + return ShapeDistance.Tri(Center, relTriangle); + } public override string ComputeHash() => ComputeSHA512($"{nameof(TriangleA)}:{Center.X},{Center.Z},{BaseLength},{ApexAngle.Rad},{Rotation.Rad}"); } diff --git a/BossMod/Components/BaitAway.cs b/BossMod/Components/BaitAway.cs index 65f13da2b9..f2be226591 100644 --- a/BossMod/Components/BaitAway.cs +++ b/BossMod/Components/BaitAway.cs @@ -109,9 +109,8 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) public override void OnTethered(Actor source, ActorTetherInfo tether) { var (player, enemy) = DetermineTetherSides(source, tether); - if (player != null && enemy != null) - if (enemyOID == default || _enemies.Contains(source)) - CurrentBaits.Add(new(enemy, player, Shape, WorldState.FutureTime(ActivationDelay))); + if (player != null && enemy != null && (enemyOID == default || _enemies.Contains(source))) + CurrentBaits.Add(new(enemy, player, Shape, WorldState.FutureTime(ActivationDelay))); } public override void OnUntethered(Actor source, ActorTetherInfo tether) @@ -124,7 +123,7 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) } // we support both player->enemy and enemy->player tethers - private (Actor? player, Actor? enemy) DetermineTetherSides(Actor source, ActorTetherInfo tether) + public (Actor? player, Actor? enemy) DetermineTetherSides(Actor source, ActorTetherInfo tether) { if (tether.ID != TID) return (null, null); @@ -133,8 +132,8 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) if (target == null) return (null, null); - var (player, enemy) = source.Type == ActorType.Player ? (source, target) : (target, source); - if (player.Type != ActorType.Player || enemy.Type == ActorType.Player) + var (player, enemy) = source.Type is ActorType.Player or ActorType.Buddy ? (source, target) : (target, source); + if (player.Type is not ActorType.Player and not ActorType.Buddy || enemy.Type is ActorType.Player or ActorType.Buddy) { ReportError($"Unexpected tether pair: {source.InstanceID:X} -> {target.InstanceID:X}"); return (null, null); diff --git a/BossMod/Components/ChasingAOEs.cs b/BossMod/Components/ChasingAOEs.cs index 79735d2e7f..43533df3c6 100644 --- a/BossMod/Components/ChasingAOEs.cs +++ b/BossMod/Components/ChasingAOEs.cs @@ -55,6 +55,17 @@ public bool Advance(WPos pos, float moveDistance, DateTime currentTime, bool rem } return true; } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + // TODO: for some reason the AI only dodges the first hit correctly and then fails the rest (looks like running against a wall) + // this is a hack that tries to counter the problem + // 11 is the biggest radius of known chasing AOE (from Zeromus ex) + 1 + if (Chasers.Count > 0) + foreach (var c in Chasers.Where(x => x.Target == actor)) + hints.AddForbiddenZone(ShapeDistance.Circle(c.PredictedPosition(), 11)); + } } // standard chasing aoe; first cast is long - assume it is baited on the nearest allowed target; successive casts are instant diff --git a/BossMod/Components/Knockback.cs b/BossMod/Components/Knockback.cs index c58ea920bc..8a98ff4592 100644 --- a/BossMod/Components/Knockback.cs +++ b/BossMod/Components/Knockback.cs @@ -9,7 +9,8 @@ public enum Kind { None, AwayFromOrigin, // standard knockback - specific distance along ray from origin to target - TowardsOrigin, // standard pull - "knockback" to source - forward along source's direction + 180 degrees + TowardsOrigin, // standard pull - "knockback" to source - specific distance along ray from origin to target + 180 degrees + DirBackward, // standard pull - "knockback" to source - forward along source's direction + 180 degrees DirForward, // directional knockback - forward along source's direction DirLeft, // directional knockback - forward along source's direction + 90 degrees DirRight, // directional knockback - forward along source's direction - 90 degrees @@ -157,6 +158,7 @@ public override void OnStatusLose(Actor actor, ActorStatus status) { Kind.AwayFromOrigin => from != s.Origin ? (from - s.Origin).Normalized() : default, Kind.TowardsOrigin => from != s.Origin ? (s.Origin - from).Normalized() : default, + Kind.DirBackward => (s.Direction + 180.Degrees()).ToDirection(), Kind.DirForward => s.Direction.ToDirection(), Kind.DirLeft => s.Direction.ToDirection().OrthoL(), Kind.DirRight => s.Direction.ToDirection().OrthoR(), @@ -166,8 +168,15 @@ public override void OnStatusLose(Actor actor, ActorStatus status) continue; // couldn't determine direction for some reason var distance = s.Distance; - if (s.Kind is Kind.TowardsOrigin) + if (s.Kind == Kind.TowardsOrigin) distance = Math.Min(s.Distance, (s.Origin - from).Length() - s.MinDistance); + if (s.Kind == Kind.DirBackward) + { + var perpendicularDir = s.Direction.ToDirection().OrthoL(); + var perpendicularDistance = Math.Abs((from - s.Origin).Cross(perpendicularDir) / perpendicularDir.Length()); + distance = Math.Min(s.Distance, perpendicularDistance); + } + if (distance <= 0) continue; // this could happen if attract starts from < min distance diff --git a/BossMod/Components/StackSpread.cs b/BossMod/Components/StackSpread.cs index 3cec960b1c..5cae89fe2b 100644 --- a/BossMod/Components/StackSpread.cs +++ b/BossMod/Components/StackSpread.cs @@ -1,4 +1,6 @@ -namespace BossMod.Components; +using BossMod.BLM; + +namespace BossMod.Components; // generic 'stack/spread' mechanic has some players that have to spread away from raid, some other players that other players need to stack with // there are various variants (e.g. everyone should spread, or everyone should stack in one or more groups, or some combination of that) @@ -25,6 +27,7 @@ public record struct Spread( public bool IncludeDeadTargets = includeDeadTargets; // if false, stacks & spreads with dead targets are ignored public List Stacks = []; public List Spreads = []; + public const string StackHint = "Stack!"; public bool Active => Stacks.Count + Spreads.Count > 0; public IEnumerable ActiveStacks => IncludeDeadTargets ? Stacks : Stacks.Where(s => !s.Target.IsDead); @@ -49,7 +52,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) ++numStacked; stackedWithOtherStackOrAvoid |= stack.ForbiddenPlayers[j] || IsStackTarget(other); } - hints.Add("Stack!", stackedWithOtherStackOrAvoid || numStacked < stack.MinSize || numStacked > stack.MaxSize); + hints.Add(StackHint, stackedWithOtherStackOrAvoid || numStacked < stack.MinSize || numStacked > stack.MaxSize); } else { @@ -64,11 +67,11 @@ public override void AddHints(int slot, Actor actor, TextHints hints) } if (numParticipatingStacks > 1) - hints.Add("Stack!"); + hints.Add(StackHint); else if (numParticipatingStacks == 1) - hints.Add("Stack!", false); + hints.Add(StackHint, false); else if (numUnsatisfiedStacks > 0) - hints.Add("Stack!"); + hints.Add(StackHint); // else: don't show anything, all potential stacks are already satisfied without a player //hints.Add("Stack!", ActiveStacks.Count(s => !s.ForbiddenPlayers[slot] && actor.Position.InCircle(s.Target.Position, s.Radius)) != 1); } @@ -96,12 +99,13 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (IsStackTarget(actor)) { - // forbid standing next to other stack markers + // forbid standing next to other stack markers or overlapping them foreach (var stackWith in ActiveStacks.Where(s => s.Target != actor)) - hints.AddForbiddenZone(ShapeDistance.Circle(stackWith.Target.Position, stackWith.Radius), stackWith.Activation); - if (Raid.PartyContainsBuddies) // if player got stackmarker and is playing with NPCs, go to a NPC to stack with them + hints.AddForbiddenZone(ShapeDistance.Circle(stackWith.Target.Position, stackWith.Radius * 2), stackWith.Activation); + // if player got stackmarker and is playing with NPCs, go to a NPC to stack with them since they will likely not come to you + if (Raid.WithoutSlot().Any(x => x.Type == ActorType.Buddy)) foreach (var stackWith in ActiveStacks.Where(s => s.Target == actor)) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Raid.WithoutSlot().FirstOrDefault(x => !IsStackTarget(x))!.Position, stackWith.Radius - 1), stackWith.Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Raid.WithoutSlot().FirstOrDefault(x => !x.IsDead && !IsStackTarget(x))!.Position, 1), stackWith.Activation); } else if (!IsSpreadTarget(actor)) { @@ -273,3 +277,97 @@ public class SpreadFromIcon(BossModule module, uint icon, ActionID aid, float ra // generic 'stack with actors with specific icon' mechanic public class StackWithIcon(BossModule module, uint icon, ActionID aid, float radius, float activationDelay, int minStackSize = 2, int maxStackSize = int.MaxValue) : IconStackSpread(module, icon, 0, aid, default, radius, 0, activationDelay, minStackSize, maxStackSize); + +// generic single hit "line stack" component, usually do not have an iconID, instead players get marked by cast event +// usually these have 50 range and 4 halfWidth, but it can be modified +public class LineStack(BossModule module, ActionID aidMarker, ActionID aidResolve, float activationDelay, float range = 50, float halfWidth = 4, int minStackSize = 4, int maxStackSize = int.MaxValue) : GenericBaitAway(module) +{ + // TODO: add forbidden slots logic? + // TODO: add logic for min and max stack size + public readonly ActionID AidMarker = aidMarker; + public readonly ActionID AidResolve = aidResolve; + public readonly float ActionDelay = activationDelay; + public readonly float Range = range; + public readonly float HalfWidth = halfWidth; + public readonly int MaxStackSize = maxStackSize; + public readonly int MinStackSize = minStackSize; + public const string HintStack = "Stack!"; + public const string HintAvoidOther = "GTFO from other line stacks!"; + public const string HintAvoid = "GTFO from line stacks!"; + public readonly List ForbiddenActors = []; + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action == AidMarker) + CurrentBaits.Add(new(caster, WorldState.Actors.Find(spell.MainTargetID)!, new AOEShapeRect(Range, HalfWidth), WorldState.FutureTime(ActionDelay))); + if (spell.Action == AidResolve && CurrentBaits.Count > 0) + CurrentBaits.RemoveAt(0); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (!ActiveBaits.Any()) + return; + var isBaitTarget = ActiveBaits.Any(x => x.Target == actor); + var isBaitNotTarget = ActiveBaits.Any(x => x.Target != actor); + var forbiddenInverted = new List>(); + var forbidden = new List>(); + var forbiddenActors = ForbiddenActors.Contains(actor); + if (isBaitNotTarget && !isBaitTarget && !forbiddenActors) + foreach (var b in ActiveBaits.Where(x => x.Target != actor)) + forbiddenInverted.Add(ShapeDistance.InvertedRect(b.Source.Position, (b.Target.Position - b.Source.Position).Normalized(), Range, 0, HalfWidth)); + // prevent overlapping if there are multiple line stacks + if (isBaitNotTarget && isBaitTarget) + foreach (var b in ActiveBaits.Where(x => x.Target != actor)) + forbidden.Add(ShapeDistance.Rect(b.Source.Position, (b.Target.Position - b.Source.Position).Normalized(), Range, 0, 2 * HalfWidth)); + if (forbiddenActors) // if too many people are dead, you can become a forbidden actor and get stack at the same time + foreach (var b in ActiveBaits.Where(x => x.Target != actor)) + forbidden.Add(ShapeDistance.Rect(b.Source.Position, (b.Target.Position - b.Source.Position).Normalized(), Range, 0, HalfWidth)); + if (forbiddenInverted.Count > 0) + hints.AddForbiddenZone(p => forbiddenInverted.Select(f => f(p)).Max(), ActiveBaits.FirstOrDefault().Activation); + if (forbidden.Count > 0) + hints.AddForbiddenZone(p => forbidden.Select(f => f(p)).Min(), ActiveBaits.FirstOrDefault().Activation); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!ActiveBaits.Any()) + return; + + var isBaitTarget = ActiveBaits.Any(x => x.Target == actor); + var isBaitNotTarget = ActiveBaits.Any(x => x.Target != actor); + var isInBaitShape = ActiveBaits.Any(x => actor.Position.InRect(x.Source.Position, (x.Target.Position - x.Source.Position).Normalized(), Range, 0, HalfWidth)); + + if (isBaitNotTarget && !isBaitTarget && !isInBaitShape) + hints.Add(HintStack); + else if ((isBaitNotTarget || isBaitTarget) && isInBaitShape) + hints.Add(HintStack, false); + + if (ActiveBaits.Count() > 1 && isBaitTarget) + { + var isInOtherBaitShape = ActiveBaits.Any(x => actor.Position.InRect(x.Source.Position, (x.Target.Position - x.Source.Position).Normalized(), Range, 0, 2 * HalfWidth)); + if (isInOtherBaitShape) + hints.Add(HintAvoidOther); + } + + if (ForbiddenActors.Contains(actor) && isInBaitShape) + hints.Add(HintAvoid); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (!ActiveBaits.Any()) + return; + + var isBaitTarget = ActiveBaits.Any(x => x.Target == pc); + var isBaitNotTarget = ActiveBaits.Any(x => x.Target != pc); + + foreach (var bait in ActiveBaits) + { + var color = isBaitTarget && bait.Target == pc || !isBaitTarget && bait.Target != pc ? ArenaColor.SafeFromAOE : ArenaColor.AOE; + bait.Shape.Draw(Arena, BaitOrigin(bait), bait.Rotation, color); + } + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) { } +} \ No newline at end of file diff --git a/BossMod/Components/Tethers.cs b/BossMod/Components/Tethers.cs index e0a8017cea..5824462eb2 100644 --- a/BossMod/Components/Tethers.cs +++ b/BossMod/Components/Tethers.cs @@ -108,8 +108,8 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) if (target == null) return null; - var (player, enemy) = source.Type == ActorType.Player ? (source, target) : (target, source); - if (player.Type != ActorType.Player || enemy.Type == ActorType.Player) + var (player, enemy) = source.Type is ActorType.Player or ActorType.Buddy ? (source, target) : (target, source); + if (player.Type is not ActorType.Player and not ActorType.Buddy || enemy.Type is ActorType.Player or ActorType.Buddy) { ReportError($"Unexpected tether pair: {source.InstanceID:X} -> {target.InstanceID:X}"); return null; @@ -132,7 +132,7 @@ public class InterceptTether(BossModule module, ActionID aid, uint tetherID) : C public uint TID { get; init; } = tetherID; private readonly List<(Actor Player, Actor Enemy)> _tethers = []; private BitMask _tetheredPlayers; - + private const string hint = "Grab the tether!"; public bool Active => _tethers.Count != 0; public override void AddHints(int slot, Actor actor, TextHints hints) @@ -141,18 +141,17 @@ public override void AddHints(int slot, Actor actor, TextHints hints) return; if (!_tetheredPlayers[slot]) { - hints.Add("Grab the tether!"); + hints.Add(hint); } } public override void DrawArenaForeground(int pcSlot, Actor pc) { - // show tethered targets with circles foreach (var side in _tethers) { if (Arena.Config.ShowOutlinesAndShadows) Arena.AddLine(side.Enemy.Position, side.Player.Position, 0xFF000000, 2); - Arena.AddLine(side.Enemy.Position, side.Player.Position, side.Player.Type == ActorType.Player ? ArenaColor.Safe : ArenaColor.Danger); + Arena.AddLine(side.Enemy.Position, side.Player.Position, side.Player.Type is ActorType.Player or ActorType.Buddy ? ArenaColor.Safe : ArenaColor.Danger); } } @@ -186,8 +185,205 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) if (target == null) return null; - var (player, enemy) = source.Type == ActorType.Player ? (source, target) : (target, source); + var (player, enemy) = source.Type is ActorType.Player or ActorType.Buddy ? (source, target) : (target, source); var playerSlot = Raid.FindSlot(player.InstanceID); return (playerSlot, player, enemy); } } + +// generic component for tethers that need to be stretched and switch between a "good" and "bad" tether +// at the end of the mechanic various things are possible, eg. single target dmg, knockback/pull, AOE etc. +public class StretchTetherDuo(BossModule module, uint tetherIDBad, uint tetherIDGood, float minimumDistance, AOEShape? shape = null, ActionID aid = default, uint enemyOID = default, float activationDelay = default, bool knockbackImmunity = false) : GenericBaitAway(module, aid) +{ + public static readonly AOEShapeCone DefaultShape = new(default, default); + public AOEShape Shape = shape ?? DefaultShape; + public uint TIDGood = tetherIDGood; + public uint TIDBad = tetherIDBad; + public float MinimumDistance = minimumDistance; + public bool KnockbackImmunity { get; init; } = knockbackImmunity; + public readonly IReadOnlyList _enemies = module.Enemies(enemyOID); + public readonly List<(Actor, uint)> TetherOnActor = []; + public readonly List<(Actor, DateTime)> ActivationDelayOnActor = []; + public float ActivationDelay = activationDelay; + public const string HintGood = "Tether is stretched!"; + public const string HintBad = "Stretch tether further!"; + public const string HintKnockbackImmmunityGood = "Immune against tether mechanic!"; + public const string HintKnockbackImmmunityBad = "Tether can be ignored with knockback immunity!"; + + protected struct PlayerImmuneState + { + public DateTime RoleBuffExpire; // 0 if not active + public DateTime JobBuffExpire; // 0 if not active + public DateTime DutyBuffExpire; // 0 if not active + + public readonly bool ImmuneAt(DateTime time) => RoleBuffExpire > time || JobBuffExpire > time || DutyBuffExpire > time; + } + + protected PlayerImmuneState[] PlayerImmunes = new PlayerImmuneState[PartyState.MaxAllianceSize]; + + public bool IsImmune(int slot, DateTime time) => KnockbackImmunity && PlayerImmunes[slot].ImmuneAt(time); + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + switch (status.ID) + { + case 3054: //Guard in PVP + case (uint)WHM.SID.Surecast: + case (uint)WAR.SID.ArmsLength: + var slot1 = Raid.FindSlot(actor.InstanceID); + if (slot1 >= 0) + PlayerImmunes[slot1].RoleBuffExpire = status.ExpireAt; + break; + case 1722: //Bluemage Diamondback + case (uint)WAR.SID.InnerStrength: + var slot2 = Raid.FindSlot(actor.InstanceID); + if (slot2 >= 0) + PlayerImmunes[slot2].JobBuffExpire = status.ExpireAt; + break; + case 2345: //Lost Manawall in Bozja + var slot3 = Raid.FindSlot(actor.InstanceID); + if (slot3 >= 0) + PlayerImmunes[slot3].DutyBuffExpire = status.ExpireAt; + break; + } + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + switch (status.ID) + { + case 3054: //Guard in PVP + case (uint)WHM.SID.Surecast: + case (uint)WAR.SID.ArmsLength: + var slot1 = Raid.FindSlot(actor.InstanceID); + if (slot1 >= 0) + PlayerImmunes[slot1].RoleBuffExpire = new(); + break; + case 1722: //Bluemage Diamondback + case (uint)WAR.SID.InnerStrength: + var slot2 = Raid.FindSlot(actor.InstanceID); + if (slot2 >= 0) + PlayerImmunes[slot2].JobBuffExpire = new(); + break; + case 2345: //Lost Manawall in Bozja + var slot3 = Raid.FindSlot(actor.InstanceID); + if (slot3 >= 0) + PlayerImmunes[slot3].DutyBuffExpire = new(); + break; + } + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + base.DrawArenaForeground(pcSlot, pc); + if (!ActiveBaits.Any()) + return; + if (!IsImmune(pcSlot, ActiveBaits.FirstOrDefault(x => x.Target == pc).Activation)) + { + if (IsTether(pc, TIDBad)) + DrawTetherLines(pc, ArenaColor.Danger); + else if (IsTether(pc, TIDGood)) + DrawTetherLines(pc, ArenaColor.Safe); + } + } + + private bool IsTether(Actor actor, uint tetherID) => TetherOnActor.Contains((actor, tetherID)); + + private void DrawTetherLines(Actor target, uint color) + { + foreach (var bait in ActiveBaits.Where(x => x.Target == target)) + { + if (Arena.Config.ShowOutlinesAndShadows) + Arena.AddLine(bait.Source.Position, bait.Target.Position, 0xFF000000, 2); + Arena.AddLine(bait.Source.Position, bait.Target.Position, color); + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + var (player, enemy) = DetermineTetherSides(source, tether); + if (player != null && enemy != null && (enemyOID == default || _enemies.Contains(source))) + { + if (!ActivationDelayOnActor.Any(x => x.Item1 == player)) + ActivationDelayOnActor.Add((player, WorldState.FutureTime(ActivationDelay))); + CurrentBaits.Add(new(enemy, player, Shape, ActivationDelayOnActor.FirstOrDefault(x => x.Item1 == player).Item2)); + TetherOnActor.Add((player, tether.ID)); + } + } + + public override void Update() + { + if (ActivationDelayOnActor.Count > 0) + { + var actorsToRemove = new List<(Actor, DateTime)>(); + foreach (var a in ActivationDelayOnActor) + if (a.Item2.AddSeconds(1) <= WorldState.CurrentTime) + actorsToRemove.Add(a); + foreach (var a in actorsToRemove) + ActivationDelayOnActor.Remove(a); + } + } + + public override void OnUntethered(Actor source, ActorTetherInfo tether) + { + var (player, enemy) = DetermineTetherSides(source, tether); + if (player != null && enemy != null) + { + CurrentBaits.RemoveAll(b => b.Source == enemy && b.Target == player); + TetherOnActor.Remove((WorldState.Actors.Find(tether.Target)!, tether.ID)); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!ActiveBaits.Any()) + return; + var immunity = IsImmune(slot, ActiveBaits.FirstOrDefault(x => x.Target == actor).Activation); + var bait = ActiveBaits.Any(x => x.Target == actor); + if (immunity && bait) + hints.Add(HintKnockbackImmmunityGood, false); + else if (TetherOnActor.Contains((actor, TIDBad))) + hints.Add(HintBad); + else if (TetherOnActor.Contains((actor, TIDGood))) + hints.Add(HintGood, false); + if (KnockbackImmunity && bait && !immunity) + hints.Add(HintKnockbackImmmunityBad); + } + + // we support both player->enemy and enemy->player tethers + private (Actor? player, Actor? enemy) DetermineTetherSides(Actor source, ActorTetherInfo tether) + { + if (tether.ID != TIDGood && tether.ID != TIDBad) + return (null, null); + + var target = WorldState.Actors.Find(tether.Target); + if (target == null) + return (null, null); + + var (player, enemy) = source.Type is ActorType.Player or ActorType.Buddy ? (source, target) : (target, source); + if (player.Type is not ActorType.Player and not ActorType.Buddy || enemy.Type is ActorType.Player or ActorType.Buddy) + { + ReportError($"Unexpected tether pair: {source.InstanceID:X} -> {target.InstanceID:X}"); + return (null, null); + } + + return (player, enemy); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (!ActiveBaits.Any()) + return; + var immunity = IsImmune(slot, ActiveBaits.FirstOrDefault(x => x.Target == actor).Activation); + if (KnockbackImmunity && !immunity && ActivationDelayOnActor.Any(x => x.Item1 == actor && x.Item2.AddSeconds(-6) <= WorldState.CurrentTime)) + { + hints.PlannedActions.Add((ActionID.MakeSpell(WAR.AID.ArmsLength), actor, 1, false)); + hints.PlannedActions.Add((ActionID.MakeSpell(WHM.AID.Surecast), actor, 1, false)); + } + if (Shape != DefaultShape) + base.AddAIHints(slot, actor, assignment, hints); + if (ActiveBaits.Any(x => x.Target == actor) && !immunity) + foreach (var b in ActiveBaits.Where(x => x.Target == actor)) + hints.AddForbiddenZone(ShapeDistance.Circle(b.Source.Position, MinimumDistance), b.Activation); + } +} diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index edd6fb693d..1747a494eb 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -9,6 +9,7 @@ public enum ActorType : ushort Pet = 0x202, Chocobo = 0x203, Enemy = 0x205, + Buddy = 0x209, EventNpc = 0x300, Treasure = 0x400, Aetheryte = 0x500, diff --git a/BossMod/Data/PartyState.cs b/BossMod/Data/PartyState.cs index bead5126e6..8b92e84f5a 100644 --- a/BossMod/Data/PartyState.cs +++ b/BossMod/Data/PartyState.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using FFXIVClientStructs.FFXIV.Client.Game.UI; namespace BossMod; @@ -30,7 +29,6 @@ public sealed class PartyState public int LimitBreakCur; public int LimitBreakMax = 10000; - public unsafe bool PartyContainsBuddies => UIState.Instance()->Buddy.DutyHelperInfo.ENpcIds.Length > 0; public PartyState(ActorState actorState) { diff --git a/BossMod/Modules/Endwalker/Alliance/A10Lions/A10Lions.cs b/BossMod/Modules/Endwalker/Alliance/A10Lions/A10Lions.cs index 197c32b84a..c8cd4497dd 100644 --- a/BossMod/Modules/Endwalker/Alliance/A10Lions/A10Lions.cs +++ b/BossMod/Modules/Endwalker/Alliance/A10Lions/A10Lions.cs @@ -15,7 +15,7 @@ public A10LionsStates(A10Lions module) : base(module) } [ModuleInfo(BossModuleInfo.Maturity.Verified, PrimaryActorOID = (uint)OID.Lion, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 866, NameID = 11294, SortOrder = 4)] -public class A10Lions(WorldState ws, Actor primary) : BossModule(ws, primary, new(-677.25f, -606.25f), new ArenaBoundsCircle(25)) +public class A10Lions(WorldState ws, Actor primary) : BossModule(ws, primary, new(-677.25f, -606.25f), new ArenaBoundsCircle(24.5f)) { private Actor? _lioness; diff --git a/BossMod/Modules/Endwalker/Alliance/A13Azeyma/A13Azeyma.cs b/BossMod/Modules/Endwalker/Alliance/A13Azeyma/A13Azeyma.cs index e3e79fdf18..4fe28feddd 100644 --- a/BossMod/Modules/Endwalker/Alliance/A13Azeyma/A13Azeyma.cs +++ b/BossMod/Modules/Endwalker/Alliance/A13Azeyma/A13Azeyma.cs @@ -10,7 +10,5 @@ class SublimeSunset(BossModule module) : Components.LocationTargetedAOEs(module, public class A13Azeyma(WorldState ws, Actor primary) : BossModule(ws, primary, NormalCenter, NormalBounds) { public static readonly WPos NormalCenter = new(-750, -750); - public static readonly WPos TriangleCenter = new(-750, -753.5f); - public static readonly ArenaBoundsCircle NormalBounds = new(30); - public static readonly ArenaBoundsComplex TriangleBounds = new([new TriangleE(TriangleCenter, 12)]); + public static readonly ArenaBoundsCircle NormalBounds = new(29.5f); } diff --git a/BossMod/Modules/Endwalker/Alliance/A13Azeyma/WildfireWard.cs b/BossMod/Modules/Endwalker/Alliance/A13Azeyma/WildfireWard.cs index 5532ed18d2..2cbc4f41bb 100644 --- a/BossMod/Modules/Endwalker/Alliance/A13Azeyma/WildfireWard.cs +++ b/BossMod/Modules/Endwalker/Alliance/A13Azeyma/WildfireWard.cs @@ -1,22 +1,31 @@ namespace BossMod.Endwalker.Alliance.A13Azeyma; class WildfireWard(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.IlluminatingGlimpse), 15, false, 1, kind: Kind.DirLeft); -class ArenaBounds(BossModule module) : BossComponent(module) +class ArenaBounds(BossModule module) : Components.GenericAOEs(module) { + private static readonly Shape triangle = new PolygonCustom([new(-761.5f, -743.38f), new(-750, -763.27f), new(-738.51f, -743.39f)]); + private static readonly Shape circle = new Circle(A13Azeyma.NormalCenter, 29.5f); + private static readonly AOEShapeCustom triangleCutOut = new([circle], [triangle]); + private static readonly ArenaBoundsComplex TriangleBounds = new([triangle]); + + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + public override void OnEventEnvControl(byte index, uint state) { if (index == 0x1C) { if (state == 0x00020001) + _aoe = new(triangleCutOut, A13Azeyma.NormalCenter, default, Module.WorldState.FutureTime(5.7f)); + else if (state == 0x00200010) { - Arena.Bounds = A13Azeyma.TriangleBounds; - Arena.Center = A13Azeyma.TriangleCenter; + _aoe = null; + Arena.Bounds = TriangleBounds; + Arena.Center = TriangleBounds.Center; } - if (state == 0x00080004) - { + else if (state == 0x00080004) Arena.Bounds = A13Azeyma.NormalBounds; - Arena.Center = A13Azeyma.NormalCenter; - } } } } diff --git a/BossMod/Modules/Endwalker/Alliance/A32Llymlaen/SurgingWave.cs b/BossMod/Modules/Endwalker/Alliance/A32Llymlaen/SurgingWave.cs index 77c14e9bbb..691807f42e 100644 --- a/BossMod/Modules/Endwalker/Alliance/A32Llymlaen/SurgingWave.cs +++ b/BossMod/Modules/Endwalker/Alliance/A32Llymlaen/SurgingWave.cs @@ -36,10 +36,13 @@ public class SurgingWaveFrothingSea : Components.Exaflare public override void OnEventEnvControl(byte index, uint state) { var _activation = WorldState.FutureTime(30); - if (state == 0x00800040 && index == 0x49) - Lines.Add(new() { Next = new(-80, -900), Advance = 2.3f * _rot1.ToDirection(), NextExplosion = _activation, TimeToMove = 0.9f, ExplosionsLeft = 13, MaxShownExplosions = 2, Rotation = _rot1 }); - if (state == 0x08000400 && index == 0x49) - Lines.Add(new() { Next = new(80, -900), Advance = 2.3f * _rot2.ToDirection(), NextExplosion = _activation, TimeToMove = 0.9f, ExplosionsLeft = 13, MaxShownExplosions = 2, Rotation = _rot2 }); + if (index == 0x49) + { + if (state == 0x00800040) + Lines.Add(new() { Next = new(-80, -900), Advance = 2.3f * _rot1.ToDirection(), NextExplosion = _activation, TimeToMove = 0.9f, ExplosionsLeft = 13, MaxShownExplosions = 2, Rotation = _rot1 }); + if (state == 0x08000400) + Lines.Add(new() { Next = new(80, -900), Advance = 2.3f * _rot2.ToDirection(), NextExplosion = _activation, TimeToMove = 0.9f, ExplosionsLeft = 13, MaxShownExplosions = 2, Rotation = _rot2 }); + } } public override void OnEventCast(Actor caster, ActorCastEvent spell) diff --git a/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD70Aeturna.cs b/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD70Aeturna.cs index abb100b04f..dd442d66fb 100644 --- a/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD70Aeturna.cs +++ b/BossMod/Modules/Endwalker/DeepDungeon/EurekaOrthos/DD70Aeturna.cs @@ -2,9 +2,9 @@ namespace BossMod.Endwalker.DeepDungeon.EurekaOrthos.DD70Aeturna; public enum OID : uint { - Boss = 0x3D1B, // R5.950, x1 - AllaganCrystal = 0x3D1C, // R1.500, x4 - Helper = 0x233C, // R0.500, x12, 523 type + Boss = 0x3D1B, // R5.95 + AllaganCrystal = 0x3D1C, // R1.5 + Helper = 0x233C } public enum AID : uint @@ -21,98 +21,17 @@ public enum AID : uint ShatterCircle = 31439, // 3D1C->self, 3.0s cast, range 8 circle ShatterCone = 31440, // 3D1C->self, 2.5s cast, range 18+R 150-degree cone SteelClaw = 31445, // 3D1B->player, 5.0s cast, single-target - Teleport = 31446, // 3D1B->location, no cast, single-target, boss teleports mid -} - -public enum IconID : uint -{ - tankbuster = 198, // player + Teleport = 31446 // 3D1B->location, no cast, single-target, boss teleports mid } public enum TetherID : uint { FerocityTetherGood = 1, // Boss->player - FerocityTetherStretch = 57, // Boss->player + FerocityTetherBad = 57 // Boss->player } class SteelClaw(BossModule module) : Components.SingleTargetDelayableCast(module, ActionID.MakeSpell(AID.SteelClaw)); - -class FerocityGood(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeCone(0, 0.Degrees()), (uint)TetherID.FerocityTetherGood) // TODO: consider generalizing stretched tethers? -{ - private ulong target; - - public override void OnTethered(Actor source, ActorTetherInfo tether) - { - base.OnTethered(source, tether); - if (tether.ID == (uint)TetherID.FerocityTetherGood) - target = tether.Target; - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) - { - if (DrawTethers && target == pc.InstanceID && CurrentBaits.Count > 0) - { - foreach (var b in ActiveBaits) - { - if (Arena.Config.ShowOutlinesAndShadows) - Arena.AddLine(b.Source.Position, b.Target.Position, 0xFF000000, 2); - Arena.AddLine(b.Source.Position, b.Target.Position, ArenaColor.Safe); - } - } - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (target == actor.InstanceID && CurrentBaits.Count > 0) - hints.Add("Tether is stretched!", false); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - base.AddAIHints(slot, actor, assignment, hints); - if (target == actor.InstanceID && CurrentBaits.Count > 0) - hints.AddForbiddenZone(ShapeDistance.Circle(Module.PrimaryActor.Position, 15)); - } -} - -class FerocityBad(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeCone(0, 0.Degrees()), (uint)TetherID.FerocityTetherStretch) -{ - private ulong target; - - public override void OnTethered(Actor source, ActorTetherInfo tether) - { - base.OnTethered(source, tether); - if (tether.ID == (uint)TetherID.FerocityTetherStretch) - target = tether.Target; - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) - { - if (DrawTethers && target == pc.InstanceID && CurrentBaits.Count > 0) - { - foreach (var b in ActiveBaits) - { - if (Arena.Config.ShowOutlinesAndShadows) - Arena.AddLine(b.Source.Position, b.Target.Position, 0xFF000000, 2); - Arena.AddLine(b.Source.Position, b.Target.Position, ArenaColor.Danger); - } - } - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (target == actor.InstanceID && CurrentBaits.Count > 0) - hints.Add("Stretch tether further!"); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - base.AddAIHints(slot, actor, assignment, hints); - if (target == actor.InstanceID && CurrentBaits.Count > 0) - hints.AddForbiddenZone(ShapeDistance.Circle(Module.PrimaryActor.Position, 15)); - } -} - +class Ferocity(BossModule module) : Components.StretchTetherDuo(module, (uint)TetherID.FerocityTetherBad, (uint)TetherID.FerocityTetherGood, 15, activationDelay: 5.7f); class PreternaturalTurnCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PreternaturalTurnCircle), new AOEShapeCircle(15)); class PreternaturalTurnDonut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PreternaturalTurnDonut), new AOEShapeDonut(6, 30)); @@ -169,8 +88,7 @@ public DD70AeturnaStates(BossModule module) : base(module) { TrivialPhase() .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D021Barnabas.cs b/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D021Barnabas.cs index 21b19f04d8..8b36e40fd4 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D021Barnabas.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D021Barnabas.cs @@ -10,9 +10,15 @@ public enum OID : uint public enum AID : uint { AutoAttack = 872, // Boss->player, no cast, single-target - DynamicPound1 = 25157, // Boss->self, 7.0s cast, range 40 width 6 rect - DynamicPound2 = 25326, // Boss->self, 7.0s cast, range 40 width 6 rect - DynamicScrapline = 25158, // Boss->self, 7.0s cast, range 8 circle + DynamicPoundMinus = 25157, // Boss->self, 7.0s cast, range 40 width 6 rect + DynamicPoundPlus = 25326, // Boss->self, 7.0s cast, range 40 width 6 rect + DynamicPoundPull = 24693, // Helper->self, no cast, range 50 width 50 rect, pull 9, between centers + DynamicPoundKB = 24694, // Helper->self, no cast, range 50 width 50 rect, knockback 9, dir left/right + DynamicScraplineMinus = 25158, // Boss->self, 7.0s cast, range 8 circle + DynamicScraplinePlus = 25328, // Boss->self, 7.0s cast, range 8 circle + DynamicScraplineMinusKB = 25053, // Helper->self, no cast, range 50 circle, pull 5, between centers + DynamicScraplineMinusPull = 25054, // Helper->self, no cast, range 50 circle, knockback 5, away from source + ElectromagneticRelease1 = 25327, // Helper->self, 9.5s cast, range 40 width 6 rect ElectromagneticRelease2 = 25329, // Helper->self, 9.5s cast, range 8 circle GroundAndPound1 = 25159, // Boss->self, 3.5s cast, range 40 width 6 rect @@ -20,66 +26,190 @@ public enum AID : uint RollingScrapline = 25323, // Boss->self, 3.0s cast, range 8 circle Shock = 25330, // Thunderball->self, 3.0s cast, range 8 circle - ShockingForce = 25324, // Boss->players, 5.0s cast, range 6 circle + ShockingForce = 25324, // Boss->players, 5.0s cast, range 6 circle, stack Thundercall = 25325, // Boss->self, 3.0s cast, single-target - Unknown1 = 24693, // Helper->self, no cast, range 50 width 50 rect - Unknown2 = 24694, // Helper->self, no cast, range 50 width 50 rect - Unknown3 = 25053, // Helper->self, no cast, range 50 circle - Unknown4 = 25054, // Helper->self, no cast, range 50 circle } -public enum SID : uint +public enum IconID : uint { - Eukrasia = 2606, // none->player, extra=0x0 - VulnerabilityUp = 1789, // Boss->33F9/player, extra=0x1 - Stun = 149, // Boss->33F9, extra=0x0 - Stun2 = 2953, // none->player, extra=0x0 - Electrocution = 2086, // none->player, extra=0x0 + Plus = 162, // player + Minus = 163, // player + BossMinus = 290, // Boss + BossPlus = 291, // Boss + Stackmarker = 62, // player } -public enum IconID : uint +class ArenaChange(BossModule module) : Components.GenericAOEs(module) { - Icon163 = 163, // player - Icon162 = 162, // player - Icon290 = 290, // Boss - Icon62 = 62, // player + private static readonly AOEShapeDonut donut = new(15, 19.5f); + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 0x00 && state == 0x00020001) + { + Module.Arena.Bounds = D021Barnabas.SmallerBounds; + _aoe = null; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.GroundAndPound1 or AID.GroundAndPound2 && Module.Arena.Bounds == D021Barnabas.StartingBounds) + _aoe = new(donut, Module.Center, default, spell.NPCFinishAt.AddSeconds(6.1f)); + } } -public enum TetherID : uint +class Magnetism(BossModule module) : Components.Knockback(module, ignoreImmunes: true) { - Tether28 = 28, // player->Boss + private enum MagneticPole { None, Plus, Minus } + private enum Shape { None, Rect, Circle } + private MagneticPole CurrentPole { get; set; } + private Shape CurrentShape { get; set; } + private readonly List<(Actor, uint)> iconOnActor = []; + private DateTime activation; + private Angle rotation; + private const int RectDistance = 9; + private const int CircleDistance = 5; + private readonly Angle offset = 90.Degrees(); + private static readonly AOEShapeCone _shape = new(30, 90.Degrees()); + + private bool IsKnockback(Actor actor, Shape shape, MagneticPole pole) + => CurrentShape == shape && CurrentPole == pole && iconOnActor.Contains((actor, (uint)(pole == MagneticPole.Plus ? IconID.Plus : IconID.Minus))); + + private bool IsPull(Actor actor, Shape shape, MagneticPole pole) + => CurrentShape == shape && CurrentPole == pole && iconOnActor.Contains((actor, (uint)(pole == MagneticPole.Plus ? IconID.Minus : IconID.Plus))); + + public override IEnumerable Sources(int slot, Actor actor) + { + if (IsKnockback(actor, Shape.Rect, MagneticPole.Plus) || IsKnockback(actor, Shape.Rect, MagneticPole.Minus)) + { + yield return new(Module.Center, RectDistance, activation, _shape, rotation + offset, Kind.DirForward); + yield return new(Module.Center, RectDistance, activation, _shape, rotation - offset, Kind.DirForward); + } + else if (IsPull(actor, Shape.Rect, MagneticPole.Plus) || IsPull(actor, Shape.Rect, MagneticPole.Minus)) + { + yield return new(Module.Center, RectDistance, activation, _shape, rotation + offset, Kind.DirBackward); + yield return new(Module.Center, RectDistance, activation, _shape, rotation - offset, Kind.DirBackward); + } + else if (IsKnockback(actor, Shape.Circle, MagneticPole.Plus) || IsKnockback(actor, Shape.Circle, MagneticPole.Minus)) + yield return new(Module.Center, CircleDistance, activation); + else if (IsPull(actor, Shape.Circle, MagneticPole.Plus) || IsPull(actor, Shape.Circle, MagneticPole.Minus)) + yield return new(Module.Center, CircleDistance, activation, default, default, Kind.TowardsOrigin); + } + + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => (Module.FindComponent()?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false) || + (Module.FindComponent()?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false) || !Module.InBounds(pos); + + public override void OnEventIcon(Actor actor, uint iconID) + { + if (iconID is ((uint)IconID.Plus) or ((uint)IconID.Minus)) + { + iconOnActor.Add((actor, iconID)); + activation = Module.WorldState.FutureTime(7); + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.DynamicPoundPlus: + case AID.DynamicScraplinePlus: + CurrentPole = MagneticPole.Plus; + rotation = spell.Rotation; + break; + case AID.DynamicScraplineMinus: + case AID.DynamicPoundMinus: + CurrentPole = MagneticPole.Minus; + rotation = spell.Rotation; + break; + case AID.ElectromagneticRelease1: + CurrentShape = Shape.Rect; + break; + case AID.ElectromagneticRelease2: + CurrentShape = Shape.Circle; + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.DynamicPoundMinus or AID.DynamicPoundPlus or AID.DynamicScraplineMinus or AID.DynamicScraplinePlus) + { + CurrentShape = Shape.None; + iconOnActor.Clear(); + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var forbidden = new List>(); + { + if (IsKnockback(actor, Shape.Circle, MagneticPole.Plus) || IsKnockback(actor, Shape.Circle, MagneticPole.Minus)) + forbidden.Add(ShapeDistance.InvertedCircle(Module.Center, 10)); + else if (IsPull(actor, Shape.Circle, MagneticPole.Plus) || IsPull(actor, Shape.Circle, MagneticPole.Minus)) + forbidden.Add(ShapeDistance.Circle(Module.Center, 13)); + else if (IsKnockback(actor, Shape.Rect, MagneticPole.Plus) || IsKnockback(actor, Shape.Rect, MagneticPole.Minus)) + forbidden.Add(ShapeDistance.InvertedCircle(Module.Center, 6)); + else if (IsPull(actor, Shape.Rect, MagneticPole.Plus) || IsPull(actor, Shape.Rect, MagneticPole.Minus)) + forbidden.Add(ShapeDistance.Rect(Module.Center, rotation, 15, 15, 12)); + if (forbidden.Count > 0) + hints.AddForbiddenZone(p => forbidden.Select(f => f(p)).Max(), activation); + } + } } class ElectromagneticRelease1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ElectromagneticRelease1), new AOEShapeRect(40, 3)); class ElectromagneticRelease2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ElectromagneticRelease2), new AOEShapeCircle(8)); -class GroundAndPound1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundAndPound1), new AOEShapeRect(40, 6)); -class GroundAndPound2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundAndPound2), new AOEShapeRect(40, 6)); +class GroundAndPound1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundAndPound1), new AOEShapeRect(40, 3)); +class GroundAndPound2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GroundAndPound2), new AOEShapeRect(40, 3)); -class DynamicPound(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DynamicPound1), new AOEShapeRect(40, 6)); -class DynamicPound2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DynamicPound2), new AOEShapeRect(40, 6)); -class DynamicScrapline(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DynamicScrapline), new AOEShapeCircle(8)); -class RollingScrapline(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RollingScrapline), new AOEShapeCircle(6)); +class DynamicPoundMinus(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DynamicPoundMinus), new AOEShapeRect(40, 3)); +class DynamicPoundPlus(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DynamicPoundPlus), new AOEShapeRect(40, 3)); +class DynamicScraplineMinus(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DynamicScraplineMinus), new AOEShapeCircle(8)); +class DynamicScraplinePlus(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DynamicScraplinePlus), new AOEShapeCircle(8)); +class RollingScrapline(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RollingScrapline), new AOEShapeCircle(8)); class Shock(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Shock), new AOEShapeCircle(8)); -class ShockingForce(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ShockingForce), 6, 8); +class ShockingForce(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ShockingForce), 6, 4); + +class StayInBounds(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (!Module.InBounds(actor.Position)) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); + } +} class D021BarnabasStates : StateMachineBuilder { public D021BarnabasStates(BossModule module) : base(module) { TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter(); } } -[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "CombatReborn Team", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 785, NameID = 10279)] -public class D021Barnabas(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, 71), new ArenaBoundsCircle(20)); +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 785, NameID = 10279)] +public class D021Barnabas(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, 71), StartingBounds) +{ + public static readonly ArenaBoundsCircle StartingBounds = new(19.5f); + public static readonly ArenaBoundsCircle SmallerBounds = new(15); +} diff --git a/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D022Lugae.cs b/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D022Lugae.cs index e0fd3ad177..d9dea58636 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D022Lugae.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D022Lugae.cs @@ -1,11 +1,13 @@ -namespace BossMod.Endwalker.Dungeon.D02TowerOfBabil.D022Lugae; +using System.Drawing; + +namespace BossMod.Endwalker.Dungeon.D02TowerOfBabil.D022Lugae; public enum OID : uint { Boss = 0x33FA, // R=3.9 Helper = 0x233C, - MagitekChakram = 0x33FB, // R3.000, x0 (spawn during fight) - MagitekExplosive = 0x33FC, // R2.000, x0 (spawn during fight) + MagitekChakram = 0x33FB, // R=3.0 + MagitekExplosive = 0x33FC // R=2.0 } public enum AID : uint @@ -19,7 +21,7 @@ public enum AID : uint MagitekRay = 25340, // Boss->self, 3.0s cast, range 50 width 6 rect MightyBlow = 25332, // MagitekChakram->self, 7.0s cast, range 40 width 8 rect SurfaceMissile = 25335, // Helper->location, 3.5s cast, range 6 circle - ThermalSuppression = 25338, // Boss->self, 5.0s cast, range 60 circle + ThermalSuppression = 25338 // Boss->self, 5.0s cast, range 60 circle } public enum SID : uint @@ -27,14 +29,73 @@ public enum SID : uint Minimum = 2504, // none->player, extra=0x14 Breathless = 2672, // none->player, extra=0x1/0x2/0x3/0x4/0x5/0x6 Heavy = 2391, // none->player, extra=0x32 - Toad = 2671, // none->player, extra=0x1B1 + Toad = 2671 // none->player, extra=0x1B1 +} + +class DownpourMagitekChakram(BossModule module) : Components.GenericAOEs(module) +{ + private enum Mechanic { None, Downpour, Chakram } + private Mechanic CurrentMechanic { get; set; } + private static readonly AOEShapeRect squareSafe = new(4, 4, 4, default, true); + private static readonly AOEShapeRect squareRisky = new(4, 4, 4); + private static readonly WPos toad = new(213, 306); + private static readonly WPos mini = new(229, 306); + private const string toadHint = "Walk onto green square!"; + private const string miniHint = "Walk onto purple square!"; + private bool avoidSquares; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (CurrentMechanic == Mechanic.Downpour) + { + var breathless = actor.FindStatus(SID.Breathless) != null; + yield return new(breathless ? squareSafe : squareRisky, toad, Color: breathless ? ArenaColor.SafeFromAOE : ArenaColor.AOE); + yield return new(squareRisky, mini); + } + else if (CurrentMechanic == Mechanic.Chakram) + { + var minimum = !avoidSquares && actor.FindStatus(SID.Minimum) == null; + yield return new(minimum ? squareSafe : squareRisky, mini, Color: minimum ? ArenaColor.SafeFromAOE : ArenaColor.AOE); + yield return new(squareRisky, toad); + } + } + + public override void OnEventEnvControl(byte index, uint state) + { + if (index is 0x01 or 0x02 && state == 0x00080004) + { + avoidSquares = false; + CurrentMechanic = Mechanic.None; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Downpour) + CurrentMechanic = Mechanic.Downpour; + if ((AID)spell.Action.ID == AID.MagitekChakram) + CurrentMechanic = Mechanic.Chakram; + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.ThermalSuppression && CurrentMechanic != Mechanic.None) + avoidSquares = true; + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (CurrentMechanic == Mechanic.Chakram && actor.FindStatus(SID.Minimum) == null && !avoidSquares) + hints.Add(miniHint); + if (CurrentMechanic == Mechanic.Downpour && actor.FindStatus(SID.Toad) == null) + hints.Add(toadHint); + } } class ThermalSuppression(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ThermalSuppression)); -class MagitekMissile(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.MagitekMissile), 6); +class MightyRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekRay), new AOEShapeRect(50, 3)); class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCross(40, 4)); class SurfaceMissile(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SurfaceMissile), 6); -class MightyBlow(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MightyBlow), new AOEShapeRect(40, 4)); class D022LugaeStates : StateMachineBuilder { @@ -42,12 +103,12 @@ public D022LugaeStates(BossModule module) : base(module) { TrivialPhase() .ActivateOnEnter() - //.ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter(); } } -[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "CombatReborn Team", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 785, NameID = 10281)] -public class D022Lugae(WorldState ws, Actor primary) : BossModule(ws, primary, new(220, 306), new ArenaBoundsSquare(20)); +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 785, NameID = 10281)] +public class D022Lugae(WorldState ws, Actor primary) : BossModule(ws, primary, new(221, 306), new ArenaBoundsSquare(19.5f)); diff --git a/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D023Anima.cs b/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D023Anima.cs index c361a2b688..417199c91c 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D023Anima.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D02TowerOfBabil/D023Anima.cs @@ -4,28 +4,31 @@ public enum OID : uint { Boss = 0x33FD, // R=18.7 LowerAnima = 0x3400, // R=18.7 - Helper = 0x233C, - MagitekCrane = 0x3320, // R0.600, x3 - IronNail = 0x3401, // R1.000, x0 (spawn during fight) - LunarNail = 0x33FE, // R1.000, x0 (spawn during fight) - MegaGraviton = 0x33FF, // R1.000, x0 (spawn during fight) + IronNail = 0x3401, // R=1.0 + LunarNail = 0x33FE, // R=1.0 + MegaGraviton = 0x33FF, // R=1.0 Actor1eb239 = 0x1EB239, // R0.500, x0 (spawn during fight), EventObj type + Helper = 0x233C } public enum AID : uint { AutoAttack = 25341, // Boss->player, no cast, single-target - AetherialPull = 25345, // MegaGraviton->player, 8.0s cast, single-target Knockback towardsorigin 30 + AetherialPull = 25345, // MegaGraviton->player, 8.0s cast, single-target, pull 30 between centers - BoundlessPain1 = 25347, // Boss->self, 8.0s cast, single-target //players pulled to the center of the room and hit by a continuous AoE that slowly expands outwards - BoundlessPain2 = 25348, // Helper->location, no cast, range 6 circle - BoundlessPain3 = 25349, // Helper->location, no cast, range 6 circle + BoundlessPainPull = 26229, // Helper->self, no cast, range 60 circle, pull 60 between centers + BoundlessPainVisual = 25347, // Boss->self, 8.0s cast, single-target, creates expanding AOE + BoundlessPainFirst = 25348, // Helper->location, no cast, range 6 circle + BoundlessPainRest = 25349, // Helper->location, no cast, range 6 circle CharnelClaw = 25357, // IronNail->self, 6.0s cast, range 40 width 5 rect - CoffinScratch = 25358, // Helper->location, 3.5s cast, range 3 circle - Imperatum = 25353, // Boss->self, 5.0s cast, range 60 circle //Players are pulled into the floor to face the lower half of Anima + CoffinScratchFirst = 25358, // Helper->location, 3.5s cast, range 3 circle + CoffinScratchRest = 21239, // Helper->location, no cast, range 3 circle + + Imperatum = 25353, // Boss->self, 5.0s cast, range 60 circle, phase change + ImperatumPull = 23929, // Helper->player, no cast, single-target, pull 60 between centers LunarNail = 25342, // Boss->self, 3.0s cast, single-target @@ -33,71 +36,151 @@ public enum AID : uint ObliviatingClaw2 = 25355, // LowerAnima->self, 3.0s cast, single-target ObliviatingClawSpawnAOE = 25356, // IronNail->self, 6.0s cast, range 3 circle - Oblivion1 = 23697, // Helper->location, no cast, range 60 circle - Oblivion2 = 23872, // Helper->location, no cast, range 60 circle - Oblivion3 = 25359, // LowerAnima->self, 6.0s cast, single-target + OblivionVisual = 25359, // LowerAnima->self, 6.0s cast, single-target + OblivionStart = 23697, // Helper->location, no cast, range 60 circle + OblivionLast = 23872, // Helper->location, no cast, range 60 circle + + MegaGraviton = 25344, // Boss->self, 5.0s cast, range 60 circle, tether mechanic + GravitonSpark = 25346, // MegaGraviton->player, no cast, single-target, on touching the graviton - MegaGraviton = 25344, // Boss->self, 5.0s cast, range 60 circle // Tether mechanic PaterPatriaeAOE = 24168, // Helper->self, 3.5s cast, range 60 width 8 rect PaterPatriae2 = 25350, // Boss->self, 3.5s cast, single-target PhantomPain1 = 21182, // Boss->self, 7.0s cast, single-target PhantomPain2 = 25343, // Helper->self, 7.0s cast, range 20 width 20 rect - Unknown1 = 23929, // Helper->player, no cast, single-target Knockback 60 - Unknown2 = 26229, // Helper->self, no cast, range 60 circle Knockback towards origin 60 - Unknown3 = 27228, // LowerAnima->self, no cast, single-target + VisualModelChange = 27228, // LowerAnima->self, no cast, single-target + + EruptingPainVisual = 25351, // Boss->self, 5.0s cast, single-target + EruptingPain = 25352, // Helper->player, 5.0s cast, range 6 circle } -public enum SID : uint +public enum TetherID : uint { - AreaOfInfluenceUp = 1749, // none->Helper, extra=0x1/0x2/0x3/0x4/0x5/0x6/0x7/0x8/0x9/0xA/0xB/0xC - UnknownStatus = 2849, // none->player, extra=0xEC7 + PhantomPain = 162, // Helper->Helper + AetherialPullBad = 57, // MegaGraviton->player + AetherialPullGood = 17, // MegaGraviton->player + AnimaDrawsPower = 22, // Helper->Boss } -public enum IconID : uint +class ArenaChange(BossModule module) : BossComponent(module) { - Nox = 197, // player chasing AOE icon + private bool pausedAI; + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 0x03) + { + if (state == 0x00020001) + Module.Arena.Center = D023Anima.LowerArenaCenter; + if (state == 0x00080004) + Module.Arena.Center = D023Anima.UpperArenaCenter; + } + } + + // TODO: This hack is required because otherwise the game crashes on phase change if AI is on and player is following an NPC + // can be removed if the source of the bug is found and fixed + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Imperatum && AI.AIManager.Instance?.MasterSlot != 0) + { + AI.AIManager.Instance?.SwitchToIdle(); + pausedAI = true; + } + } + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.LowerAnima && pausedAI) + { + AI.AIManager.Instance?.SwitchToFollow(Service.Config.Get().FollowSlot); + pausedAI = false; + } + } } -public enum TetherID : uint +class BoundlessPain(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + private static readonly AOEShapeCircle circle = new(18); + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.BoundlessPainPull: + _aoe = new(circle, Module.Arena.Center); + break; + case AID.BoundlessPainFirst: + case AID.BoundlessPainRest: + if (++NumCasts == 20) + { + _aoe = null; + NumCasts = 0; + } + break; + } + } +} + +class Gravitons(BossModule module) : Components.GenericAOEs(module) { - Tether162 = 162, // Helper->Helper - Tether57 = 57, // MegaGraviton->player - Tether17 = 17, // MegaGraviton->player - Tether22 = 22, // Helper->Boss + private static readonly AOEShapeCircle circle = new(1); + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var g in Module.Enemies(OID.MegaGraviton).Where(x => !x.IsDead)) + yield return new(circle, g.Position); + } } -class CoffinScratch(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.CoffinScratch), 3); +class AetherialPull(BossModule module) : Components.StretchTetherDuo(module, (uint)TetherID.AetherialPullBad, (uint)TetherID.AetherialPullGood, 33, activationDelay: 7.9f, knockbackImmunity: true); -class PhantomPain2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PhantomPain2), new AOEShapeRect(20, 10)); -class PaterPatriaeAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PaterPatriaeAOE), new AOEShapeRect(60, 4)); -class CharnelClaw(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CharnelClaw), new AOEShapeRect(40, 2.5f)); +class CoffinScratch(BossModule module) : Components.StandardChasingAOEs(module, new AOEShapeCircle(3), ActionID.MakeSpell(AID.CoffinScratchFirst), ActionID.MakeSpell(AID.CoffinScratchRest), 6, 2, 5) +{ + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + base.OnEventCast(caster, spell); + if (Chasers.Count == 0) + { + ExcludedTargets.Reset(); + NumCasts = 0; + } + } +} +class PhantomPain(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PhantomPain2), new AOEShapeRect(10, 10, 10)); +class PaterPatriaeAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.PaterPatriaeAOE), new AOEShapeRect(60, 4)); +class CharnelClaw(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CharnelClaw), new AOEShapeRect(40, 2.5f), 5); +class ErruptingPain(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.EruptingPain), 6); class ObliviatingClawSpawnAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ObliviatingClawSpawnAOE), new AOEShapeCircle(3)); -class AetherialPull(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.AetherialPull), 30, kind: Kind.TowardsOrigin); - +class Oblivion(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.OblivionVisual), "Raidwide x16"); class MegaGraviton(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.MegaGraviton)); -class Imperatum(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Imperatum)); class D023AnimaStates : StateMachineBuilder { public D023AnimaStates(BossModule module) : base(module) { TrivialPhase() - //.ActivateOnEnter() //chasing aoe - //.ActivateOnEnter() //bad squares, for some reason offset by 5 so displaying incorrectly - //.ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter(); } } -[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "CombatReborn Team", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 785, NameID = 10285)] -public class D023Anima(WorldState ws, Actor primary) : BossModule(ws, primary, primary.Position.Z > -250 ? new(0, -180) : new(0, -400), new ArenaBoundsSquare(20)) +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 785, NameID = 10285)] +public class D023Anima(WorldState ws, Actor primary) : BossModule(ws, primary, UpperArenaCenter, new ArenaBoundsSquare(19.5f)) { + public static readonly WPos UpperArenaCenter = new(0, -180); + public static readonly WPos LowerArenaCenter = new(0, -400); protected override void DrawEnemies(int pcSlot, Actor pc) { Arena.Actors(Enemies(OID.Boss), ArenaColor.Enemy); diff --git a/BossMod/Modules/Endwalker/Dungeon/D03Vanaspati/D033Svarbhanu.cs b/BossMod/Modules/Endwalker/Dungeon/D03Vanaspati/D033Svarbhanu.cs index ee5ba61ed0..e9edc4a4e8 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D03Vanaspati/D033Svarbhanu.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D03Vanaspati/D033Svarbhanu.cs @@ -28,7 +28,7 @@ public enum AID : uint class ChaoticUndercurrent(BossModule module) : Components.GenericAOEs(module) { private enum Pattern { None, BBRR, RRBB, BRRB, RBBR } - private Pattern CurrentPattern = Pattern.None; + private Pattern CurrentPattern; private readonly List _aoes = []; private static readonly AOEShapeRect rect = new(40, 5); private static readonly Angle rotation = 90.Degrees(); diff --git a/BossMod/Modules/Endwalker/Dungeon/D06DeadEnds/D063Rala.cs b/BossMod/Modules/Endwalker/Dungeon/D06DeadEnds/D063Rala.cs index 890a791bf8..9ec24d85ae 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D06DeadEnds/D063Rala.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D06DeadEnds/D063Rala.cs @@ -30,9 +30,10 @@ public enum AID : uint public enum SID : uint { HiddenStatus = 2056, // none->GoldenWings, extra=0x16C, probably just a visual? - Doom = 1769, // Helper->player, extra=0x0, heal to full doom + Doom = 1769 // Helper->player, extra=0x0, heal to full doom } -class Necrosis(BossModule module) : BossComponent(module) + +class Doom(BossModule module) : BossComponent(module) { private readonly List _doomed = []; @@ -52,7 +53,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) { if (_doomed.Contains(actor) && !(actor.Role == Role.Healer)) hints.Add("You were doomed! Get healed to full fast."); - if (_doomed.Contains(actor) && actor.Role == Role.Healer) + else if (_doomed.Contains(actor) && actor.Role == Role.Healer) hints.Add("Heal yourself to full! (Doom)."); foreach (var c in _doomed) if (!_doomed.Contains(actor) && actor.Role == Role.Healer) @@ -60,9 +61,9 @@ public override void AddHints(int slot, Actor actor, TextHints hints) } } -class LamellarLight1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LamellarLight1), new AOEShapeCircle(15), 3); +class LamellarLightCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LamellarLight1), new AOEShapeCircle(15), 3); class Lifesbreath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Lifesbreath), new AOEShapeRect(50, 5)); -class LamellarLight3(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LamellarLight3), new AOEShapeRect(40, 2)); +class LamellarLightRect(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LamellarLight3), new AOEShapeRect(40, 2)); class StillEmbrace(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.StillEmbrace), 6); class Benevolence(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.Benevolence), 6, 4); @@ -77,8 +78,9 @@ class D063RalaStates : StateMachineBuilder public D063RalaStates(BossModule module) : base(module) { TrivialPhase() - .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D112GalateaMagna.cs b/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D112GalateaMagna.cs index 80dbd42da8..bdff07833f 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D112GalateaMagna.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D112GalateaMagna.cs @@ -227,7 +227,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) { if (_doomed.Contains(actor) && !(actor.Role == Role.Healer || actor.Class == Class.BRD)) hints.Add("You were doomed! Get cleansed fast."); - if (_doomed.Contains(actor) && (actor.Role == Role.Healer || actor.Class == Class.BRD)) + else if (_doomed.Contains(actor) && (actor.Role == Role.Healer || actor.Class == Class.BRD)) hints.Add("Cleanse yourself! (Doom)."); foreach (var c in _doomed) if (!_doomed.Contains(actor) && (actor.Role == Role.Healer || actor.Class == Class.BRD)) @@ -241,7 +241,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { if (_doomed.Count > 0 && actor.Role == Role.Healer) hints.PlannedActions.Add((ActionID.MakeSpell(WHM.AID.Esuna), c, 1, false)); - if (_doomed.Count > 0 && actor.Class == Class.BRD) + else if (_doomed.Count > 0 && actor.Class == Class.BRD) hints.PlannedActions.Add((ActionID.MakeSpell(BRD.AID.WardensPaean), c, 1, false)); } } diff --git a/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D113Cagnazzo.cs b/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D113Cagnazzo.cs index 6f588f8798..2fd152b9a3 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D113Cagnazzo.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D11LapisManalis/D113Cagnazzo.cs @@ -106,7 +106,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) var dir = spell.LocXZ - caster.Position; _casters.Add((caster.Position, new AOEShapeRect(dir.Length(), 4), Angle.FromDirection(dir))); } - if ((AID)spell.Action.ID == AID.HydraulicRam) + else if ((AID)spell.Action.ID == AID.HydraulicRam) _activation = spell.NPCFinishAt.AddSeconds(1.5f); //since these are charges of different length with 0s cast time, the activation times are different for each and there are different patterns, so we just pretend that they all start after the telegraphs end } @@ -136,7 +136,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.HydrobombTelegraph) _casters.Add(spell.LocXZ); - if ((AID)spell.Action.ID == AID.HydraulicRam) + else if ((AID)spell.Action.ID == AID.HydraulicRam) _activation = spell.NPCFinishAt.AddSeconds(2.2f); } diff --git a/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D121Lyngbakr.cs b/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D121Lyngbakr.cs index ebb77961eb..b62e3a6788 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D121Lyngbakr.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D121Lyngbakr.cs @@ -68,7 +68,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) class SonicBloop(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.SonicBloop)); class Waterspout(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Waterspout), 5); class TidalBreath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TidalBreath), new AOEShapeCone(40, 90.Degrees())); -class Tidalspout(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.Tidalspout), 6); +class Tidalspout(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.Tidalspout), 6, 4); class Upsweep(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Upsweep)); class BodySlam(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.BodySlam)); diff --git a/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D122Arkas.cs b/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D122Arkas.cs index e72c930b31..4164424ae2 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D122Arkas.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D12Aetherfont/D122Arkas.cs @@ -70,14 +70,61 @@ class LightningClaw(BossModule module) : Components.StackWithIcon(module, (uint) class ForkedFissures(BossModule module) : Components.GenericAOEs(module) { - private static readonly WPos[] patternIndex04Start = [new(419.293f, -445.661f), new(422.919f, -448.675f), new(419.359f, -445.715f), new(419.333f, -437.25f), new(432.791f, -434.82f), new(423.239f, -442.489f), new(426.377f, -437.596f), new(419.335f, -445.663f), new(417.162f, -442.421f), new(427.274f, -448.618f), new(430.839f, -441.877f), new(419.292f, -445.596f), new(427.482f, -448.548f), new(419.101f, -434.242f), new(424.274f, -433.427f), new(419.326f, -445.681f)]; - private static readonly WPos[] patternIndex04End = [new(420.035f, -454.124f), new(427.42f, -448.692f), new(412.039f, -447.562f), new(417.533f, -427.085f), new(433.860f, -427.97f), new(426.993f, -437.034f), new(433.646f, -433.433f), new(411.276f, -434.165f), new(419.394f, -436.118f), new(430.442f, -453.971f), new(439.872f, -438.59f), new(423.667f, -442.039f), new(431.815f, -441.032f), new(425.437f, -432.547f), new(428.824f, -425.528f), new(424.002f, -448.966f)]; - private static readonly WPos[] patternIndex03Start = [new(419.343f, -434.343f), new(416.325f, -437.829f), new(419.304f, -434.353f), new(427.97f, -434.253f), new(430.244f, -447.772f), new(422.523f, -438.223f), new(427.408f, -441.363f), new(419.274f, -434.245f), new(422.582f, -432.152f), new(416.35f, -442.222f), new(423.09f, -445.755f), new(419.412f, -434.285f), new(419.294f, -434.309f), new(416.47f, -442.448f), new(430.633f, -433.69f), new(431.389f, -439.114f)]; - private static readonly WPos[] patternIndex03End = [new(410.880f, -435.019f), new(416.312f, -442.557f), new(417.441f, -427.085f), new(437.949f, -432.547f), new(436.942f, -448.875f), new(428.031f, -442.039f), new(431.571f, -448.63f), new(430.9f, -426.261f), new(428.916f, -434.379f), new(411.032f, -445.457f), new(426.413f, -454.917f), new(422.934f, -438.59f), new(416.037f, -438.926f), new(423.941f, -446.738f), new(432.364f, -440.177f), new(439.475f, -443.809f)]; - private static readonly WPos[] patternIndex02Start = [new(430.635f, -434.592f), new(430.708f, -434.484f), new(434.518f, -440.005f), new(424.457f, -445.105f), new(430.834f, -434.374f), new(431.156f, -439.05f), new(430.599f, -434.383f), new(434.571f, -440.454f), new(423.033f, -437.371f), new(422.069f, -437.329f), new(419.287f, -441.464f), new(430.513f, -434.548f), new(417.501f, -435.027f), new(431.252f, -446.301f), new(430.458f, -434.36f), new(425.28f, -430.49f)]; - private static readonly WPos[] patternIndex02End = [new(439.139f, -435.325f), new(422.232f, -437.583f), new(431.083f, -446.983f), new(420.279f, -454.215f), new(429.831f, -440.299f), new(424.063f, -445.762f), new(434.379f, -440.269f), new(439.811f, -441.733f), new(411.612f, -433.28f), new(418.936f, -441.794f), new(412.07f, -447.532f), new(424.308f, -430.228f), new(410.025f, -440.269f), new(430.594f, -453.88f), new(429.587f, -425.834f), new(414.298f, -429.557f)]; - private static readonly WPos[] patternIndex01Start = [new(430.357f, -445.557f), new(434.507f, -440.159f), new(430.357f, -445.557f), new(424.561f, -449.554f), new(425.887f, -446.107f), new(423.516f, -434.294f), new(430.346f, -445.616f), new(419.902f, -439.485f), new(430.357f, -445.557f), new(430.404f, -445.54f), new(429.973f, -432.501f), new(427.648f, -437.101f), new(430.357f, -445.557f), new(427.648f, -438.04f), new(424.713f, -449.483f), new(418.756f, -446.251f)]; - private static readonly WPos[] patternIndex01End = [new(439.2f, -444.602f), new(435.416f, -429.313f), new(429.618f, -454.246f), new(423.24f, -454.887f), new(419.303f, -439.078f), new(417.441f, -427.054f), new(424.704f, -444.846f), new(410.757f, -435.294f), new(424.643f, -449.577f), new(427.451f, -437.247f), new(424.796f, -425.132f), new(423.148f, -433.951f), new(434.867f, -439.109f), new(431.693f, -426.627f), new(418.051f, -446.036f), new(411.063f, -445.579f)]; + private static readonly Dictionary Patterns = new() + { + [0x04] = (new WPos[] + { + new(419.293f, -445.661f), new(422.919f, -448.675f), new(419.359f, -445.715f), new(419.333f, -437.25f), + new(432.791f, -434.82f), new(423.239f, -442.489f), new(426.377f, -437.596f), new(419.335f, -445.663f), + new(417.162f, -442.421f), new(427.274f, -448.618f), new(430.839f, -441.877f), new(419.292f, -445.596f), + new(427.482f, -448.548f), new(419.101f, -434.242f), new(424.274f, -433.427f), new(419.326f, -445.681f) + }, new WPos[] + { + new(420.035f, -454.124f), new(427.42f, -448.692f), new(412.039f, -447.562f), new(417.533f, -427.085f), + new(433.860f, -427.97f), new(426.993f, -437.034f), new(433.646f, -433.433f), new(411.276f, -434.165f), + new(419.394f, -436.118f), new(430.442f, -453.971f), new(439.872f, -438.59f), new(423.667f, -442.039f), + new(431.815f, -441.032f), new(425.437f, -432.547f), new(428.824f, -425.528f), new(424.002f, -448.966f) + }), + [0x03] = (new WPos[] + { + new(419.343f, -434.343f), new(416.325f, -437.829f), new(419.304f, -434.353f), new(427.97f, -434.253f), + new(430.244f, -447.772f), new(422.523f, -438.223f), new(427.408f, -441.363f), new(419.274f, -434.245f), + new(422.582f, -432.152f), new(416.35f, -442.222f), new(423.09f, -445.755f), new(419.412f, -434.285f), + new(419.294f, -434.309f), new(416.47f, -442.448f), new(430.633f, -433.69f), new(431.389f, -439.114f) + }, new WPos[] + { + new(410.880f, -435.019f), new(416.312f, -442.557f), new(417.441f, -427.085f), new(437.949f, -432.547f), + new(436.942f, -448.875f), new(428.031f, -442.039f), new(431.571f, -448.63f), new(430.9f, -426.261f), + new(428.916f, -434.379f), new(411.032f, -445.457f), new(426.413f, -454.917f), new(422.934f, -438.59f), + new(416.037f, -438.926f), new(423.941f, -446.738f), new(432.364f, -440.177f), new(439.475f, -443.809f) + }), + [0x02] = (new WPos[] + { + new(430.635f, -434.592f), new(430.708f, -434.484f), new(434.518f, -440.005f), new(424.457f, -445.105f), + new(430.834f, -434.374f), new(431.156f, -439.05f), new(430.599f, -434.383f), new(434.571f, -440.454f), + new(423.033f, -437.371f), new(422.069f, -437.329f), new(419.287f, -441.464f), new(430.513f, -434.548f), + new(417.501f, -435.027f), new(431.252f, -446.301f), new(430.458f, -434.36f), new(425.28f, -430.49f) + }, new WPos[] + { + new(439.139f, -435.325f), new(422.232f, -437.583f), new(431.083f, -446.983f), new(420.279f, -454.215f), + new(429.831f, -440.299f), new(424.063f, -445.762f), new(434.379f, -440.269f), new(439.811f, -441.733f), + new(411.612f, -433.28f), new(418.936f, -441.794f), new(412.07f, -447.532f), new(424.308f, -430.228f), + new(410.025f, -440.269f), new(430.594f, -453.88f), new(429.587f, -425.834f), new(414.298f, -429.557f) + }), + [0x01] = (new WPos[] + { + new(430.357f, -445.557f), new(434.507f, -440.159f), new(430.357f, -445.557f), new(424.561f, -449.554f), + new(425.887f, -446.107f), new(423.516f, -434.294f), new(430.346f, -445.616f), new(419.902f, -439.485f), + new(430.357f, -445.557f), new(430.404f, -445.54f), new(429.973f, -432.501f), new(427.648f, -437.101f), + new(430.357f, -445.557f), new(427.648f, -438.04f), new(424.713f, -449.483f), new(418.756f, -446.251f) + }, new WPos[] + { + new(439.2f, -444.602f), new(435.416f, -429.313f), new(429.618f, -454.246f), new(423.24f, -454.887f), + new(419.303f, -439.078f), new(417.441f, -427.054f), new(424.704f, -444.846f), new(410.757f, -435.294f), + new(424.643f, -449.577f), new(427.451f, -437.247f), new(424.796f, -425.132f), new(423.148f, -433.951f), + new(434.867f, -439.109f), new(431.693f, -426.627f), new(418.051f, -446.036f), new(411.063f, -445.579f) + }) + }; private readonly List _patternStart = []; private readonly List _patternEnd = []; @@ -87,31 +134,13 @@ class ForkedFissures(BossModule module) : Components.GenericAOEs(module) public override void OnEventEnvControl(byte index, uint state) { - if (state is 0x00200010 or 0x00020001) + if (state is 0x00200010 or 0x00020001 && Patterns.TryGetValue(index, out var pattern)) { - if (index == 0x04) - { - _patternStart.AddRange(patternIndex04Start); - _patternEnd.AddRange(patternIndex04End); - } - if (index == 0x03) - { - _patternStart.AddRange(patternIndex03Start); - _patternEnd.AddRange(patternIndex03End); - } - if (index == 0x02) - { - _patternStart.AddRange(patternIndex02Start); - _patternEnd.AddRange(patternIndex02End); - } - if (index == 0x01) - { - _patternStart.AddRange(patternIndex01Start); - _patternEnd.AddRange(patternIndex01End); - } + _patternStart.AddRange(pattern.Start); + _patternEnd.AddRange(pattern.End); for (var i = _patternStart.Count - 1; i >= 0; i--) { - _aoes.Add(new(new AOEShapeRect((_patternEnd[i] - _patternStart[i]).Length(), 2), _patternStart[i], Angle.FromDirection(_patternEnd[i] - _patternStart[i]), WorldState.FutureTime(6))); + _aoes.Add(new AOEInstance(new AOEShapeRect((_patternEnd[i] - _patternStart[i]).Length(), 2), _patternStart[i], Angle.FromDirection(_patternEnd[i] - _patternStart[i]), WorldState.FutureTime(6))); _patternStart.RemoveAt(i); _patternEnd.RemoveAt(i); } diff --git a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D133Durante.cs b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D133Durante.cs index 562fb16753..1a37ed5697 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D133Durante.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D133Durante.cs @@ -97,49 +97,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion), new AOEShapeCircle(11)); class Explosion2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Explosion2), new AOEShapeCircle(9)); class FallenGrace(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.FallenGrace), 6); - -// TODO: create and use generic 'line stack' component -class AntipodalAssault(BossModule module) : Components.GenericBaitAway(module) -{ - private Actor? target; - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID.AntipodalAssaultMarker) - { - target = WorldState.Actors.Find(spell.MainTargetID); - CurrentBaits.Add(new(Module.PrimaryActor, target!, new AOEShapeRect(50, 4))); // the actual range is not 50, but just a charge of 8 width, but always goes until the edge of the arena, so we can simplify it - } - if ((AID)spell.Action.ID == AID.AntipodalAssault2) - CurrentBaits.Clear(); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (CurrentBaits.Count > 0 && actor != target) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 50, 0, 4)); - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (CurrentBaits.Count > 0) - { - if (!actor.Position.InRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 50, 0, 4)) - hints.Add("Stack!"); - else - hints.Add("Stack!", false); - } - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - foreach (var bait in CurrentBaits) - bait.Shape.Draw(Arena, BaitOrigin(bait), bait.Rotation, ArenaColor.SafeFromAOE); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) { } -} - +class AntipodalAssault(BossModule module) : Components.LineStack(module, ActionID.MakeSpell(AID.AntipodalAssaultMarker), ActionID.MakeSpell(AID.AntipodalAssault2), 5.4f); class HardSlash(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HardSlash), new AOEShapeCone(50, 45.Degrees())); class TwilightPhase(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TwilightPhase2), new AOEShapeRect(30, 10, 30)); class DarkImpact(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkImpact2), new AOEShapeCircle(25)); diff --git a/BossMod/Modules/Endwalker/TreasureHunt/TheShiftingGymnasionAgonon/GymnasiouMegakantha.cs b/BossMod/Modules/Endwalker/TreasureHunt/TheShiftingGymnasionAgonon/GymnasiouMegakantha.cs index 5d91787e36..7e79d59a15 100644 --- a/BossMod/Modules/Endwalker/TreasureHunt/TheShiftingGymnasionAgonon/GymnasiouMegakantha.cs +++ b/BossMod/Modules/Endwalker/TreasureHunt/TheShiftingGymnasionAgonon/GymnasiouMegakantha.cs @@ -49,13 +49,14 @@ class VineWhip(BossModule module) : Components.SingleTargetCast(module, ActionID class OdiousAtmosphere(BossModule module) : Components.GenericAOEs(module) { private AOEInstance? _aoe; + private static readonly AOEShapeCone cone = new(40, 90.Degrees()); public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.OdiousAtmosphere0) - _aoe = new(new AOEShapeCone(40, 90.Degrees()), caster.Position, spell.Rotation, spell.NPCFinishAt); + _aoe = new(cone, caster.Position, spell.Rotation, spell.NPCFinishAt); } public override void OnEventCast(Actor caster, ActorCastEvent spell) diff --git a/BossMod/Modules/Global/Quest/FF15Collab/Garuda.cs b/BossMod/Modules/Global/Quest/FF15Collab/Garuda.cs index 41576fd1bf..1ed5538128 100644 --- a/BossMod/Modules/Global/Quest/FF15Collab/Garuda.cs +++ b/BossMod/Modules/Global/Quest/FF15Collab/Garuda.cs @@ -113,53 +113,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme class MistralSong(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MistralSong), new AOEShapeCone(20, 75.Degrees())); class WickedTornado(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WickedTornado), new AOEShapeDonut(8, 20)); - -// TODO: create and use generic 'line stack' component -class MiniSupercell(BossModule module) : Components.GenericBaitAway(module) -{ - private Actor? target; - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID.MiniSupercell) - { - target = WorldState.Actors.Find(spell.MainTargetID); - CurrentBaits.Add(new(Module.PrimaryActor, target!, new AOEShapeRect(45, 3))); - } - } - - public override void OnCastFinished(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID == AID.MiniSupercell2) - CurrentBaits.Clear(); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (CurrentBaits.Count > 0 && actor != target) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 45, 0, 3)); - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (CurrentBaits.Count > 0) - { - if (!actor.Position.InRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 45, 0, 3)) - hints.Add("Stack!"); - else - hints.Add("Stack!", false); - } - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - foreach (var bait in CurrentBaits) - bait.Shape.Draw(Arena, BaitOrigin(bait), bait.Rotation, ArenaColor.SafeFromAOE); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) { } -} - +class MiniSupercell(BossModule module) : Components.LineStack(module, ActionID.MakeSpell(AID.MiniSupercell), ActionID.MakeSpell(AID.MiniSupercell2), 5, 45, 3, 2); class MiniSupercellKB(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.MiniSupercell2), 50, shape: new AOEShapeRect(45, 3), stopAtWall: true); class GravitationalForce(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 5, ActionID.MakeSpell(AID.GravitationalForce2), m => m.Enemies(OID.GravityVoidzone), 0); diff --git a/BossMod/Modules/Heavensward/Dungeon/D06Aetherochemical/D063LahabreaIgeyorhm.cs b/BossMod/Modules/Heavensward/Dungeon/D06Aetherochemical/D063LahabreaIgeyorhm.cs index 1427b73abb..16f33cc427 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D06Aetherochemical/D063LahabreaIgeyorhm.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D06Aetherochemical/D063LahabreaIgeyorhm.cs @@ -2,11 +2,11 @@ public enum OID : uint { - Boss = 0x3DA4, // R3.500, x1 - Igeyorhm = 0x3DA3, // R3.500, x1 - Helper = 0x233C, // R0.500, x12, 523 type - BurningStar = 0x3DA6, // R1.500, x0 (spawn during fight) - FrozenStar = 0x3DA5, // R1.500, x0 (spawn during fight) + Boss = 0x3DA4, // R3.5 + Igeyorhm = 0x3DA3, // R3.5 + BurningStar = 0x3DA6, // R1.5 + FrozenStar = 0x3DA5, // R1.5 + Helper = 0x233C } public enum AID : uint @@ -43,17 +43,18 @@ public enum AID : uint GripOfNight = 32790, // Boss->self, 6.0s cast, range 40 150-degree cone - ShadowFlare = 31885, // Boss/Lahabrea->self, 5.0s cast, range 40 circle + ShadowFlare = 31885 // Boss/Lahabrea->self, 5.0s cast, range 40 circle } public enum TetherID : uint { - StarTether = 110, // BurningStar/FrozenStar->BurningStar/FrozenStar + StarTether = 110 // BurningStar/FrozenStar->BurningStar/FrozenStar } class ShadowFlare(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ShadowFlare)); class GripOfNight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GripOfNight), new AOEShapeCone(40, 75.Degrees())); class DarkFireIIAOE(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.DarkFireIIAOE), 6); +class EndOfDays(BossModule module) : Components.LineStack(module, ActionID.MakeSpell(AID.EndOfDays), ActionID.MakeSpell(AID.EndOfDays2), 5.1f); class Stars(BossModule module) : Components.GenericAOEs(module) { @@ -143,48 +144,6 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -// TODO: create and use generic 'line stack' component -class EndOfDays(BossModule module) : Components.GenericBaitAway(module) -{ - private Actor? target; - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID.EndOfDays) - { - target = WorldState.Actors.Find(spell.MainTargetID); - CurrentBaits.Add(new(Module.PrimaryActor, target!, new AOEShapeRect(50, 4))); - } - if ((AID)spell.Action.ID == AID.EndOfDays2) - CurrentBaits.Clear(); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (CurrentBaits.Count > 0 && actor != target) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 50, 0, 4)); - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (CurrentBaits.Count > 0) - { - if (!actor.Position.InRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 50, 0, 4)) - hints.Add("Stack!"); - else - hints.Add("Stack!", false); - } - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - foreach (var bait in CurrentBaits) - bait.Shape.Draw(Arena, BaitOrigin(bait), bait.Rotation, ArenaColor.SafeFromAOE); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) { } -} - class D063LahabreaIgeyorhmStates : StateMachineBuilder { public D063LahabreaIgeyorhmStates(BossModule module) : base(module) diff --git a/BossMod/Modules/Heavensward/Dungeon/D06Aetherochemical/D064AscianPrime.cs b/BossMod/Modules/Heavensward/Dungeon/D06Aetherochemical/D064AscianPrime.cs index 07bfb47bf5..c0be751fba 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D06Aetherochemical/D064AscianPrime.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D06Aetherochemical/D064AscianPrime.cs @@ -1,17 +1,14 @@ -using BossMod.Endwalker.Hunt.RankS.KerShroud; - -namespace BossMod.Heavensward.Dungeon.D06Aetherochemical.D064AscianPrime; +namespace BossMod.Heavensward.Dungeon.D06Aetherochemical.D064AscianPrime; public enum OID : uint { - Boss = 0x3DA7, // R3.800, x1 - LahabreasShade = 0x3DAB, // R3.500, x1 - IgeyorhmsShade = 0x3DAA, // R3.500, x1 - Helper = 0x233C, // R0.500, x23, 523 type - - FrozenStar = 0x3DA8, // R1.500, x0 (spawn during fight) - BurningStar = 0x3DA9, // R1.500, x0 (spawn during fight) - ArcaneSphere = 0x3DAC, // R7.000, x0 (spawn during fight) + Boss = 0x3DA7, // R3.8 + LahabreasShade = 0x3DAB, // R3.5 + IgeyorhmsShade = 0x3DAA, // R3.5 + FrozenStar = 0x3DA8, // R1.5 + BurningStar = 0x3DA9, // R1.5 + ArcaneSphere = 0x3DAC, // R7.0 + Helper = 0x233C } public enum AID : uint @@ -74,7 +71,7 @@ public enum AID : uint UniversalManipulationTeleport = 31419, // Boss->location, no cast, single-target UniversalManipulation = 31900, // Boss->self, 5.0s cast, range 40 circle - UniversalManipulation2 = 33044, // Boss->player, no cast, single-target + UniversalManipulation2 = 33044 // Boss->player, no cast, single-target } public enum SID : uint @@ -85,7 +82,7 @@ public enum SID : uint BurningChains1 = 3505, // none->player, extra=0x0 BurningChains2 = 769, // none->player, extra=0x0 DarkWhispers = 3535, // none->player, extra=0x0 Spread marker - Untargetable = 2056, // Boss->Boss, extra=0x231, before limitbreak phase + Untargetable = 2056 // Boss->Boss, extra=0x231, before limitbreak phase } public enum IconID : uint @@ -95,14 +92,14 @@ public enum IconID : uint DarkWhispers = 139, // player AncientFrost = 161, // player BurningChains = 97, // player - DarkFire = 311, // player + DarkFire = 311 // player } public enum TetherID : uint { StarTether = 110, // FrozenStar/BurningStar->FrozenStar/BurningStar BurningChains = 9, // player->player - ArcaneSphere = 197, // ArcaneSphere->Boss + ArcaneSphere = 197 // ArcaneSphere->Boss } class AncientCircle(BossModule module) : Components.UniformStackSpread(module, 5, 4) @@ -163,50 +160,8 @@ class AncientEruptionAOE(BossModule module) : Components.LocationTargetedAOEs(mo class DarkBlizzardIIIAOE4(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkBlizzardIIIAOE4), new AOEShapeCone(41, 10.Degrees())); class DarkBlizzardIIIAOE5(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DarkBlizzardIIIAOE5), new AOEShapeCone(41, 10.Degrees())); class DarkFireIIAOE(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.DarkFireIIAOE), 6); - class BurningChains(BossModule module) : Components.Chains(module, (uint)TetherID.BurningChains, ActionID.MakeSpell(AID.BurningChains)); - -// TODO: create and use generic 'line stack' component -class EntropicFlame(BossModule module) : Components.GenericBaitAway(module) -{ - private Actor? target; - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID.EntropicFlame) - { - target = WorldState.Actors.Find(spell.MainTargetID); - CurrentBaits.Add(new(Module.PrimaryActor, target!, new AOEShapeRect(50, 4))); - } - if ((AID)spell.Action.ID == AID.EntropicFlame2) - CurrentBaits.Clear(); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (CurrentBaits.Count > 0 && actor != target) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 50, 0, 4)); - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (CurrentBaits.Count > 0) - { - if (!actor.Position.InRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 50, 0, 4)) - hints.Add("Stack!"); - else - hints.Add("Stack!", false); - } - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - foreach (var bait in CurrentBaits) - bait.Shape.Draw(Arena, BaitOrigin(bait), bait.Rotation, ArenaColor.SafeFromAOE); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) { } -} +class EntropicFlame(BossModule module) : Components.LineStack(module, ActionID.MakeSpell(AID.EntropicFlame), ActionID.MakeSpell(AID.EntropicFlame3), 5.2f); class Stars(BossModule module) : Components.GenericAOEs(module) { diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D013Philia.cs b/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D013Philia.cs index 765570d638..8ecdbef4ce 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D013Philia.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D01Holminster/D013Philia.cs @@ -200,47 +200,7 @@ class PendulumAOE(BossModule module) : Components.LocationTargetedAOEs(module, A class RightKnout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RightKnout), new AOEShapeCone(24, 105.Degrees())); class Taphephobia(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Taphephobia2), 6); -// TODO: create and use generic 'line stack' component -class IntoTheLight(BossModule module) : Components.GenericBaitAway(module) -{ - private Actor? target; - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID.IntoTheLight) - { - target = WorldState.Actors.Find(spell.MainTargetID); - CurrentBaits.Add(new(Module.PrimaryActor, target!, new AOEShapeRect(50, 4))); - } - if ((AID)spell.Action.ID == AID.IntoTheLight2) - CurrentBaits.Clear(); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (CurrentBaits.Count > 0 && actor != target) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 50, 0, 4)); - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (CurrentBaits.Count > 0) - { - if (!actor.Position.InRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 50, 0, 4)) - hints.Add("Stack!"); - else - hints.Add("Stack!", false); - } - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - foreach (var bait in CurrentBaits) - bait.Shape.Draw(Arena, BaitOrigin(bait), bait.Rotation, ArenaColor.SafeFromAOE); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) { } -} +class IntoTheLight(BossModule module) : Components.LineStack(module, ActionID.MakeSpell(AID.IntoTheLight), ActionID.MakeSpell(AID.IntoTheLight2), 5.3f); class CatONineTails(BossModule module) : Components.GenericRotatingAOE(module) { diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D033Eros.cs b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D033Eros.cs index 4b27bcc0c9..9005fa5e7b 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D033Eros.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D03QitanaRavel/D033Eros.cs @@ -4,7 +4,7 @@ public enum OID : uint { Boss = 0x27B1, //R=7.02 Helper = 0x233C, //R=0.5 - PoisonVoidzone = 0x1E972C, + PoisonVoidzone = 0x1E972C } public enum AID : uint @@ -12,12 +12,12 @@ public enum AID : uint AutoAttack = 872, // Boss->player, no cast, single-target Rend = 15513, // Boss->player, 4.0s cast, single-target, tankbuster HoundOutOfHeaven = 15514, // Boss->self, 5.0s cast, single-target - HoundOutOfHeavenTetherStretchSuccess = 17079, // Boss->player, no cast, single-target, tether break success - HoundOutOfHeavenTetherStretchFail = 17080, // Boss->player, no cast, single-target, tether break fail + HoundOutOfHeavenSuccess = 17079, // Boss->player, no cast, single-target, tether stretch success + HoundOutOfHeavenFail = 17080, // Boss->player, no cast, single-target, tether stretch fail Glossolalia = 15515, // Boss->self, 3.0s cast, range 50 circle, raidwide ViperPoison = 15516, // Boss->self, 6.0s cast, single-target ViperPoisonPatterns = 15518, // Helper->location, 6.0s cast, range 6 circle - ViperPoisonBaitAway = 15517, // Helper->player, 6.0s cast, range 6 circle + ViperPoisonBait = 15517, // Helper->player, 6.0s cast, range 6 circle Jump = 15519, // Boss->location, no cast, single-target, visual? Inhale = 17168, // Boss->self, 4.0s cast, range 50 circle, attract 50 between centers HeavingBreath = 15520, // Boss->self, 3.5s cast, range 50 circle, knockback 35 forward @@ -28,153 +28,29 @@ public enum AID : uint ConfessionOfFaithRight = 15527, // Helper->self, 5.5s cast, range 60 41-degree cone ConfessionOfFaithStack = 15525, // Helper->players, 5.8s cast, range 6 circle, stack ConfessionOfFaithCenter = 15522, // Helper->self, 5.5s cast, range 60 40-degree cone - ConfessionOfFaithSpread = 15523, // Helper->player, 5.8s cast, range 5 circle, spread -} - -public enum IconID : uint -{ - tankbuster = 198, // player - stack = 62, // player - poisonbait = 171, // player - spread = 96, // player + ConfessionOfFaithSpread = 15523 // Helper->player, 5.8s cast, range 5 circle, spread } public enum TetherID : uint { HoundOutOfHeavenTetherGood = 1, // Boss->player - HoundOutOfHeavenTetherStretch = 57, // Boss->player -} - -class HoundOutOfHeavenGood(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeCone(0, 0.Degrees()), (uint)TetherID.HoundOutOfHeavenTetherGood) // TODO: consider generalizing stretched tethers? -{ - public List targets = []; - - public override void OnTethered(Actor source, ActorTetherInfo tether) - { - base.OnTethered(source, tether); - if (tether.ID == (uint)TetherID.HoundOutOfHeavenTetherGood) - targets.Add(WorldState.Actors.Find(tether.Target)!); - } - - public override void OnUntethered(Actor source, ActorTetherInfo tether) - { - base.OnUntethered(source, tether); - if (tether.ID == (uint)TetherID.HoundOutOfHeavenTetherGood) - targets.Remove(WorldState.Actors.Find(tether.Target)!); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) - { - if (targets.Contains(pc)) - { - foreach (var b in ActiveBaits) - { - if (Arena.Config.ShowOutlinesAndShadows) - Arena.AddLine(b.Source.Position, b.Target.Position, 0xFF000000, 2); - Arena.AddLine(b.Source.Position, b.Target.Position, ArenaColor.Safe); - } - } - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (targets.Contains(actor)) - hints.Add("Tether is stretched!", false); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - base.AddAIHints(slot, actor, assignment, hints); - if (targets.Contains(actor)) - hints.AddForbiddenZone(ShapeDistance.Circle(Module.PrimaryActor.Position, 15)); - } -} - -class HoundOutOfHeavenBad(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeCone(0, 0.Degrees()), (uint)TetherID.HoundOutOfHeavenTetherStretch) -{ - public List targets = []; - - public override void OnTethered(Actor source, ActorTetherInfo tether) - { - base.OnTethered(source, tether); - if (tether.ID == (uint)TetherID.HoundOutOfHeavenTetherStretch) - targets.Add(WorldState.Actors.Find(tether.Target)!); - } - - public override void OnUntethered(Actor source, ActorTetherInfo tether) - { - base.OnUntethered(source, tether); - if (tether.ID == (uint)TetherID.HoundOutOfHeavenTetherStretch) - targets.Remove(WorldState.Actors.Find(tether.Target)!); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) - { - if (targets.Contains(pc)) - { - foreach (var b in ActiveBaits) - { - if (Arena.Config.ShowOutlinesAndShadows) - Arena.AddLine(b.Source.Position, b.Target.Position, 0xFF000000, 2); - Arena.AddLine(b.Source.Position, b.Target.Position, ArenaColor.Danger); - } - } - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (targets.Contains(actor)) - hints.Add("Stretch tether further!"); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - base.AddAIHints(slot, actor, assignment, hints); - if (targets.Contains(actor)) - hints.AddForbiddenZone(ShapeDistance.Circle(Module.PrimaryActor.Position, 15)); - } + HoundOutOfHeavenTetherBad = 57 // Boss->player } +class HoundOutOfHeaven(BossModule module) : Components.StretchTetherDuo(module, (uint)TetherID.HoundOutOfHeavenTetherBad, (uint)TetherID.HoundOutOfHeavenTetherGood, 15, activationDelay: 5.2f); class ViperPoisonPatterns(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 6, ActionID.MakeSpell(AID.ViperPoisonPatterns), m => m.Enemies(OID.PoisonVoidzone).Where(z => z.EventState != 7), 0); class ConfessionOfFaithLeft(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ConfessionOfFaithLeft), new AOEShapeCone(60, 47.Degrees(), 20.Degrees())); // TODO: verify; there should not be an offset in reality here... class ConfessionOfFaithRight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ConfessionOfFaithRight), new AOEShapeCone(60, 47.Degrees(), -20.Degrees())); // TODO: verify; there should not be an offset in reality here... -class ConfessionOfFaithStack(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ConfessionOfFaithStack), 6); +class ConfessionOfFaithStack(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.ConfessionOfFaithStack), 6, 4); class ConfessionOfFaithCenter(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ConfessionOfFaithCenter), new AOEShapeCone(60, 40.Degrees())); class ConfessionOfFaithSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.ConfessionOfFaithSpread), 5); -class ViperPoisonBait(BossModule module) : Components.GenericBaitAway(module) +class ViperPoisonBait(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.ViperPoisonBait), new AOEShapeCircle(6), true) { - public List targets = []; - - public override void OnEventIcon(Actor actor, uint iconID) - { - if (iconID == (uint)IconID.poisonbait) - { - CurrentBaits.Add(new(actor, actor, new AOEShapeCircle(6))); - targets.Add(actor); - } - } - - public override void OnCastFinished(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID == AID.ViperPoisonBaitAway) - { - CurrentBaits.Clear(); - targets.Clear(); - } - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - base.AddHints(slot, actor, hints); - if (targets.Contains(actor)) - hints.Add("Bait voidzone away!"); - } - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { base.AddAIHints(slot, actor, assignment, hints); - if (targets.Contains(actor)) + if (ActiveBaits.Any(x => x.Target == actor)) hints.AddForbiddenZone(ShapeDistance.Rect(new(17, -518), new(17, -558), 13)); } } @@ -202,8 +78,7 @@ public D033ErosStates(BossModule module) : base(module) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs index 716adf9cc0..fb347c8d30 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs @@ -86,13 +86,10 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) if (NumCasts is 0 or 1) yield return new(rect, _casters[1].Position, default, _activation.AddSeconds(7.6f), ArenaColor.Danger); } - if (_casters.Count > 4) + if (_casters.Count > 4 && NumCasts is 0 or 1) { - if (NumCasts is 0 or 1) - { - yield return new(rect, _casters[2].Position, default, _activation.AddSeconds(8.1f)); - yield return new(rect, _casters[3].Position, default, _activation.AddSeconds(8.6f)); - } + yield return new(rect, _casters[2].Position, default, _activation.AddSeconds(8.1f)); + yield return new(rect, _casters[3].Position, default, _activation.AddSeconds(8.6f)); } if (_casters.Count > 4) { @@ -101,13 +98,10 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) if (NumCasts is 2 or 3) yield return new(rect, _casters[3].Position, default, _activation.AddSeconds(8.6f), ArenaColor.Danger); } - if (_casters.Count == 6) + if (_casters.Count == 6 && NumCasts is 2 or 3) { - if (NumCasts is 2 or 3) - { - yield return new(rect, _casters[4].Position, default, _activation.AddSeconds(9.1f)); - yield return new(rect, _casters[5].Position, default, _activation.AddSeconds(11.1f)); - } + yield return new(rect, _casters[4].Position, default, _activation.AddSeconds(9.1f)); + yield return new(rect, _casters[5].Position, default, _activation.AddSeconds(11.1f)); } if (_casters.Count == 6) { @@ -117,7 +111,7 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) yield return new(rect, _casters[5].Position, default, _activation.AddSeconds(11.1f), ArenaColor.Danger); } } - if (AreCastersInPositions(positionsSet3) || AreCastersInPositions(positionsSet4)) + else if (AreCastersInPositions(positionsSet3) || AreCastersInPositions(positionsSet4)) { if (_casters.Count > 2) { @@ -126,13 +120,10 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) if (NumCasts is 0 or 1) yield return new(rect, _casters[1].Position, default, _activation.AddSeconds(7.1f), ArenaColor.Danger); } - if (_casters.Count > 4) + if (_casters.Count > 4 && NumCasts is 0 or 1) { - if (NumCasts is 0 or 1) - { - yield return new(rect, _casters[2].Position, default, _activation.AddSeconds(8.1f)); - yield return new(rect, _casters[3].Position, default, _activation.AddSeconds(8.1f)); - } + yield return new(rect, _casters[2].Position, default, _activation.AddSeconds(8.1f)); + yield return new(rect, _casters[3].Position, default, _activation.AddSeconds(8.1f)); } if (_casters.Count > 4) { @@ -141,13 +132,10 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) if (NumCasts is 2 or 3) yield return new(rect, _casters[3].Position, default, _activation.AddSeconds(8.1f), ArenaColor.Danger); } - if (_casters.Count == 6) + if (_casters.Count == 6 && NumCasts is 2 or 3) { - if (NumCasts is 2 or 3) - { - yield return new(rect, _casters[4].Position, default, _activation.AddSeconds(11.1f)); - yield return new(rect, _casters[5].Position, default, _activation.AddSeconds(11.1f)); - } + yield return new(rect, _casters[4].Position, default, _activation.AddSeconds(11.1f)); + yield return new(rect, _casters[5].Position, default, _activation.AddSeconds(11.1f)); } if (_casters.Count == 6) { @@ -209,7 +197,7 @@ public override void OnActorEAnim(Actor actor, uint state) { if (state == 0x00040008) Module.Arena.Bounds = D055ForgivenObscenity.arenaRect; - if (state == 0x00010002) + else if (state == 0x00010002) { _aoe = null; Module.Arena.Bounds = D055ForgivenObscenity.arenaCircle; diff --git a/BossMod/Modules/Stormblood/Trial/T09Seiryu/BlueBolt.cs b/BossMod/Modules/Stormblood/Trial/T09Seiryu/BlueBolt.cs deleted file mode 100644 index 6a00753360..0000000000 --- a/BossMod/Modules/Stormblood/Trial/T09Seiryu/BlueBolt.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace BossMod.Stormblood.Trial.T09Seiryu; - -// TODO: create and use generic 'line stack' component -class BlueBolt(BossModule module) : Components.GenericBaitAway(module) -{ - private Actor? target; - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID.BlueBoltMarker) - { - target = WorldState.Actors.Find(spell.MainTargetID); - CurrentBaits.Add(new(Module.PrimaryActor, target!, new AOEShapeRect(83, 2.5f), Module.WorldState.FutureTime(5.9f))); - } - if ((AID)spell.Action.ID == AID.BlueBolt) - CurrentBaits.Clear(); - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (CurrentBaits.Count > 0 && actor != target && !Module.FindComponent()!.CurrentBaits.Any(x => x.Target == actor)) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 83, default, 2.5f)); - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (CurrentBaits.Count > 0 && !Module.FindComponent()!.CurrentBaits.Any(x => x.Target == actor)) - { - if (!actor.Position.InRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 83, default, 2.5f)) - hints.Add("Stack!"); - else - hints.Add("Stack!", false); - } - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - foreach (var bait in CurrentBaits) - bait.Shape.Draw(Arena, BaitOrigin(bait), bait.Rotation, ArenaColor.SafeFromAOE); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) { } -} diff --git a/BossMod/Modules/Stormblood/Trial/T09Seiryu/ForbiddenArts.cs b/BossMod/Modules/Stormblood/Trial/T09Seiryu/ForbiddenArts.cs deleted file mode 100644 index ffb293d7bf..0000000000 --- a/BossMod/Modules/Stormblood/Trial/T09Seiryu/ForbiddenArts.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace BossMod.Stormblood.Trial.T09Seiryu; - -// TODO: create and use generic 'line stack' component -class ForbiddenArts(BossModule module) : Components.GenericBaitAway(module) -{ - private Actor? target; - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if ((AID)spell.Action.ID == AID.ForbiddenArtsMarker) - { - target = WorldState.Actors.Find(spell.MainTargetID); - CurrentBaits.Add(new(Module.PrimaryActor, target!, new AOEShapeRect(84.4f, 4), Module.WorldState.FutureTime(5.2f))); - } - if ((AID)spell.Action.ID is AID.ForbiddenArts1 or AID.ForbiddenArts2) - { - if (++NumCasts == 2) - { - NumCasts = 0; - CurrentBaits.Clear(); - } - } - } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (CurrentBaits.Count > 0 && actor != target) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 84.4f, default, 4)); - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (CurrentBaits.Count > 0) - { - if (!actor.Position.InRect(Module.PrimaryActor.Position, (target!.Position - Module.PrimaryActor.Position).Normalized(), 84.4f, default, 4)) - hints.Add("Stack!"); - else - hints.Add("Stack!", false); - } - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - foreach (var bait in CurrentBaits) - bait.Shape.Draw(Arena, BaitOrigin(bait), bait.Rotation, ArenaColor.SafeFromAOE); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) { } -} diff --git a/BossMod/Modules/Stormblood/Trial/T09Seiryu/T09Seiryu.cs b/BossMod/Modules/Stormblood/Trial/T09Seiryu/T09Seiryu.cs index ab41b0289a..421ae2af18 100644 --- a/BossMod/Modules/Stormblood/Trial/T09Seiryu/T09Seiryu.cs +++ b/BossMod/Modules/Stormblood/Trial/T09Seiryu/T09Seiryu.cs @@ -37,7 +37,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } class ForceOfNature2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ForceOfNature2), new AOEShapeCircle(5)); -class KanaboBait(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeCone(45, 30.Degrees()), (uint)TetherID.BaitAway, ActionID.MakeSpell(AID.Kanabo1), (uint)OID.IwaNoShiki, 5.9f) +class KanaboBait(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeCone(45, 30.Degrees()), (uint)TetherID.BaitAway, ActionID.MakeSpell(AID.KanaboVisual2), (uint)OID.IwaNoShiki, 5.9f) { public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { @@ -47,10 +47,26 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } -class KanaboAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Kanabo1), new AOEShapeCone(45, 30.Degrees())); - +class KanaboAOE(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Kanabo2), new AOEShapeCone(45, 30.Degrees())); +class BlueBolt(BossModule module) : Components.LineStack(module, ActionID.MakeSpell(AID.BlueBoltMarker), ActionID.MakeSpell(AID.BlueBolt), 5.9f, 83, 2.5f); +class ForbiddenArts(BossModule module) : Components.LineStack(module, ActionID.MakeSpell(AID.ForbiddenArtsMarker), ActionID.MakeSpell(AID.ForbiddenArtsSecond), 5.2f, 84.4f, 4); // this hits twice class RedRush(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeRect(82.6f, 2.5f), (uint)TetherID.BaitAway, ActionID.MakeSpell(AID.RedRush), (uint)OID.AkaNoShiki, 6) { + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + base.OnTethered(source, tether); + var (player, enemy) = DetermineTetherSides(source, tether); + if (player != null && enemy != null) + Module.FindComponent()!.ForbiddenActors.Add(player); + } + + public override void OnUntethered(Actor source, ActorTetherInfo tether) + { + base.OnUntethered(source, tether); + var (player, enemy) = DetermineTetherSides(source, tether); + if (player != null && enemy != null) + Module.FindComponent()!.ForbiddenActors.Remove(player); + } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { base.AddAIHints(slot, actor, assignment, hints); @@ -86,4 +102,13 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { public static readonly ArenaBounds phase1Bounds = new ArenaBoundsCircle(19.5f); public static readonly ArenaBounds phase2Bounds = new ArenaBoundsCircle(20); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actor(PrimaryActor, ArenaColor.Enemy); + foreach (var s in Enemies(OID.DoroNoShiki)) + Arena.Actor(s, ArenaColor.Enemy); + foreach (var s in Enemies(OID.NumaNoShiki)) + Arena.Actor(s, ArenaColor.Enemy); + } } diff --git a/BossMod/Modules/Stormblood/Trial/T09Seiryu/T09SeiryuEnums.cs b/BossMod/Modules/Stormblood/Trial/T09Seiryu/T09SeiryuEnums.cs index 336e73ef71..e8631b299c 100644 --- a/BossMod/Modules/Stormblood/Trial/T09Seiryu/T09SeiryuEnums.cs +++ b/BossMod/Modules/Stormblood/Trial/T09Seiryu/T09SeiryuEnums.cs @@ -44,8 +44,8 @@ public enum AID : uint FifthElement = 14334, // Boss->self, 4.0s cast, range 100 circle ForbiddenArtsMarker = 14277, // Helper->player, no cast, single-target, applies linestack marker - ForbiddenArts1 = 15474, // Boss->self, no cast, range 80+R width 8 rect - ForbiddenArts2 = 15490, // Boss->self, no cast, range 80+R width 8 rect + ForbiddenArtsFirst = 15490, // Boss->self, no cast, range 80+R width 8 rect + ForbiddenArtsSecond = 15474, // Boss->self, no cast, range 80+R width 8 rect FortuneBladeSigil = 14342, // Helper->self, 6.5s cast, range 50+R width 4 rect @@ -54,8 +54,8 @@ public enum AID : uint GreatTyphoon40 = 14354, // Helper->self, 3.0s cast, range ?-40 donut, outside of arena InfirmSoul = 14333, // Boss->player, 5.0s cast, range 4 circle, tankbuster - KanaboVisual = 14316, // IwaNoShiki->location, no cast, ??? - Kanabo1 = 15392, // IwaNoShiki->self, 6.0s cast, single-target + KanaboVisual1 = 14316, // IwaNoShiki->location, no cast, ??? + KanaboVisual2 = 15392, // IwaNoShiki->self, 6.0s cast, single-target Kanabo2 = 15391, // IwaNoShiki->self, 3.0s cast, range 40+R 60-degree cone KujiKiri = 14305, // Boss->self, 4.0s cast, single-target @@ -93,15 +93,15 @@ public enum AID : uint ForceOfNature2 = 14345, // Helper->self, 5.0s cast, range 5 circle ForceOfNature3 = 14313, // YamaNoShiki->self, no cast, single-target - SummonShiki = 14285, // Boss->self, 3.0s cast, single-target + SummonShiki = 14285 // Boss->self, 3.0s cast, single-target } public enum IconID : uint { - Spreadmarker = 169, // player + Spreadmarker = 169 // player } public enum TetherID : uint { - BaitAway = 17, // AkaNoShiki/AoNoShiki/IwaNoShiki->player + BaitAway = 17 // AkaNoShiki/AoNoShiki/IwaNoShiki->player } diff --git a/BossMod/Replay/Analysis/AbilityInfo.cs b/BossMod/Replay/Analysis/AbilityInfo.cs index 07b3bcf90b..c6a63e4e24 100644 --- a/BossMod/Replay/Analysis/AbilityInfo.cs +++ b/BossMod/Replay/Analysis/AbilityInfo.cs @@ -1,7 +1,6 @@ using BossMod.Components; using ImGuiNET; using System.Globalization; -using System.Text; namespace BossMod.ReplayAnalysis; @@ -501,7 +500,7 @@ public void DrawContextMenu() private void AddActionData(Replay replay, Replay.Action action) { - if (action.Source.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo) + if (action.Source.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy) return; var data = _data.GetOrAdd(action.ID); @@ -510,7 +509,7 @@ private void AddActionData(Replay replay, Replay.Action action) data.TargetOIDs.Add(action.MainTarget.OID); data.SeenTargetSelf |= action.Source == action.MainTarget; data.SeenTargetOtherEnemy |= action.MainTarget != action.Source && action.MainTarget?.Type == ActorType.Enemy; - data.SeenTargetPlayer |= action.MainTarget?.Type == ActorType.Player; + data.SeenTargetPlayer |= action.MainTarget?.Type is ActorType.Player or ActorType.Buddy; data.SeenTargetLocation |= action.MainTarget == null; data.SeenAOE |= action.Targets.Count > 1; @@ -522,7 +521,7 @@ private void AddActionData(Replay replay, Replay.Action action) private void AddCastData(Replay replay, Replay.Participant caster, Replay.Cast cast) { - if (caster.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo) + if (caster.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy) return; var data = _data.GetOrAdd(cast.ID); @@ -531,7 +530,7 @@ private void AddCastData(Replay replay, Replay.Participant caster, Replay.Cast c data.TargetOIDs.Add(cast.Target.OID); data.SeenTargetSelf |= caster == cast.Target; data.SeenTargetOtherEnemy |= cast.Target != caster && cast.Target?.Type == ActorType.Enemy; - data.SeenTargetPlayer |= cast.Target?.Type == ActorType.Player; + data.SeenTargetPlayer |= cast.Target?.Type is ActorType.Player or ActorType.Buddy; data.SeenTargetLocation |= cast.Target == null; data.CastTime = cast.ExpectedCastTime + 0.3f; @@ -539,7 +538,7 @@ private void AddCastData(Replay replay, Replay.Participant caster, Replay.Cast c } private static IEnumerable AlivePlayersAt(Replay r, DateTime t) - => r.Participants.Where(p => p.Type is ActorType.Player or ActorType.Chocobo && p.ExistsInWorldAt(t) && !p.DeadAt(t)); + => r.Participants.Where(p => p.Type is ActorType.Player or ActorType.Buddy or ActorType.Chocobo && p.ExistsInWorldAt(t) && !p.DeadAt(t)); private IEnumerable ActionTargetStrings(ActionData data) { diff --git a/BossMod/Replay/Analysis/ParticipantInfo.cs b/BossMod/Replay/Analysis/ParticipantInfo.cs index 14381279c5..7f99cbcc11 100644 --- a/BossMod/Replay/Analysis/ParticipantInfo.cs +++ b/BossMod/Replay/Analysis/ParticipantInfo.cs @@ -147,7 +147,7 @@ private void DrawSubContextMenu(uint oid, ParticipantData data) } } - private static bool IsIgnored(Replay.Participant p) => p.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Area or ActorType.Treasure; + private static bool IsIgnored(Replay.Participant p) => p.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Area or ActorType.Treasure or ActorType.Buddy; private string RadiusString(ParticipantData d) => d.MinRadius != d.MaxRadius ? string.Create(CultureInfo.InvariantCulture, $"{d.MinRadius:f3}-{d.MaxRadius:f3}") : string.Create(CultureInfo.InvariantCulture, $"{d.MinRadius:f3}"); private string GuessName(uint oid, ParticipantData d) => Utils.StringToIdentifier(d.Names.Count > 0 ? d.Names[0].name : $"Actor{oid:X}"); diff --git a/BossMod/Replay/Analysis/StatusInfo.cs b/BossMod/Replay/Analysis/StatusInfo.cs index e63dc1ff56..dfb8af464a 100644 --- a/BossMod/Replay/Analysis/StatusInfo.cs +++ b/BossMod/Replay/Analysis/StatusInfo.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using System.Text; namespace BossMod.ReplayAnalysis; @@ -24,7 +23,7 @@ public StatusInfo(List replays, uint oid) { foreach (var enc in replay.Encounters.Where(enc => enc.OID == oid)) { - foreach (var status in replay.EncounterStatuses(enc).Where(s => !(s.Source?.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo) && !(s.Target.Type is ActorType.Pet or ActorType.Chocobo))) + foreach (var status in replay.EncounterStatuses(enc).Where(s => !(s.ID is 43 or 44 or 418) && !(s.Source?.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy) && !(s.Target.Type is ActorType.Pet or ActorType.Chocobo))) { var data = _data.GetOrAdd(status.ID); if (status.Source != null) diff --git a/BossMod/Replay/Visualization/ColumnEnemiesCastEvents.cs b/BossMod/Replay/Visualization/ColumnEnemiesCastEvents.cs index 1488eb1ee5..4037d318a5 100644 --- a/BossMod/Replay/Visualization/ColumnEnemiesCastEvents.cs +++ b/BossMod/Replay/Visualization/ColumnEnemiesCastEvents.cs @@ -21,7 +21,7 @@ public ColumnEnemiesCastEvents(Timeline timeline, StateMachineTree tree, List a.Source.Type is not (ActorType.Player or ActorType.Pet or ActorType.Chocobo)).ToList(); + _actions = replay.EncounterActions(enc).Where(a => a.Source.Type is not (ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy)).ToList(); foreach (var a in _actions) { var f = _filters.GetOrAdd(a.ID); @@ -97,7 +97,7 @@ private uint EventColor(Replay.Action action) { bool phys = false; bool magic = false; - foreach (var t in action.Targets.Where(t => t.Target.Type == ActorType.Player)) + foreach (var t in action.Targets.Where(t => t.Target.Type is ActorType.Player or ActorType.Buddy)) { foreach (var e in t.Effects.Where(e => e.Type is ActionEffectType.Damage or ActionEffectType.BlockedDamage or ActionEffectType.ParriedDamage)) { diff --git a/BossMod/Replay/Visualization/ColumnEnemiesDetails.cs b/BossMod/Replay/Visualization/ColumnEnemiesDetails.cs index 799a7621fe..4bbd9a3d85 100644 --- a/BossMod/Replay/Visualization/ColumnEnemiesDetails.cs +++ b/BossMod/Replay/Visualization/ColumnEnemiesDetails.cs @@ -27,7 +27,7 @@ public ColumnEnemiesDetails(Timeline timeline, StateMachineTree tree, List foreach (var (oid, participants) in enc.ParticipantsByOID) { var data = new PerOID() { OID = oid }; - foreach (var p in participants.Where(p => (p.HasAnyActions || p.Casts.Count > 0) && !(p.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo))) + foreach (var p in participants.Where(p => (p.HasAnyActions || p.Casts.Count > 0) && !(p.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy))) data.Columns.Add((p, null)); if (data.Columns.Count > 0) _data.Add(data); diff --git a/BossMod/Replay/Visualization/ColumnUtils.cs b/BossMod/Replay/Visualization/ColumnUtils.cs index 104c0893e7..1fd815d877 100644 --- a/BossMod/Replay/Visualization/ColumnUtils.cs +++ b/BossMod/Replay/Visualization/ColumnUtils.cs @@ -56,7 +56,7 @@ public static void AddCastTooltip(this ColumnGenericHistory.Entry entry, Replay. public static bool ActionHasDamageToPlayerEffects(Replay.Action action) { - return action.Targets.Any(t => t.Target.Type == ActorType.Player && t.Effects.Any(e => e.Type is ActionEffectType.Damage or ActionEffectType.BlockedDamage or ActionEffectType.ParriedDamage)); + return action.Targets.Any(t => t.Target.Type is ActorType.Player or ActorType.Buddy && t.Effects.Any(e => e.Type is ActionEffectType.Damage or ActionEffectType.BlockedDamage or ActionEffectType.ParriedDamage)); } } diff --git a/BossMod/Replay/Visualization/EventList.cs b/BossMod/Replay/Visualization/EventList.cs index 9137ed4a72..86c6dc2c3f 100644 --- a/BossMod/Replay/Visualization/EventList.cs +++ b/BossMod/Replay/Visualization/EventList.cs @@ -90,7 +90,7 @@ private void DrawContents(Replay.Encounter? filter, ModuleRegistry.Info? moduleI } bool haveActions = actions.Any(); - bool actionIsCrap(Replay.Action a) => a.Source.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo; + bool actionIsCrap(Replay.Action a) => a.Source.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy; foreach (var n in _tree.Node("Interesting actions", !haveActions)) { DrawActions(actions.Where(a => !actionIsCrap(a)), tp, aidType); @@ -101,7 +101,7 @@ private void DrawContents(Replay.Encounter? filter, ModuleRegistry.Info? moduleI } bool haveStatuses = statuses.Any(); - bool statusIsCrap(Replay.Status s) => s.Source?.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo || s.Target.Type is ActorType.Pet or ActorType.Chocobo; + bool statusIsCrap(Replay.Status s) => s.Source?.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy || s.Target.Type is ActorType.Pet or ActorType.Chocobo; foreach (var n in _tree.Node("Interesting statuses", !haveStatuses)) { DrawStatuses(statuses.Where(s => !statusIsCrap(s)), tp, sidType); diff --git a/BossMod/Replay/Visualization/OpList.cs b/BossMod/Replay/Visualization/OpList.cs index dc69ac35b6..8256e3874a 100644 --- a/BossMod/Replay/Visualization/OpList.cs +++ b/BossMod/Replay/Visualization/OpList.cs @@ -1,6 +1,5 @@ using ImGuiNET; using System.IO; -using System.Text; namespace BossMod.ReplayVisualization; @@ -71,16 +70,12 @@ private bool FilterInterestingActor(ulong instanceID, DateTime timestamp, bool a var p = replay.FindParticipant(instanceID, timestamp)!; if ((p.OwnerID & 0xFF000000) == 0x10000000) return false; // player's pet/area - if (p.Type == ActorType.Player && !allowPlayers) - return false; - if (_filteredOIDs.Contains(p.OID)) - return false; - return true; + return (p.Type is not ActorType.Player and not ActorType.Buddy and not ActorType.Pet || allowPlayers) && !_filteredOIDs.Contains(p.OID); } private bool FilterInterestingStatus(Replay.Status s) { - if (s.Source?.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo) + if (s.Source?.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy) return false; // don't care about statuses applied by players if (s.Target.Type is ActorType.Pet) return false; // don't care about statuses applied to pets @@ -92,6 +87,7 @@ private bool FilterInterestingStatus(Replay.Status s) return false; // don't care about filtered out statuses return true; } + private bool FilterInterestingStatuses(ulong instanceID, int index, DateTime timestamp) => FindStatuses(instanceID, index, timestamp).Any(FilterInterestingStatus); private bool FilterOp(WorldState.Operation o) @@ -115,6 +111,10 @@ private bool FilterOp(WorldState.Operation o) ActorState.OpEffectResult => false, ActorState.OpStatus op => FilterInterestingStatuses(op.InstanceID, op.Index, op.Timestamp), PartyState.OpLimitBreakChange => false, + ActorState.OpEventNpcYell op => FilterInterestingActor(op.InstanceID, op.Timestamp, false), + ActorState.OpEventObjectStateChange op => FilterInterestingActor(op.InstanceID, op.Timestamp, false), + ActorState.OpEventObjectAnimation op => FilterInterestingActor(op.InstanceID, op.Timestamp, false), + ActorState.OpRename op => FilterInterestingActor(op.InstanceID, op.Timestamp, false), ClientState.OpActionRequest => false, //ClientState.OpActionReject => false, ClientState.OpCooldown => false, diff --git a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs index 0d7ea78702..e184344999 100644 --- a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs +++ b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs @@ -218,7 +218,7 @@ private void DrawCommonColumns(Actor actor) foreach (var s in actor.Statuses.Where(s => s.ID != 0)) { var src = _player.WorldState.Actors.Find(s.SourceID); - if (src?.Type is ActorType.Player or ActorType.Pet) + if (src?.Type is ActorType.Player or ActorType.Pet or ActorType.Buddy) continue; if (s.ID is 360 or 362 or 364 or 365 or 413 or 902) continue; // skip FC buff diff --git a/BossMod/Util/ShapeDistance.cs b/BossMod/Util/ShapeDistance.cs index a5d0fcd684..4a1eec159e 100644 --- a/BossMod/Util/ShapeDistance.cs +++ b/BossMod/Util/ShapeDistance.cs @@ -79,6 +79,12 @@ public static Func DonutSector(WPos origin, float innerRadius, floa }; } + public static Func InvertedDonutSector(WPos origin, float innerRadius, float outerRadius, Angle centerDir, Angle halfAngle) + { + var donutSectir = DonutSector(origin, innerRadius, outerRadius, centerDir, halfAngle); + return p => -donutSectir(p); + } + public static Func Tri(WPos origin, RelTriangle tri) { var ab = tri.B - tri.A; @@ -104,6 +110,13 @@ public static Func Tri(WPos origin, RelTriangle tri) return Math.Max(Math.Max(d1, d2), d3); }; } + + public static Func InvertedTri(WPos origin, RelTriangle tri) + { + var triangle = Tri(origin, tri); + return p => -triangle(p); + } + public static Func TriList(WPos origin, List tris) => Union([.. tris.Select(tri => Tri(origin, tri))]); public static Func Rect(WPos origin, WDir dir, float lenFront, float lenBack, float halfWidth) @@ -166,6 +179,12 @@ public static Func Cross(WPos origin, Angle direction, float length }; } + public static Func InvertedCross(WPos origin, Angle direction, float length, float halfWidth) + { + var cross = Cross(origin, direction, length, halfWidth); + return p => -cross(p); + } + // positive offset increases area public static Func ConvexPolygon(IEnumerable<(WPos, WPos)> edges, bool cw, float offset = 0) {