diff --git a/BossMod/BossModule/AOEShapes.cs b/BossMod/BossModule/AOEShapes.cs index 7ba7c8a0e7..38249c10f3 100644 --- a/BossMod/BossModule/AOEShapes.cs +++ b/BossMod/BossModule/AOEShapes.cs @@ -120,3 +120,138 @@ public sealed record class AOEShapeTriCone(float SideLength, Angle HalfAngle, An 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 sealed record class AOEShapeCustom(List UnionShapes, List DifferenceShapes) : AOEShape +{ + private static readonly Dictionary> _triangulationCache = []; + private static readonly Dictionary _polygonCacheStatic = []; + private readonly Dictionary _polygonCache = []; + private static readonly Dictionary<(string, WPos, Angle), Func> _distanceFuncCache = []; + + private RelSimplifiedComplexPolygon GetCombinedPolygon(WPos origin) + { + var cacheKey = CreateCacheKey(UnionShapes, DifferenceShapes); + if (_polygonCacheStatic.TryGetValue(cacheKey, out var cachedResult)) + return (RelSimplifiedComplexPolygon)cachedResult; + + var unionOperands = new PolygonClipper.Operand(); + foreach (var shape in UnionShapes) + unionOperands.AddPolygon(shape.ToPolygon(origin)); + + var differenceOperands = new PolygonClipper.Operand(); + foreach (var shape in DifferenceShapes) + differenceOperands.AddPolygon(shape.ToPolygon(origin)); + + var clipper = new PolygonClipper(); + var finalResult = clipper.Difference(unionOperands, differenceOperands); + + _polygonCacheStatic[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 combinedPolygon = GetCombinedPolygon(origin); + var relativePosition = position - origin; + var result = combinedPolygon.Contains(new WDir(relativePosition.X, relativePosition.Z)); + _polygonCache[cacheKey] = result; + return result; + } + + private static string CreateCacheKey(IEnumerable UnionShapes, IEnumerable DifferenceShapes) + { + using var sha512 = SHA512.Create(); + var unionKey = string.Join(",", UnionShapes.Select(s => ComputeShapeHash(s, sha512))); + var differenceKey = string.Join(",", DifferenceShapes.Select(s => ComputeShapeHash(s, sha512))); + return $"{unionKey}|{differenceKey}"; + } + + private static string ComputeShapeHash(Shape shape, SHA512 sha512) + { + var data = Encoding.UTF8.GetBytes(shape.ToString()); + var hash = sha512.ComputeHash(data); + return BitConverter.ToString(hash).Replace("-", "", StringComparison.Ordinal); + } + + public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) + { + var cacheKey = CreateCacheKey(UnionShapes, DifferenceShapes); + if (!_triangulationCache.TryGetValue(cacheKey, out var triangles)) + { + var combinedPolygon = GetCombinedPolygon(origin); + triangles = combinedPolygon.Triangulate(); + _triangulationCache[cacheKey] = triangles; + } + + foreach (var triangle in triangles) + { + arena.ZoneTri(origin + triangle.A, origin + triangle.B, origin + triangle.C, color); // probably not very efficient to split the polygon into triangles, there must be a better way + } + } + + public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.Danger) + { + var combinedPolygon = GetCombinedPolygon(origin); + foreach (var part in combinedPolygon.Parts) + { + foreach (var (start, end) in part.ExteriorEdges) + { + arena.PathLineTo(origin + start); + arena.PathLineTo(origin + end); + } + MiniArena.PathStroke(true, color); + foreach (var holeIndex in part.Holes) + { + foreach (var (start, end) in part.InteriorEdges(holeIndex)) + { + arena.PathLineTo(origin + start); + arena.PathLineTo(origin + end); + } + MiniArena.PathStroke(true, color); + } + } + } + + public override Func Distance(WPos origin, Angle rotation) // probably not a very efficient way, will make AI pathfinding lag + { + var funcCacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes), origin, rotation); + if (_distanceFuncCache.TryGetValue(funcCacheKey, out var cachedFunc)) + return cachedFunc; + + float distanceFunc(WPos position) + { + var combinedPolygon = GetCombinedPolygon(origin); + var relativePosition = position - origin; + var distances = new List(); + var inside = combinedPolygon.Contains(relativePosition); + + foreach (var part in combinedPolygon.Parts) + { + distances.AddRange(part.ExteriorEdges.Select(edge => DistanceToEdge(relativePosition, edge))); + foreach (var holeIndex in part.Holes) + distances.AddRange(part.InteriorEdges(holeIndex).Select(edge => DistanceToEdge(relativePosition, edge))); + } + + var minDistance = distances.Min(); + var finalDistance = inside ? -minDistance : minDistance; + return finalDistance; + } + + _distanceFuncCache[funcCacheKey] = distanceFunc; + return distanceFunc; + } + + private float DistanceToEdge(WDir point, (WDir, WDir) edge) + { + var (a, b) = edge; + var ab = b - a; + var ap = point - a; + var abLengthSquared = ab.LengthSq(); + var t = Math.Clamp(Vector2.Dot(ap.ToVec2(), ab.ToVec2()) / abLengthSquared, 0, 1); + var projection = a + ab * t; + return (point - projection).Length(); + } +} diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index 7d63e411dc..c862338d65 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -357,18 +357,11 @@ private static (WPos Center, float Radius, RelSimplifiedComplexPolygon Poly) Cal private static string CreateCacheKey(IEnumerable unionShapes, IEnumerable differenceShapes, IEnumerable additionalShapes) { - using var sha512 = SHA512.Create(); - var unionKey = string.Join(",", unionShapes.Select(s => ComputeShapeHash(s, sha512))); - var differenceKey = string.Join(",", differenceShapes.Select(s => ComputeShapeHash(s, sha512))); - var additionalKey = string.Join(",", additionalShapes.Select(s => ComputeShapeHash(s, sha512))); - return $"{unionKey}|{differenceKey}|{additionalKey}"; - } - - private static string ComputeShapeHash(Shape shape, SHA512 sha512) - { - var data = Encoding.UTF8.GetBytes(shape.ToString()); - var hash = sha512.ComputeHash(data); - return BitConverter.ToString(hash).Replace("-", "", StringComparison.Ordinal); + var unionKey = string.Join(",", unionShapes.Select(s => s.ComputeHash())); + var differenceKey = string.Join(",", differenceShapes.Select(s => s.ComputeHash())); + var additionalKey = string.Join(",", additionalShapes.Select(s => s.ComputeHash())); + var combinedKey = $"{unionKey}|{differenceKey}|{additionalKey}"; + return Shape.ComputeSHA512(combinedKey); } private static RelSimplifiedComplexPolygon CombinePolygons(List unionPolygons, List differencePolygons, List secondUnionPolygons) diff --git a/BossMod/BossModule/Shapes.cs b/BossMod/BossModule/Shapes.cs index f98fc19bdc..eefb7ea175 100644 --- a/BossMod/BossModule/Shapes.cs +++ b/BossMod/BossModule/Shapes.cs @@ -5,19 +5,29 @@ public abstract record class Shape public static readonly Dictionary StaticCache = []; public abstract RelSimplifiedComplexPolygon ToPolygon(WPos center); public const float MaxApproxError = 0.01f; + public abstract string ComputeHash(); + + public static string ComputeSHA512(string input) + { + var bytes = Encoding.UTF8.GetBytes(input); + var hash = SHA512.HashData(bytes); + return BitConverter.ToString(hash).Replace("-", "", StringComparison.Ordinal); + } } public record class Circle(WPos Center, float Radius) : Shape { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, Radius, typeof(Circle)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var vertices = CurveApprox.Circle(Radius, MaxApproxError).Select(p => p + (Center - center)).ToList(); var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(vertices)]); - StaticCache[(Center, center, Radius, typeof(Circle))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(Circle)}:{Center.X},{Center.Z},{Radius}"); } // for custom polygons defined by a list of vertices @@ -25,26 +35,34 @@ public record class PolygonCustom(IEnumerable Vertices) : Shape { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((center, Vertices, typeof(PolygonCustom)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var relativeVertices = Vertices.Select(v => v - center).ToList(); var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(relativeVertices)]); - StaticCache[(center, Vertices, typeof(PolygonCustom))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() + { + var verticesHash = string.Join(",", Vertices.Select(v => $"{v.X},{v.Z}")); + return ComputeSHA512($"{nameof(PolygonCustom)}:{verticesHash}"); + } } public record class Donut(WPos Center, float InnerRadius, float OuterRadius) : Shape { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, InnerRadius, OuterRadius, typeof(Donut)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var vertices = CurveApprox.Donut(InnerRadius, OuterRadius, MaxApproxError).Select(p => p + (Center - center)).ToList(); var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(vertices)]); - StaticCache[(Center, center, InnerRadius, OuterRadius, typeof(Donut))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(Donut)}:{Center.X},{Center.Z},{InnerRadius},{OuterRadius}"); } // for rectangles defined by a center, halfwidth, halfheight and optionally rotation @@ -52,7 +70,7 @@ public record class Rectangle(WPos Center, float HalfWidth, float HalfHeight, An { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, HalfWidth, HalfHeight, Rotation, typeof(Rectangle)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var cos = MathF.Cos(Rotation.Rad); var sin = MathF.Sin(Rotation.Rad); @@ -64,9 +82,11 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) new WDir(-HalfWidth * cos - HalfHeight * sin, -HalfWidth * sin + HalfHeight * cos) + (Center - center) }; var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(vertices)]); - StaticCache[(Center, center, HalfWidth, HalfHeight, Rotation, typeof(Rectangle))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(Rectangle)}:{Center.X},{Center.Z},{HalfWidth},{HalfHeight},{Rotation.Rad}"); } // for rectangles defined by a start point, end point and halfwidth @@ -76,13 +96,14 @@ public record class RectangleSE(WPos Start, WPos End, float HalfWidth) : Rectang HalfHeight: (End - Start).Length() / 2, Rotation: new Angle(MathF.Atan2(End.Z - Start.Z, End.X - Start.X)) + 90.Degrees() ); + public record class Square(WPos Center, float HalfSize, Angle Rotation = default) : Rectangle(Center, HalfSize, HalfSize, Rotation); public record class Cross(WPos Center, float Length, float HalfWidth, Angle Rotation = default) : Shape { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, Length, HalfWidth, Rotation, typeof(Cross)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var cos = MathF.Cos(Rotation.Rad); var sin = MathF.Sin(Rotation.Rad); @@ -109,9 +130,11 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) new(horizontalVertices) }; var result = new RelSimplifiedComplexPolygon(polygons); - StaticCache[(Center, center, Length, HalfWidth, Rotation, typeof(Cross))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(Cross)}:{Center.X},{Center.Z},{Length},{HalfWidth},{Rotation.Rad}"); } // Equilateral triangle defined by center, radius and rotation @@ -119,7 +142,7 @@ public record class TriangleE(WPos Center, float Radius, Angle Rotation = defaul { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, Radius, Rotation, typeof(TriangleE)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var sqrt3 = MathF.Sqrt(3); @@ -139,9 +162,11 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) var rotatedVertices = vertices.Select(v => new WDir(v.X * cos - v.Z * sin, v.X * sin + v.Z * cos)).ToList(); var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(rotatedVertices)]); - StaticCache[(Center, center, Radius, Rotation, typeof(TriangleE))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(TriangleE)}:{Center.X},{Center.Z},{Radius},{Rotation.Rad}"); } // custom Triangle defined by three sides and rotation, mind the triangle inequality, side1 + side2 >= side3 @@ -149,7 +174,7 @@ public record class TriangleS(WPos Center, float SideA, float SideB, float SideC { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, SideA, SideB, SideC, Rotation, typeof(TriangleS)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var sides = new[] { SideA, SideB, SideC }.OrderByDescending(s => s).ToArray(); @@ -176,9 +201,11 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) var adjustedVertices = rotatedVertices.Select(v => v + (Center - center)).ToList(); var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(adjustedVertices)]); - StaticCache[(Center, center, SideA, SideB, SideC, Rotation, typeof(TriangleS))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(TriangleS)}:{Center.X},{Center.Z},{SideA},{SideB},{SideC},{Rotation.Rad}"); } // Triangle definded by base length and angle at the apex, apex points north by default @@ -186,7 +213,7 @@ public record class TriangleA(WPos Center, float BaseLength, Angle ApexAngle, An { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, BaseLength, ApexAngle, Rotation, typeof(TriangleA)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var apexAngleRad = ApexAngle.Rad; @@ -212,9 +239,11 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) var adjustedVertices = rotatedVertices.Select(v => v + (Center - center)).ToList(); var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(adjustedVertices)]); - StaticCache[(Center, center, BaseLength, ApexAngle, Rotation, typeof(TriangleA))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(TriangleA)}:{Center.X},{Center.Z},{BaseLength},{ApexAngle.Rad},{Rotation.Rad}"); } // for polygons defined by a radius and n amount of vertices @@ -222,7 +251,7 @@ public record class Polygon(WPos Center, float Radius, int Vertices, Angle Rotat { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, Radius, Vertices, Rotation, typeof(Polygon)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var angleIncrement = 2 * MathF.PI / Vertices; @@ -238,35 +267,41 @@ public override RelSimplifiedComplexPolygon ToPolygon(WPos center) } var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(vertices)]); - StaticCache[(Center, center, Radius, Vertices, Rotation, typeof(Polygon))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(Polygon)}:{Center.X},{Center.Z},{Radius},{Vertices},{Rotation.Rad}"); } public record class Cone(WPos Center, float Radius, Angle StartAngle, Angle EndAngle) : Shape { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, Radius, StartAngle, EndAngle, typeof(Cone)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var vertices = CurveApprox.CircleSector(Center, Radius, StartAngle, EndAngle, MaxApproxError).Select(p => p - center).ToList(); var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(vertices)]); - StaticCache[(Center, center, Radius, StartAngle, EndAngle, typeof(Cone))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(Cone)}:{Center.X},{Center.Z},{Radius},{StartAngle.Rad},{EndAngle.Rad}"); } public record class DonutSegment(WPos Center, float InnerRadius, float OuterRadius, Angle StartAngle, Angle EndAngle) : Shape { public override RelSimplifiedComplexPolygon ToPolygon(WPos center) { - if (StaticCache.TryGetValue((Center, center, InnerRadius, OuterRadius, StartAngle, EndAngle, typeof(DonutSegment)), out var cachedResult)) + if (StaticCache.TryGetValue((ComputeHash(), center), out var cachedResult)) return cachedResult; var vertices = CurveApprox.DonutSector(InnerRadius, OuterRadius, StartAngle, EndAngle, MaxApproxError).Select(p => p + (Center - center)).ToList(); var result = new RelSimplifiedComplexPolygon([new RelPolygonWithHoles(vertices)]); - StaticCache[(Center, center, InnerRadius, OuterRadius, StartAngle, EndAngle, typeof(DonutSegment))] = result; + StaticCache[(ComputeHash(), center)] = result; return result; } + + public override string ComputeHash() => ComputeSHA512($"{nameof(DonutSegment)}:{Center.X},{Center.Z},{InnerRadius},{OuterRadius},{StartAngle.Rad},{EndAngle.Rad}"); } diff --git a/BossMod/Modules/Endwalker/Alliance/A23Halone/A23Halone.cs b/BossMod/Modules/Endwalker/Alliance/A23Halone/A23Halone.cs index 47137dca9f..5043fc722b 100644 --- a/BossMod/Modules/Endwalker/Alliance/A23Halone/A23Halone.cs +++ b/BossMod/Modules/Endwalker/Alliance/A23Halone/A23Halone.cs @@ -11,4 +11,4 @@ class IceRondel(BossModule module) : Components.StackWithCastTargets(module, Act class Niphas(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Niphas), new AOEShapeCircle(9)); [ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "veyn, Malediktus", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 911, NameID = 12064)] -public class A23Halone(WorldState ws, Actor primary) : BossModule(ws, primary, new(-700, 600), new ArenaBoundsCircle(30)); +public class A23Halone(WorldState ws, Actor primary) : BossModule(ws, primary, new(-700, 600), Octagons.arenaDefault); diff --git a/BossMod/Modules/Endwalker/Alliance/A23Halone/A23HaloneStates.cs b/BossMod/Modules/Endwalker/Alliance/A23Halone/A23HaloneStates.cs index 9e431e2520..8b1200c950 100644 --- a/BossMod/Modules/Endwalker/Alliance/A23Halone/A23HaloneStates.cs +++ b/BossMod/Modules/Endwalker/Alliance/A23Halone/A23HaloneStates.cs @@ -184,6 +184,7 @@ private void AddPhase(uint id, float delay) .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit() + .OnExit(() => Module.Arena.Bounds = Octagons.arenaDefault) .SetHint(StateMachine.StateHint.DowntimeStart); ComponentCondition(id + 0x200, 8.7f, comp => comp.NumCasts > 0, "Raidwide", 10) // TODO: these timings differ a lot, depending on whether large is killed last?.. diff --git a/BossMod/Modules/Endwalker/Alliance/A23Halone/Octagons.cs b/BossMod/Modules/Endwalker/Alliance/A23Halone/Octagons.cs index 7604918c11..0f3a047117 100644 --- a/BossMod/Modules/Endwalker/Alliance/A23Halone/Octagons.cs +++ b/BossMod/Modules/Endwalker/Alliance/A23Halone/Octagons.cs @@ -4,75 +4,74 @@ namespace BossMod.Endwalker.Alliance.A23Halone; // NW (Octagon3): Alliance A // NE (Octagon1): Alliance C // S (Octagon2): Alliance B -class Octagons(BossModule module) : BossComponent(module) +class Octagons(BossModule module) : Components.GenericAOEs(module) { + private ArenaBounds? _arena; + private const float InnerRadius = 11.5f; + private const float OuterRadius = 12.5f; + private const int Vertices = 8; private static readonly WPos[] spears = [new(-686, 592), new(-700, 616.2f), new(-714, 592)]; + private static readonly Angle[] angle = [37.5f.Degrees(), 22.5f.Degrees(), -37.5f.Degrees()]; + private static readonly List shapes = [new Polygon(spears[0], InnerRadius, Vertices, angle[0]), + new Polygon(spears[0], OuterRadius, Vertices, angle[0]), new Polygon(spears[1], InnerRadius, Vertices, angle[1]), + new Polygon(spears[1], OuterRadius, Vertices, angle[1]), new Polygon(spears[2], InnerRadius, Vertices, angle[2]), + new Polygon(spears[2], OuterRadius, Vertices, angle[2])]; + private static readonly List baseArena = [new Circle(new WPos(-700, 600), 30)]; + public readonly List octagonsInner = []; + public readonly List octagonsOuter = []; + public static readonly ArenaBounds arenaDefault = new ArenaBoundsCircle(30); + public AOEInstance? _aoe; + public static readonly AOEShapeCustom customShape = new(baseArena, [shapes[0], shapes[2], shapes[4]]); - private static IEnumerable Octagon1() - { - for (int i = 0; i < 9; ++i) - yield return Helpers.RotateAroundOrigin(37.5f + i * 45, spears[0], new(spears[0].X + 11.5f, spears[0].Z)); - } + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); - private static IEnumerable Octagon2() + public override void OnEventEnvControl(byte index, uint state) { - for (int i = 0; i < 9; ++i) - yield return Helpers.RotateAroundOrigin(22.5f + i * 45, spears[1], new(spears[1].X + 11.5f, spears[1].Z)); + //x07 = south, x06 = east, x05 = west x00020001 walls activate, x00200004 disappear + // telegraph - 0x00100008 + switch (state) + { + case 0x00100008 when index == 0x07: + _aoe = new(customShape, Module.Arena.Center); + break; + case 0x00020001 when index == 0x07: + AddOctagons(); + _aoe = null; + break; + case 0x00200004: + RemoveOctagons(index); + break; + } + _arena = new ArenaBoundsComplex(baseArena, octagonsOuter, octagonsInner); + Module.Arena.Bounds = _arena; } - private static IEnumerable Octagon3() + private void AddOctagons() { - for (int i = 0; i < 9; ++i) - yield return Helpers.RotateAroundOrigin(-37.5f + i * 45, spears[2], new(spears[2].X + 11.5f, spears[2].Z)); + octagonsInner.AddRange([shapes[0], shapes[2], shapes[4]]); + octagonsOuter.AddRange([shapes[1], shapes[3], shapes[5]]); } - public override void DrawArenaForeground(int pcSlot, Actor pc) + private void RemoveOctagons(byte index) { - if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[0], 1) && !x.IsDead)) - foreach (var c in Octagon1()) - Arena.PathLineTo(c); - MiniArena.PathStroke(false, ArenaColor.Border, 2); - if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[1], 1) && !x.IsDead)) - foreach (var c in Octagon2()) - Arena.PathLineTo(c); - MiniArena.PathStroke(false, ArenaColor.Border, 2); - if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[2], 1) && !x.IsDead)) - foreach (var c in Octagon3()) - Arena.PathLineTo(c); - MiniArena.PathStroke(false, ArenaColor.Border, 2); + switch (index) + { + case 0x06: + octagonsInner.Remove(shapes[0]); + octagonsOuter.Remove(shapes[1]); + break; + case 0x07: + octagonsInner.Remove(shapes[2]); + octagonsOuter.Remove(shapes[3]); + break; + case 0x05: + octagonsInner.Remove(shapes[4]); + octagonsOuter.Remove(shapes[5]); + break; + } } - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - var octagons = new List>(); //inverted octagons - var octagons2 = new List>(); //octagons - - base.AddAIHints(slot, actor, assignment, hints); - - if (Module.Enemies(OID.GlacialSpearSmall).Count(x => !x.IsDead) == 3) - { - octagons.Add(ShapeDistance.InvertedConvexPolygon(Octagon1(), true)); - octagons.Add(ShapeDistance.InvertedConvexPolygon(Octagon2(), true)); - octagons.Add(ShapeDistance.InvertedConvexPolygon(Octagon3(), true)); - } - else if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[0], 1) && !x.IsDead) && actor.Position.InConvexPolygon(Octagon1())) - octagons.Add(ShapeDistance.InvertedConvexPolygon(Octagon1(), true)); - else if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[1], 1) && !x.IsDead) && actor.Position.InConvexPolygon(Octagon2())) - octagons.Add(ShapeDistance.InvertedConvexPolygon(Octagon2(), true)); - else if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[2], 1) && !x.IsDead) && actor.Position.InConvexPolygon(Octagon3())) - octagons.Add(ShapeDistance.InvertedConvexPolygon(Octagon3(), true)); - if (octagons.Count == 0) - { - if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[0], 1) && !x.IsDead) && !actor.Position.InConvexPolygon(Octagon1())) - octagons2.Add(ShapeDistance.ConvexPolygon(Octagon1(), true)); - if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[1], 1) && !x.IsDead) && !actor.Position.InConvexPolygon(Octagon2())) - octagons2.Add(ShapeDistance.ConvexPolygon(Octagon2(), true)); - if (Module.Enemies(OID.GlacialSpearSmall).Any(x => x.Position.AlmostEqual(spears[2], 1) && !x.IsDead) && !actor.Position.InConvexPolygon(Octagon3())) - octagons2.Add(ShapeDistance.ConvexPolygon(Octagon3(), true)); - } - if (octagons.Count > 0) - hints.AddForbiddenZone(p => octagons.Select(f => f(p)).Max()); - if (octagons2.Count > 0) - hints.AddForbiddenZone(p => octagons2.Select(f => f(p)).Min()); + //TODO fix AI map creation } } diff --git a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntilon.cs b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntilon.cs index 62326e4d50..921f502d03 100644 --- a/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntilon.cs +++ b/BossMod/Modules/Endwalker/Dungeon/D13LunarSubterrane/D132DamcyanAntilon.cs @@ -115,7 +115,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) if ((AID)spell.Action.ID == AID.AntilonMarchTelegraph) { var dir = spell.LocXZ - caster.Position; - _aoes.Add(new(new AOEShapeRect(dir.Length(), 4), caster.Position, Angle.FromDirection(dir))); + _aoes.Add(new(new AOEShapeRect(dir.Length(), 4.5f), caster.Position, Angle.FromDirection(dir))); // actual charge is only 4 halfwidth, but the telegraphs and actual AOEs can be in different positions by upto 0.5y according to my logs } if ((AID)spell.Action.ID == AID.AntlionMarch) _activation = spell.NPCFinishAt.AddSeconds(0.2f); //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 @@ -204,7 +204,8 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (!Module.InBounds(actor.Position)) // return into module bounds if accidently left bounds hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); else if (!Module.FindComponent()!.Sources(slot, actor).Any() && !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && - !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.Stacks.Count == 0) + !Module.FindComponent()!.ActiveAOEs(slot, actor).Any() && Module.FindComponent()!.Stacks.Count == 0 && + !Module.FindComponent()!.TowerDanger) if (actor.Role is Role.Melee or Role.Tank) hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.PrimaryActor.Position, Module.PrimaryActor.HitboxRadius + 3)); } diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D052ForgivenApathy.cs b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D052ForgivenApathy.cs index 3c9ae44b92..b069148e06 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D052ForgivenApathy.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D052ForgivenApathy.cs @@ -41,7 +41,7 @@ public D052ForgivenApathyStates(BossModule module) : base(module) [ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "Malediktus", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 659, NameID = 8267)] public class D052ForgivenApathy : BossModule { - public D052ForgivenApathy(WorldState ws, Actor primary) : base(ws, primary, primary.Position.X > -100 ? arena1.Center : arena2.Center, primary.Position.X > -100 ? arena1 : arena2) + public D052ForgivenApathy(WorldState ws, Actor primary) : base(ws, primary, primary.Position.Z > -106 ? arena2.Center : arena1.Center, primary.Position.Z > -106 ? arena2 : arena1) { ForgivenPrejudice = Enemies(OID.ForgivenPrejudice); ForgivenExtortion = Enemies(OID.ForgivenExtortion); @@ -51,6 +51,7 @@ public D052ForgivenApathy(WorldState ws, Actor primary) : base(ws, primary, prim private static readonly List arenacoords1 = [new(21, -215.1f), new(16.3f, -213.2f), new(9.8f, -208.8f), new(4.4f, -206.3f), new(0.2f, -204.8f), new(-9.6f, -202.8f), new(-10, -202.5f), new(-10.7f, -201.9f), new(-11.5f, -201.4f), new(-13.2f, -200.9f), new(-8.1f, -186.8f), new(-3.7f, -188.5f), new(-1.9f, -188.7f), new(2.5f, -190f), new(9.3f, -193.5f), new(18.8f, -198.8f), new(27.1f, -203.2f)]; + private static readonly List arenacoords2 = [new(-176, -138.1f), new(-199.1f, -124.8f), new(-197.3f, -121.5f), new(-204.15f, -117.3f), new(-204f, -116.1f), new(-205, -114.5f), new(-205.3f, -114.3f), new(-205.2f, -112.4f), new(-205.5f, -111.9f), new(-206.6f, -111f), new(-207f, -110.6f), new(-198.8f, -98.4f), new(-190, -103.5f), new(-190, -104.5f), new(-187.1f, -106.1f), new(-186.3f, -105.7f), new(-177.1f, -111.1f), new(-177.2f, -111.7f), new(-174, -113.6f), new(-173.3f, -113.2f), new(-164.1f, -118.5f)]; diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs index ff74a2c7e6..4be8bf1757 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D055ForgivenObscenity.cs @@ -62,11 +62,22 @@ class GoldChaser(BossModule module) : Components.GenericAOEs(module) { private DateTime _activation; private readonly List _casters = []; - private static readonly AOEShapeRect rect = new(100, 2.5f, 100); + private static readonly AOEShapeRect rect = new(100, 2.53f, 100); // halfwidth is 2.5, but +0.03 safety margin because ring position doesn't seem to be exactly caster position + private static readonly List positionsSet1 = [new WPos(-227.5f, 253), new WPos(-232.5f, 251.5f)]; + private static readonly List positionsSet2 = [new WPos(-252.5f, 253), new WPos(-247.5f, 251.5f)]; + private static readonly List positionsSet3 = [new WPos(-242.5f, 253), new WPos(-237.5f, 253)]; + private static readonly List positionsSet4 = [new WPos(-252.5f, 253), new WPos(-227.5f, 253)]; + + private bool AreCastersInPositions(List positions) + { + return _casters.Count >= 2 && positions.Count == 2 && + (_casters[0].Position == positions[0] && _casters[1].Position == positions[1] || + _casters[0].Position == positions[1] && _casters[1].Position == positions[0]); + } public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_casters.Count > 1 && (_casters[0].Position.AlmostEqual(new(-227.5f, 253), 1) && _casters[1].Position.AlmostEqual(new(-232.5f, 251.5f), 1) || _casters[0].Position.AlmostEqual(new(-252.5f, 253), 1) && _casters[1].Position.AlmostEqual(new(-247.5f, 251.5f), 1))) + if (AreCastersInPositions(positionsSet1) || AreCastersInPositions(positionsSet2)) { if (_casters.Count > 2) { @@ -106,7 +117,7 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) yield return new(rect, _casters[5].Position, default, _activation.AddSeconds(11.1f), ArenaColor.Danger); } } - if (_casters.Count > 1 && (_casters[0].Position.AlmostEqual(new(-242.5f, 253), 1) && _casters[1].Position.AlmostEqual(new(-237.5f, 253), 1) || _casters[0].Position.AlmostEqual(new(-252.5f, 253), 1) && _casters[1].Position.AlmostEqual(new(-227.5f, 253), 1))) + if (AreCastersInPositions(positionsSet3) || AreCastersInPositions(positionsSet4)) { if (_casters.Count > 2) { @@ -158,6 +169,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) { if ((AID)spell.Action.ID == AID.Ringsmith) _activation = WorldState.CurrentTime; + if ((AID)spell.Action.ID == AID.VenaAmoris) { if (++NumCasts == 6)