From 319ad6a15178eadfcada8183a3be35b80768fbf5 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:12:31 +0200 Subject: [PATCH] ArenaBoundsDifference, AOE caching, hydaelyn improvements --- BossMod/BossModule/ArenaBounds.cs | 444 +++++++++++++++--- BossMod/BossModule/MiniArena.cs | 55 ++- BossMod/Modules/DemoModule.cs | 11 +- .../Endwalker/Extreme/Ex2Hydaelyn/Aureole.cs | 15 +- .../Extreme/Ex2Hydaelyn/Ex2Hydaelyn.cs | 5 + .../Extreme/Ex2Hydaelyn/Ex2HydaelynEnums.cs | 111 ++--- .../Extreme/Ex2Hydaelyn/Ex2HydaelynStates.cs | 14 +- .../Extreme/Ex2Hydaelyn/HerosSundering.cs | 62 --- .../Extreme/Ex2Hydaelyn/ParhelicCircle.cs | 54 +-- .../Extreme/Ex2Hydaelyn/WeaponTracker.cs | 75 +-- .../Trial/T02Hydaelyn/ParhelicCircle.cs | 48 +- .../Trial/T02Hydaelyn/T02HydaelynEnums.cs | 2 +- BossMod/Util/ShapeDistance.cs | 4 +- 13 files changed, 572 insertions(+), 328 deletions(-) delete mode 100644 BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/HerosSundering.cs diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index 4333c95ab3..6dd75aee28 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -5,6 +5,7 @@ // you can use hash-code to cache clipping results - it will change whenever anything in the instance changes public abstract class ArenaBounds(WPos center, float halfSize) { + public const float ScalingFactor = 1000000; public WPos Center { get; init; } = center; public float HalfSize { get; init; } = halfSize; // largest horizontal/vertical dimension: radius for circle, max of width/height for rect @@ -13,9 +14,34 @@ public abstract class ArenaBounds(WPos center, float halfSize) private readonly Clip2D _clipper = new(); public IEnumerable ClipPoly => _clipper.ClipPoly; - - public List<(WPos, WPos, WPos)> ClipAndTriangulate(ClipperLib.PolyTree poly) => _clipper.ClipAndTriangulate(poly); - public List<(WPos, WPos, WPos)> ClipAndTriangulate(IEnumerable poly) => _clipper.ClipAndTriangulate(poly); + private readonly Dictionary> _polyCache = []; + private readonly Dictionary, List<(WPos, WPos, WPos)>> _polyCache2 = []; + private readonly Dictionary<(WPos, float), List<(WPos, WPos, WPos)>> _triangulateCircle = []; + private readonly Dictionary<(WPos, float, float, Angle, Angle), List<(WPos, WPos, WPos)>> _triangulateCone = []; + private readonly Dictionary<(WPos, float, float), List<(WPos, WPos, WPos)>> _triangulateDonut = []; + private readonly Dictionary<(WPos, WPos, WPos), List<(WPos, WPos, WPos)>> _triangulateTri = []; + private readonly Dictionary<(WPos, WDir, WDir), List<(WPos, WPos, WPos)>> _triangulateTri2 = []; + private readonly Dictionary<(WPos, Angle, Angle, float), List<(WPos, WPos, WPos)>> _triangulateTri3 = []; + private readonly Dictionary<(WPos, WDir, float, float, float), List<(WPos, WPos, WPos)>> _triangulateRect = []; + private readonly Dictionary<(WPos, Angle, float, float, float), List<(WPos, WPos, WPos)>> _triangulateRect2 = []; + private readonly Dictionary<(WPos, WPos, float), List<(WPos, WPos, WPos)>> _triangulateRect3 = []; + + public List<(WPos, WPos, WPos)> ClipAndTriangulate(ClipperLib.PolyTree poly) + { + if (_polyCache.TryGetValue(poly, out var cachedResult)) + return cachedResult; + var result = _clipper.ClipAndTriangulate(poly); + _polyCache[poly] = result; + return result; + } + public List<(WPos, WPos, WPos)> ClipAndTriangulate(IEnumerable poly) + { + if (_polyCache2.TryGetValue(poly, out var cachedResult)) + return cachedResult; + var result = _clipper.ClipAndTriangulate(poly); + _polyCache2[poly] = result; + return result; + } private float _screenHalfSize; public float ScreenHalfSize @@ -47,7 +73,8 @@ public float ScreenHalfSize // TODO: think of a better way to do that (analytical clipping?) if (innerRadius >= outerRadius || innerRadius < 0 || halfAngle.Rad <= 0) return []; - + if (_triangulateCone.TryGetValue((center, innerRadius, outerRadius, centerDirection, halfAngle), out var cachedResult)) + return cachedResult; bool fullCircle = halfAngle.Rad >= MathF.PI; bool donut = innerRadius > 0; var points = (donut, fullCircle) switch @@ -57,78 +84,90 @@ public float ScreenHalfSize (true, false) => CurveApprox.DonutSector(center, innerRadius, outerRadius, centerDirection - halfAngle, centerDirection + halfAngle, MaxApproxError), (true, true) => CurveApprox.Donut(center, innerRadius, outerRadius, MaxApproxError), }; - return ClipAndTriangulate(points); + var result = ClipAndTriangulate(points); + _triangulateCone[(center, innerRadius, outerRadius, centerDirection, halfAngle)] = result; + return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateCircle(WPos center, float radius) { - return ClipAndTriangulate(CurveApprox.Circle(center, radius, MaxApproxError)); + if (_triangulateCircle.TryGetValue((center, radius), out var cachedResult)) + return cachedResult; + var result = ClipAndTriangulate(CurveApprox.Circle(center, radius, MaxApproxError)); + _triangulateCircle[(center, radius)] = result; + return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateDonut(WPos center, float innerRadius, float outerRadius) { if (innerRadius >= outerRadius || innerRadius < 0) return []; - return ClipAndTriangulate(CurveApprox.Donut(center, innerRadius, outerRadius, MaxApproxError)); + if (_triangulateDonut.TryGetValue((center, innerRadius, outerRadius), out var cachedResult)) + return cachedResult; + var result = ClipAndTriangulate(CurveApprox.Donut(center, innerRadius, outerRadius, MaxApproxError)); + _triangulateDonut[(center, innerRadius, outerRadius)] = result; + return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateTri(WPos a, WPos b, WPos c) { - return ClipAndTriangulate([a, b, c]); + if (_triangulateTri.TryGetValue((a, b, c), out var cachedResult)) + return cachedResult; + var result = ClipAndTriangulate([a, b, c]); + _triangulateTri[(a, b, c)] = result; + return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateIsoscelesTri(WPos apex, WDir height, WDir halfBase) { - return ClipAndTriangulateTri(apex, apex + height + halfBase, apex + height - halfBase); + if (_triangulateTri2.TryGetValue((apex, height, halfBase), out var cachedResult)) + return cachedResult; + var result = ClipAndTriangulateTri(apex, apex + height + halfBase, apex + height - halfBase); + _triangulateTri2[(apex, height, halfBase)] = result; + return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateIsoscelesTri(WPos apex, Angle direction, Angle halfAngle, float height) { + if (_triangulateTri3.TryGetValue((apex, direction, halfAngle, height), out var cachedResult)) + return cachedResult; var dir = direction.ToDirection(); var normal = dir.OrthoL(); - return ClipAndTriangulateIsoscelesTri(apex, height * dir, height * halfAngle.Tan() * normal); + var result = ClipAndTriangulateIsoscelesTri(apex, height * dir, height * halfAngle.Tan() * normal); + _triangulateTri3[(apex, direction, halfAngle, height)] = result; + return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateRect(WPos origin, WDir direction, float lenFront, float lenBack, float halfWidth) { + if (_triangulateRect.TryGetValue((origin, direction, lenFront, lenBack, halfWidth), out var cachedResult)) + return cachedResult; var side = halfWidth * direction.OrthoR(); var front = origin + lenFront * direction; var back = origin - lenBack * direction; - return ClipAndTriangulate([front + side, front - side, back - side, back + side]); + var result = ClipAndTriangulate([front + side, front - side, back - side, back + side]); + _triangulateRect[(origin, direction, lenFront, lenBack, halfWidth)] = result; + return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateRect(WPos origin, Angle direction, float lenFront, float lenBack, float halfWidth) { - return ClipAndTriangulateRect(origin, direction.ToDirection(), lenFront, lenBack, halfWidth); + if (_triangulateRect2.TryGetValue((origin, direction, lenFront, lenBack, halfWidth), out var cachedResult)) + return cachedResult; + var result = ClipAndTriangulateRect(origin, direction.ToDirection(), lenFront, lenBack, halfWidth); + _triangulateRect2[(origin, direction, lenFront, lenBack, halfWidth)] = result; + return result; } public List<(WPos, WPos, WPos)> ClipAndTriangulateRect(WPos start, WPos end, float halfWidth) { + if (_triangulateRect3.TryGetValue((start, end, halfWidth), out var cachedResult)) + return cachedResult; var dir = (end - start).Normalized(); var side = halfWidth * dir.OrthoR(); - return ClipAndTriangulate([start + side, start - side, end - side, end + side]); - } - - public static (WPos center, float HalfHeight, float Halfwidth) CalculatePolygonProperties(IEnumerable points) - { - float minX = float.MaxValue; - float maxX = float.MinValue; - float minZ = float.MaxValue; - float maxZ = float.MinValue; - - foreach (var point in points) - { - if (point.X < minX) minX = point.X; - if (point.X > maxX) maxX = point.X; - if (point.Z < minZ) minZ = point.Z; - if (point.Z > maxZ) maxZ = point.Z; - } - - float halfWidth = (maxX - minX) / 2; - float halfHeight = (maxZ - minZ) / 2; - WPos center = new((minX + maxX) / 2, (minZ + maxZ) / 2); - - return (center, halfHeight, halfWidth); + var result = ClipAndTriangulate([start + side, start - side, end - side, end + side]); + _triangulateRect3[(start, end, halfWidth)] = result; + return result; } } @@ -218,29 +257,79 @@ public override WDir ClampToBounds(WDir offset, float scale) //should work for any simple (no self intersections or holes) polygon with an IEnumerable/List of points public class ArenaBoundsPolygon : ArenaBounds { - public float HalfWidth { get; private set; } - public float HalfHeight { get; private set; } + private (WPos Center, float HalfHeight, float HalfWidth)? _cachedPolygonProperties; + private readonly Dictionary _containsCache = []; + private readonly Dictionary<(WPos, WDir), float> _intersectRayCache = []; + private readonly Dictionary, Pathfinding.Map> _buildMapCache = []; + private readonly Dictionary<(WPos, WDir), WDir> _clampToBoundsCache = []; + public readonly IEnumerable Points; - public ArenaBoundsPolygon(IEnumerable points) : base(CalculatePolygonProperties(points).center, MathF.Max(CalculatePolygonProperties(points).Halfwidth, CalculatePolygonProperties(points).HalfHeight)) + public ArenaBoundsPolygon(IEnumerable points) : base(default, default) { Points = points; - (Center, HalfHeight, HalfWidth) = CalculatePolygonProperties(points); + _cachedPolygonProperties = CalculatePolygonProperties2(points); + + var props = _cachedPolygonProperties.Value; + Center = props.Center; + HalfSize = MathF.Max(props.HalfHeight, props.HalfWidth); + } + + private (WPos Center, float HalfHeight, float HalfWidth) CalculatePolygonProperties() + { + if (!_cachedPolygonProperties.HasValue) + _cachedPolygonProperties = CalculatePolygonProperties2(Points); + return _cachedPolygonProperties.Value; + } + + private static (WPos center, float HalfHeight, float Halfwidth) CalculatePolygonProperties2(IEnumerable points) + { + float minX = float.MaxValue; + float maxX = float.MinValue; + float minZ = float.MaxValue; + float maxZ = float.MinValue; + + foreach (var point in points) + { + if (point.X < minX) minX = point.X; + if (point.X > maxX) maxX = point.X; + if (point.Z < minZ) minZ = point.Z; + if (point.Z > maxZ) maxZ = point.Z; + } + + float halfWidth = (maxX - minX) / 2; + float halfHeight = (maxZ - minZ) / 2; + WPos center = new((minX + maxX) / 2, (minZ + maxZ) / 2); + + return (center, halfHeight, halfWidth); } public override IEnumerable BuildClipPoly(float offset) => Points; - public override bool Contains(WPos position) => position.InPolygon(Points); + public override bool Contains(WPos position) + { + if (_containsCache.TryGetValue(position, out var cachedResult)) + return cachedResult; + var result = position.InPolygon(Points); + _containsCache[position] = result; + return result; + } public override Pathfinding.Map BuildMap(float resolution) { - var map = new Pathfinding.Map(resolution, CalculatePolygonProperties(Points).center, CalculatePolygonProperties(Points).Halfwidth, CalculatePolygonProperties(Points).HalfHeight); + if (_buildMapCache.TryGetValue(Points, out var cachedResult)) + return cachedResult; + var unionProperties = CalculatePolygonProperties(); + var map = new Pathfinding.Map(resolution, unionProperties.Center, unionProperties.HalfWidth, unionProperties.HalfHeight); map.BlockPixelsInside(ShapeDistance.InvertedPolygon(Points), 0, 0); + _buildMapCache[Points] = map; return map; } public override float IntersectRay(WPos origin, WDir dir) { + if (_intersectRayCache.TryGetValue((origin, dir), out var cachedResult)) + return cachedResult; float minDistance = float.MaxValue; int i = 0; foreach (var point in Points) @@ -253,11 +342,15 @@ public override float IntersectRay(WPos origin, WDir dir) minDistance = distance; ++i; } + _intersectRayCache[(origin, dir)] = minDistance; return minDistance; } public override WDir ClampToBounds(WDir offset, float scale) { + WPos position = Center + offset; + if (_clampToBoundsCache.TryGetValue((position, offset), out var cachedResult)) + return cachedResult; float minDistance = float.MaxValue; WPos closestPoint = default; @@ -277,8 +370,9 @@ public override WDir ClampToBounds(WDir offset, float scale) } ++i; } - - return closestPoint - Center; + var result = closestPoint - Center; + _clampToBoundsCache[(position, offset)] = result; + return result; } } @@ -326,11 +420,10 @@ public override WDir ClampToBounds(WDir offset, float scale = 1) } } -//for unions of different ArenaBounds, shapes that have clockwise winding order get united, counterclockwise differentiated +//for unions of different ArenaBounds //buggy if there are more than 2 disjointed shapes or more than one non-self-intersecting hole, consider improving BuildClipPoly if such an arena shape is needed public class ArenaBoundsUnion : ArenaBounds { - private const float ScalingFactor = 1000000; private List _boundsList; public List BoundsList { @@ -465,14 +558,7 @@ public override bool Contains(WPos position) { if (_containsCache.TryGetValue(position, out var cachedResult)) return cachedResult; - var combinedPoly = BuildClipPoly().ToList(); - var clipperPoly = combinedPoly.Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); - - if (!clipperPoly.First().Equals(clipperPoly.Last())) - clipperPoly.Add(clipperPoly.First()); - - var intPoint = new ClipperLib.IntPoint(position.X * ScalingFactor, position.Z * ScalingFactor); - var result = ClipperLib.Clipper.PointInPolygon(intPoint, clipperPoly) == 1; + var result = _boundsList.Any(bound => bound.Contains(position)); _containsCache[position] = result; return result; } @@ -557,9 +643,261 @@ public override WDir ClampToBounds(WDir offset, float scale = 1) } } } + _clampToBoundsCache[(position, offset)] = closestPoint - Center; return closestPoint - Center; } _clampToBoundsCache[(position, offset)] = offset; return offset; } } + +//for differences of different ArenaBounds +//buggy if there are more than 2 disjointed shapes or more than one non-self-intersecting hole, consider improving BuildClipPoly if such an arena shape is needed +public class ArenaBoundsDifference : ArenaBounds +{ + private ArenaBounds _baseBound; + private List _subtractBounds; + public ArenaBounds BaseBound + { + get => _baseBound; + set + { + if (!ReferenceEquals(_baseBound, value)) //clear caches for clamptobounds, intersectray and contains if base bound gets modified, BuildClipPolygon and BuildMap stays incase map gets reverted later + { + _baseBound = value; + _containsCache.Clear(); + _intersectRayCache.Clear(); + _clampToBoundsCache.Clear(); + _cachedDifferenceProperties = null; + } + } + } + public List BoundsList + { + get => _subtractBounds; + set + { + if (!ReferenceEquals(_subtractBounds, value)) //clear caches for clamptobounds, intersectray and contains if subtracted bounds gets modified, BuildClipPolygon and BuildMap stays incase map gets reverted later + { + _subtractBounds = value; + _containsCache.Clear(); + _intersectRayCache.Clear(); + _clampToBoundsCache.Clear(); + _cachedDifferenceProperties = null; + } + } + } + private (WPos Center, float HalfHeight, float HalfWidth)? _cachedDifferenceProperties; + private readonly Dictionary<(ArenaBounds, List), IEnumerable> _polyCache = []; + private readonly Dictionary _containsCache = []; + private readonly Dictionary<(WPos, WDir), float> _intersectRayCache = []; + private readonly Dictionary<(ArenaBounds, List), Pathfinding.Map> _buildMapCache = []; + private readonly Dictionary<(WPos, WDir), WDir> _clampToBoundsCache = []; + + public ArenaBoundsDifference(ArenaBounds baseBound, IEnumerable subtractBounds) : base(default, default) + { + _baseBound = baseBound; + _subtractBounds = subtractBounds.ToList(); + _cachedDifferenceProperties = CalculateDifferenceProperties2(_baseBound, _subtractBounds); + + var props = _cachedDifferenceProperties.Value; + Center = props.Center; + HalfSize = MathF.Max(props.HalfHeight, props.HalfWidth); + } + + private (WPos Center, float HalfHeight, float HalfWidth) CalculateDifferenceProperties() + { + if (!_cachedDifferenceProperties.HasValue) + _cachedDifferenceProperties = CalculateDifferenceProperties2(_baseBound, _subtractBounds); + return _cachedDifferenceProperties.Value; + } + + private static (WPos Center, float HalfHeight, float HalfWidth) CalculateDifferenceProperties2(ArenaBounds baseBound, IEnumerable subtractBounds) + { + float minX = baseBound.Center.X - baseBound.HalfSize; + float maxX = baseBound.Center.X + baseBound.HalfSize; + float minZ = baseBound.Center.Z - baseBound.HalfSize; + float maxZ = baseBound.Center.Z + baseBound.HalfSize; + + foreach (var bound in subtractBounds) + { + foreach (var point in bound.BuildClipPoly()) + { + if (point.X < minX) minX = Math.Max(minX, point.X); + if (point.X > maxX) maxX = Math.Min(maxX, point.X); + if (point.Z < minZ) minZ = Math.Max(minZ, point.Z); + if (point.Z > maxZ) maxZ = Math.Min(maxZ, point.Z); + } + } + + float halfWidth = (maxX - minX) / 2; + float halfHeight = (maxZ - minZ) / 2; + WPos center = new((minX + maxX) / 2, (minZ + maxZ) / 2); + + return (center, halfHeight, halfWidth); + } + + public override IEnumerable BuildClipPoly(float offset = 0) + { + if (_polyCache.TryGetValue((BaseBound, _subtractBounds), out var cachedResult)) + return cachedResult; + + var clipperBase = new ClipperLib.Clipper(); + var basePoly = _baseBound.BuildClipPoly(offset).Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); + clipperBase.AddPath(basePoly, ClipperLib.PolyType.ptSubject, true); + + var resultPoly = new ClipperLib.PolyTree(); + clipperBase.Execute(ClipperLib.ClipType.ctUnion, resultPoly); + + foreach (var subBound in _subtractBounds) + { + var clipper = new ClipperLib.Clipper(); + clipper.AddPaths(ClipperLib.Clipper.PolyTreeToPaths(resultPoly), ClipperLib.PolyType.ptSubject, true); + + var subPoly = subBound.BuildClipPoly(offset).Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); + clipper.AddPath(subPoly, ClipperLib.PolyType.ptClip, true); + + var intermediateResult = new ClipperLib.PolyTree(); + clipper.Execute(ClipperLib.ClipType.ctDifference, intermediateResult, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); + resultPoly = intermediateResult; + } + + var finalResult = new List(); + var resultPolygons = ClipperLib.Clipper.ClosedPathsFromPolyTree(resultPoly); + foreach (var polygon in resultPolygons) + { + var polyResult = polygon.Select(pt => new WPos(pt.X / ScalingFactor, pt.Y / ScalingFactor)).ToList(); + if (polyResult.Any()) + { + polyResult.Add(polyResult.First()); + finalResult.AddRange(polyResult); + } + } + _polyCache[(BaseBound, _subtractBounds)] = finalResult; + return finalResult; + } + + public override Pathfinding.Map BuildMap(float resolution = 0.5f) + { + if (_buildMapCache.TryGetValue((BaseBound, _subtractBounds), out var cachedResult)) + return cachedResult; + var props = CalculateDifferenceProperties(); + + var map = new Pathfinding.Map(resolution, props.Center, props.HalfWidth, props.HalfHeight); + + foreach (var (x, y, pos) in map.EnumeratePixels()) + { + bool insideBase = _baseBound.Contains(pos); + bool insideSubtract = _subtractBounds.Any(sub => sub.Contains(pos)); + map.Pixels[y * map.Width + x].MaxG = insideBase && !insideSubtract ? float.MaxValue : 0; + } + _buildMapCache[(BaseBound, _subtractBounds)] = map; + return map; + } + + public override bool Contains(WPos position) + { + if (_containsCache.TryGetValue(position, out var cachedResult)) + return cachedResult; + var result = _baseBound.Contains(position) && !_subtractBounds.Any(bound => bound.Contains(position)); + _containsCache[position] = result; + return result; + } + + public override float IntersectRay(WPos origin, WDir dir) + { + if (_intersectRayCache.TryGetValue((origin, dir), out var cachedResult)) + return cachedResult; + + var props = CalculateDifferenceProperties(); + + var clipperBase = new ClipperLib.Clipper(); + var basePoly = _baseBound.BuildClipPoly().Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); + clipperBase.AddPath(basePoly, ClipperLib.PolyType.ptSubject, true); + + var resultPoly = new ClipperLib.PolyTree(); + clipperBase.Execute(ClipperLib.ClipType.ctUnion, resultPoly); + + foreach (var subBound in _subtractBounds) + { + var clipper = new ClipperLib.Clipper(); + clipper.AddPaths(ClipperLib.Clipper.PolyTreeToPaths(resultPoly), ClipperLib.PolyType.ptSubject, true); + + var subPoly = subBound.BuildClipPoly().Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); + clipper.AddPath(subPoly, ClipperLib.PolyType.ptClip, true); + + var intermediateResult = new ClipperLib.PolyTree(); + clipper.Execute(ClipperLib.ClipType.ctDifference, intermediateResult, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); + resultPoly = intermediateResult; + } + + float nearestIntersection = float.MaxValue; + var resultPolygons = ClipperLib.Clipper.ClosedPathsFromPolyTree(resultPoly); + foreach (var polygon in resultPolygons) + { + for (int i = 0; i < polygon.Count; i++) + { + ClipperLib.IntPoint currentPoint = polygon[i]; + ClipperLib.IntPoint nextPoint = polygon[(i + 1) % polygon.Count]; + WPos p1 = new(currentPoint.X / ScalingFactor, currentPoint.Y / ScalingFactor); + WPos p2 = new(nextPoint.X / ScalingFactor, nextPoint.Y / ScalingFactor); + + float distance = Intersect.RaySegment(origin, dir, p1, p2); + if (distance >= 0 && distance < nearestIntersection) + nearestIntersection = distance; + } + } + _intersectRayCache[(origin, dir)] = nearestIntersection; + return nearestIntersection; + } + + public override WDir ClampToBounds(WDir offset, float scale = 1) + { + WPos position = Center + offset; + if (_clampToBoundsCache.TryGetValue((position, offset), out var cachedResult)) + return cachedResult; + var clipperBase = new ClipperLib.Clipper(); + var basePoly = _baseBound.BuildClipPoly().Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); + clipperBase.AddPath(basePoly, ClipperLib.PolyType.ptSubject, true); + + var resultPoly = new ClipperLib.PolyTree(); + clipperBase.Execute(ClipperLib.ClipType.ctUnion, resultPoly); + + foreach (var subBound in _subtractBounds) + { + var clipper = new ClipperLib.Clipper(); + clipper.AddPaths(ClipperLib.Clipper.PolyTreeToPaths(resultPoly), ClipperLib.PolyType.ptSubject, true); + + var subPoly = subBound.BuildClipPoly().Select(p => new ClipperLib.IntPoint(p.X * ScalingFactor, p.Z * ScalingFactor)).ToList(); + clipper.AddPath(subPoly, ClipperLib.PolyType.ptClip, true); + + var intermediateResult = new ClipperLib.PolyTree(); + clipper.Execute(ClipperLib.ClipType.ctDifference, intermediateResult, ClipperLib.PolyFillType.pftNonZero, ClipperLib.PolyFillType.pftNonZero); + resultPoly = intermediateResult; + } + + var closestPoint = position; + float closestDist = float.MaxValue; + var polygons = ClipperLib.Clipper.ClosedPathsFromPolyTree(resultPoly); + foreach (var polygon in polygons) + { + for (int i = 0; i < polygon.Count; i++) + { + WPos p1 = new(polygon[i].X / ScalingFactor, polygon[i].Y / ScalingFactor); + WPos p2 = new(polygon[(i + 1) % polygon.Count].X / ScalingFactor, polygon[(i + 1) % polygon.Count].Y / ScalingFactor); + + WPos currentClosest = Intersect.ClosestPointOnSegment(p1, p2, position); + float currentDist = (currentClosest - position).Length(); + + if (currentDist < closestDist) + { + closestDist = currentDist; + closestPoint = currentClosest; + } + } + } + var result = closestPoint - Center; + _clampToBoundsCache[(position, offset)] = result; + return result; + } +} diff --git a/BossMod/BossModule/MiniArena.cs b/BossMod/BossModule/MiniArena.cs index 0c4de8772d..3c110d7c45 100644 --- a/BossMod/BossModule/MiniArena.cs +++ b/BossMod/BossModule/MiniArena.cs @@ -43,7 +43,7 @@ public void Begin(float cameraAzimuthRadians) ImGui.GetWindowDrawList().PushClipRect(Vector2.Max(cursor, wmin), Vector2.Min(cursor + fullSize, wmax)); if (Config.OpaqueArenaBackground) { - if (Bounds is ArenaBoundsPolygon or ArenaBoundsDonut or ArenaBoundsUnion) //only use the more expensive fill algorithm if needed, draw time is 0.05 to 0.1ms higher + if (Bounds is ArenaBoundsPolygon or ArenaBoundsDonut or ArenaBoundsUnion or ArenaBoundsDifference) //only use the more expensive fill algorithm if needed, draw time is 0.05 to 0.1ms higher { var clipPoly = Bounds.BuildClipPoly(); var triangles = Bounds.ClipAndTriangulate(clipPoly); @@ -202,27 +202,38 @@ public void TextWorld(WPos center, string text, uint color, float fontSize = 17) // draw arena border public void Border(uint color) { - if (Bounds is ArenaBoundsUnion union) - { - var polygons = union.BuildClipPoly().ToList(); - var groupedPolygons = GroupPolygons(polygons); - foreach (var polygon in groupedPolygons) - DrawPolygon(polygon, color); - } - else if (Bounds is ArenaBoundsDonut donut) - { - AddCircle(Bounds.Center, donut.OuterRadius, color, 2); - AddCircle(Bounds.Center, donut.InnerRadius, color, 2); - } - else + ProcessBounds(Bounds, color); + } + + private void ProcessBounds(ArenaBounds bounds, uint color) + { + switch (bounds) { - foreach (var p in Bounds.ClipPoly) - PathLineTo(p); - PathStroke(true, color, 2); + case ArenaBoundsUnion union: + DrawPolygons(union, color); + break; + case ArenaBoundsDifference difference: + DrawPolygons(difference, color); + break; + case ArenaBoundsDonut donut: + AddCircle(bounds.Center, donut.OuterRadius, color, 2); + AddCircle(bounds.Center, donut.InnerRadius, color, 2); + break; + default: + DefaultHandling(bounds, color); + break; } } - private void DrawPolygon(IEnumerable vertices, uint color) + private void DrawPolygons(ArenaBounds bounds, uint color) + { + var polygons = bounds.BuildClipPoly().ToList(); + var groupedPolygons = GroupPolygons(polygons); + foreach (var polygon in groupedPolygons) + DrawPolygons2(polygon, color); + } + + private void DrawPolygons2(IEnumerable vertices, uint color) { var lastPoint = vertices.First(); PathLineTo(lastPoint); @@ -235,6 +246,14 @@ private void DrawPolygon(IEnumerable vertices, uint color) PathStroke(true, color, 2); } + private void DefaultHandling(ArenaBounds bounds, uint color) + { + foreach (var p in bounds.ClipPoly) + PathLineTo(p); + PathStroke(true, color, 2); + } + + private static IEnumerable> GroupPolygons(IEnumerable vertices) { List currentPolygon = []; diff --git a/BossMod/Modules/DemoModule.cs b/BossMod/Modules/DemoModule.cs index ad3211f72c..624dc47780 100644 --- a/BossMod/Modules/DemoModule.cs +++ b/BossMod/Modules/DemoModule.cs @@ -23,7 +23,7 @@ public override void AddGlobalHints(GlobalHints hints) public override void DrawArenaBackground(int pcSlot, Actor pc) { - Arena.ZoneCircle(Module.Bounds.Center, 12, ArenaColor.AOE); + Arena.ZoneCircle(Module.Bounds.Center, 10, ArenaColor.AOE); } public override void DrawArenaForeground(int pcSlot, Actor pc) @@ -32,7 +32,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) } } - //for testing ray intersections with new arena shapes + // for testing ray intersections with new arena shapes // class RayIntersectionTest(BossModule module) : Components.Knockback(module) // { @@ -48,10 +48,17 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsUnion([new ArenaBoundsDonut(new(80, 100), 20,30), new ArenaBoundsDonut(new(120, 120), 20,30), new ArenaBoundsDonut(new(100, 100), 20,30)])) // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsUnion([new ArenaBoundsCircle(new(105, 115), 15), new ArenaBoundsCircle(new(120, 120), 15), new ArenaBoundsCircle(new(120, 100), 15)])) // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsUnion([new ArenaBoundsDonut(new(100, 100), 20,30), new ArenaBoundsRect(new(120, 120), 5,20,240.Degrees()), new ArenaBoundsRect(new(80, 80), 5,20,-120.Degrees()), new ArenaBoundsRect(new(80, 120), 5,20,120.Degrees())])) + // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsUnion([new ArenaBoundsDonut(new(100, 100), 20, 30), new ArenaBoundsRect(new(100, 75), 5, 20), new ArenaBoundsRect(Helpers.RotateAroundOrigin(-120, new(100, 100),new(100, 75)), 5, 20, 120.Degrees()), new ArenaBoundsRect(Helpers.RotateAroundOrigin(120, new(100, 100),new(100, 75)), 5, 20, -120.Degrees())])) + // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsDonut(new(100, 100), 20, 30)) // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsUnion([new ArenaBoundsCircle(new(105, 115), 10), new ArenaBoundsCircle(new(140, 100), 10), new ArenaBoundsCircle(new(120, 95), 10)])) // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsUnion([new ArenaBoundsDonut(new(105, 115), 5,10), new ArenaBoundsDonut(new(120, 100), 5,10)])) // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsUnion([new ArenaBoundsDonut(new(80, 100), 10,20), new ArenaBoundsDonut(new(120, 120), 10,20), new ArenaBoundsCircle(new(100, 100), 30)])) + // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsDifference(new ArenaBoundsSquare(new(100, 100), 30), [new ArenaBoundsCircle(new(80, 100), 10), new ArenaBoundsCircle(new(120, 120), 10)])) + // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsDifference(new ArenaBoundsDonut(new(100, 100), 20,30), [new ArenaBoundsCircle(new(80, 100), 25), new ArenaBoundsCircle(new(120, 120), 25)])) + // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsDifference(new ArenaBoundsSquare(new(100, 100), 20), [new ArenaBoundsDonut(new(80, 100), 15,25), new ArenaBoundsCircle(new(120, 120), 25)])) + // public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsDifference(new ArenaBoundsSquare(new(100, 100), 20), [new ArenaBoundsDonut(new(80, 100), 15,25),new ArenaBoundsDonut(new(80, 100), 26,28)])) + public DemoModule(WorldState ws, Actor primary) : base(ws, primary, new ArenaBoundsSquare(new(100, 100), 20)) { ActivateComponent(); diff --git a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Aureole.cs b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Aureole.cs index c0770debce..4907a308ee 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Aureole.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Aureole.cs @@ -1,22 +1,9 @@ namespace BossMod.Endwalker.Extreme.Ex2Hydaelyn; -// component tracking [lateral] aureole mechanic +// component tracking [lateral] aureole mechanic, only exists for the timeline anymore class Aureole(BossModule module) : BossComponent(module) { public bool Done { get; private set; } - private AOEShapeCone _aoe = new(40, 75.Degrees(), (AID)(module.PrimaryActor.CastInfo?.Action.ID ?? 0) is AID.LateralAureole1 or AID.LateralAureole2 ? -90.Degrees() : 0.Degrees()); - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (_aoe.Check(actor.Position, Module.PrimaryActor) || _aoe.Check(actor.Position, Module.PrimaryActor.Position, Module.PrimaryActor.Rotation + 180.Degrees())) - hints.Add("GTFO from aoe!"); - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - _aoe.Draw(Arena, Module.PrimaryActor); - _aoe.Draw(Arena, Module.PrimaryActor.Position, Module.PrimaryActor.Rotation + 180.Degrees()); - } public override void OnEventCast(Actor caster, ActorCastEvent spell) { diff --git a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2Hydaelyn.cs b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2Hydaelyn.cs index 79c4ac8a14..dc4426cc86 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2Hydaelyn.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2Hydaelyn.cs @@ -8,6 +8,11 @@ class PureCrystal(BossModule module) : Components.CastCounter(module, ActionID.M // cast counter for post-intermission AOE class Exodus(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Exodus)); +class LateralAureole1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LateralAureole1AOE), new AOEShapeCone(40, 75.Degrees())); +class LateralAureole2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LateralAureole2AOE), new AOEShapeCone(40, 75.Degrees())); +class Aureole1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Aureole1AOE), new AOEShapeCone(40, 75.Degrees())); +class Aureole2(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Aureole2AOE), new AOEShapeCone(40, 75.Degrees())); +class HerosSundering(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.HerosSundering), new AOEShapeCone(40, 45.Degrees())); [ConfigDisplay(Order = 0x020, Parent = typeof(EndwalkerConfig))] public class Ex2HydaelynConfig() : CooldownPlanningConfigNode(90); diff --git a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2HydaelynEnums.cs b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2HydaelynEnums.cs index d55f2ed30f..1b933b9796 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2HydaelynEnums.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2HydaelynEnums.cs @@ -17,30 +17,58 @@ public enum OID : uint public enum AID : uint { - AutoAttackSword = 870, - Enrage = 24571, // Boss->self - WeaponChangeAOEChakram = 26004, // Boss->self, no cast, chakram aoe - WeaponChangeAOEStaff = 26008, // Boss->self, no cast, staff aoe - ShiningSaberAOE = 26006, // Boss->target, no cast, shared damage - CrystallizeSwordStaffWater = 26010, // Boss->self: (1) water+blue->red/green, (6) ? - CrystallizeStaffEarth = 26011, // Boss->self: (3) earth+green->blue - CrystallizeStaffIce = 26012, // Boss->self: (2) ice+green->red, (4) ice+green->green - CrystallizeChakramIce = 26013, // Boss->self: (2) ice+red->green - CrystallizeChakramEarth = 26014, // Boss->self: (3) earth+red->blue - CrystallizeTriggerEarth = 26015, // Boss->self, no cast, removes buff and triggers aoe - CrystallizeTriggerIce = 26016, // Boss->self, no cast, removes buff and triggers aoe - CrystallizeTriggerWater = 26017, // Boss->self, no cast, removes buff and triggers aoe - CrystallineWater = 26018, // Helper->healer, no cast - CrystallineStone = 26019, // Helper->target, no cast - CrystallineBlizzard = 26020, // Helper->target, no cast - Halo = 26021, // Boss->self - LightOfTheCrystal = 26022, // Helper->self, no cast, aoe when lightwave hits crystal - RayOfLight = 26023, // Helper->self, no cast, aoe inside lightwave - HerosGlory = 26024, // Boss->self - TeleportToCenter = 26025, // Boss->n/a - InfralateralArcAOE = 26026, // Boss->self, no cast - ParhelicCircle = 26028, // Boss->self - Incandescence = 26031, // MysticRefulgence->self, no cast, aoe + AutoAttackSword = 870, // Boss/Echo->player, no cast, single-target + HerosRadiance = 26049, // Boss->self, 5,0s cast, range 40 circle + ShiningSaber = 26824, // Boss->self, 4,9s cast, single-target, stack + ShiningSaberAOE = 26006, // Boss->players, no cast, range 6 circle + CrystallizeSwordStaffWater = 26010, // Boss->self, 4,0s cast, single-target, (1) water+blue->red/green, (6) ? + WeaponChangeAOEStaff = 26008, // Boss->self, no cast, range 10 circle + CrystallizeTriggerIce = 26016, // Boss->self, no cast, single-target + CrystallineWater = 26018, // Helper->players, no cast, range 6 circle, healer light party stack + MagosRadiance = 26050, // Boss->self, 5,0s cast, range 40 circle + AutoAttackStaff = 27732, // Boss->player, no cast, single-target + LateralAureole2 = 28435, // Boss->self, 5,0s cast, single-target + LateralAureole2AOE = 28436, // Helper->self, 5,5s cast, range 40 150-degree cone + CrystallizeStaffIce = 26012, // Boss->self, 4,0s cast, single-target, (2) ice+green->red, (4) ice+green->green + WeaponChangeAOEChakram = 26004, // Boss->self, no cast, range 5-40 donut + CrystallizeTriggerWater = 26017, // Boss->self, no cast, single-target + CrystallineBlizzard = 26020, // Helper->players, no cast, range 5 circle + AutoAttackChakram = 27733, // Boss->player, no cast, single-target + MousaScorn = 26048, // Boss->players, 5,0s cast, range 4 circle + LateralAureole1 = 26053, // Boss->self, 5,0s cast, single-target + LateralAureole1AOE = 26256, // Helper->self, 5,5s cast, range 40 150-degree cone + CrystallizeChakramEarth = 26014, // Boss->self, 4,0s cast, single-target, (3) earth+red->blue + WeaponChangeVisualSword = 26051, // Boss->self, no cast, single-target + WeaponChangeAOESword = 28338, // Helper->self, no cast, range 40 width 10 cross + CrystallizeTriggerEarth = 26015, // Boss->self, no cast, single-target + CrystallineStone = 26019, // Helper->players, no cast, range 6 circle + CrystalPhase = 26044, // Boss->self, no cast, single-target, trigger for crystal phase + PureCrystal = 26045, // Helper->self, no cast, range 40 circle + IncreaseConviction = 26046, // CrystalOfLight->Boss, no cast, single-target, performed every 1s by glowing crystals to increase conviction + CrystalOfLightDeath = 26732, // Helper->Echo, no cast, single-target, 1 cast per echo after each crystal dies + ExodusVisual = 26043, // Boss->self, no cast, single-target, happens after all echoes die + Exodus = 26155, // Helper->self, no cast, range 40 circle + ExodusEnrage = 27911, // Helper->self, no cast, range 40 circle, enrage if crystal phase takes too long + Halo = 26021, // Boss->self, 5,0s cast, range 40 circle + LightwaveSword = 26259, // Boss->self, 4,0s cast, single-target + RayOfLight = 26023, // Helper->self, no cast, range 15 width 16 rect, aoe inside lightwave + LightOfTheCrystal = 26022, // Helper->self, 1,0s cast, range 40 circle, aoe when lightwave hits crystal + TeleportToCenter = 26025, // Boss->location, no cast, single-target + InfralateralArc = 26217, // Boss->self, 4,9s cast, single-target + InfralateralArcAOE = 26026, // Boss->self, no cast, range 40 90-degree cone + HerosGlory = 26024, // Boss->self, 5,0s cast, range 40 180-degree cone + HerosSundering = 26047, // Boss->self/players, 5,0s cast, range 40 90-degree cone + ParhelicCircle = 26028, // Boss->self, 6,0s cast, single-target + MysticRefulgenceTeleport = 26030, // MysticRefulgence->location, no cast, single-target + IncandescenceTrigger = 26029, // MysticRefulgence2->self, no cast, single-target + Incandescence = 26031, // MysticRefulgence->self, no cast, range 6 circle + Aureole1 = 27793, // Boss->self, 5,0s cast, single-target + Aureole1AOE = 27794, // Helper->self, 5,5s cast, range 40 150-degree cone + Aureole2 = 28433, // Boss->self, 5,0s cast, single-target + Aureole2AOE = 28434, // Helper->self, 5,5s cast, range 40 150-degree cone + CrystallizeChakramWater = 28373, // Boss->self, 4,0s cast, single-target, (5) water+red->red + CrystallizeChakramIce = 26013, // Boss->self, 4,0s cast, single-target, (2) ice+red->green + CrystallizeStaffEarth = 26011, // Boss->self, 4,0s cast, single-target, (3) earth+green->blue Parhelion = 26032, // Boss->self ParhelionNext = 26033, // Boss->self, no cast BeaconParhelion = 26034, // Parhelion->location @@ -52,42 +80,19 @@ public enum AID : uint DichroicSpectrum = 26040, // Helper->mt BrightSpectrum = 26041, // Helper->non-tank EchoesAOE = 26042, // Helper->target, no cast - ExodusVisual = 26043, // Boss->self, no cast, ??? (happens after all echoes die) - PureCrystal = 26045, // Helper->self, no cast, raidwide - IncreaseConviction = 26046, // CrystalOfLight->Boss, no cast, performed every 1s by glowing crystals to increase conviction - HerosSundering = 26047, // Boss->mt - MousaScorn = 26048, // Boss->mt - HerosRadiance = 26049, // Boss->self - MagosRadiance = 26050, // Boss->self - WeaponChangeVisualSword = 26051, // Boss->self, no cast, sword visual - LateralAureole1 = 26053, // Boss->self - Exodus = 26155, // Helper->self, no cast, raidwide - InfralateralArc = 26217, // Boss->self - LateralAureole1AOE = 26256, // Helper->self - LightwaveSword = 26259, // Boss->self LightwaveStaff = 26260, // Boss->self LightwaveChakram = 26261, // Boss->self - CrystalOfLightDeath = 26732, // Helper->Echo, no cast, 1 cast per echo after each crystal dies - ShiningSaber = 26824, // Boss->self - AutoAttackStaff = 27732, - AutoAttackChakram = 27733, Subparhelion = 27734, // Boss->self - Aureole1 = 27793, // Boss->self - Aureole1AOE = 27794, // Helper->self - WeaponChangeAOESword = 28338, // Helper->self, no cast, sword aoe - CrystallizeChakramWater = 28373, // Boss->self: (5) water+red->red - Aureole2 = 28433, // Boss->self - Aureole2AOE = 28434, // Helper->self - LateralAureole2 = 28435, // Boss->self - LateralAureole2AOE = 28436, // Helper->self + HerosRadianceEnrage = 24571, // Boss->self } public enum SID : uint { - CrystallizeElement = 2056, // invisible in ui, extra determines element type - HerosMantle = 2876, // sword stance - MagosMantle = 2877, // staff stance - MousaMantle = 2878, // chakram stance + CrystallizeElement = 2056, // Boss->Boss, extra=0x151/0x153/0x152, invisible in ui, extra determines element type + HydaelynsWeapon = 2273, // Boss->Boss, extra=0x1B4/0x1B5, (n/a for sword, 1B4 for staff, 1B5 for chakram) + MagosMantle = 2877, // none->Boss, extra=0x0 + MousaMantle = 2878, // none->Boss, extra=0x0 + HerosMantle = 2876, // none->Boss/Echo, extra=0x0 } public enum IconID : uint diff --git a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2HydaelynStates.cs b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2HydaelynStates.cs index cefa20c639..92efcfa18b 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2HydaelynStates.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/Ex2HydaelynStates.cs @@ -92,7 +92,7 @@ private void ForkSecondMerge(uint id, float delay) SwitchWeapon(id + 0x160000, 1.3f, false); CrystallizeAureole(id + 0x170000, 7.3f, false); SwitchWeapon(id + 0x180000, 1.3f, true); - Cast(id + 0x190000, AID.Enrage, 9.5f, 10, "Enrage"); + Cast(id + 0x190000, AID.HerosRadianceEnrage, 9.5f, 10, "Enrage"); } private void Intermission(uint id, float delay) @@ -219,9 +219,15 @@ private State Aureole(uint id, float delay) { // note: what is the difference between aureole spells? seems to be determined by weapon?.. CastMulti(id, new AID[] { AID.Aureole1, AID.Aureole2, AID.LateralAureole1, AID.LateralAureole2 }, delay, 5) - .ActivateOnEnter(); + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); return ComponentCondition(id + 2, 0.5f, comp => comp.Done, "Aureole") - .DeactivateOnExit(); + .DeactivateOnExit() + .DeactivateOnExit() + .DeactivateOnExit() + .DeactivateOnExit(); } private void ParhelicCircle(uint id, float delay) @@ -235,7 +241,7 @@ private void ParhelicCircle(uint id, float delay) private void SwitchWeapon(uint id, float delay, bool toSword) { ComponentCondition(id, delay, comp => comp.AOEImminent, "Select weapon"); - ComponentCondition(id + 0x10, toSword ? 4.5f : 3.7f, comp => !comp.AOEImminent, "Weapon AOE"); + ComponentCondition(id + 0x10, toSword ? 6.9f : 6, comp => !comp.AOEImminent, "Weapon AOE"); } // note: activates Crystallize component and sets positioning flag diff --git a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/HerosSundering.cs b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/HerosSundering.cs deleted file mode 100644 index 2455aa6f9a..0000000000 --- a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/HerosSundering.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace BossMod.Endwalker.Extreme.Ex2Hydaelyn; - -class HerosSundering : BossComponent -{ - private readonly Actor? _target; - private BitMask _otherHit; - - private static readonly AOEShapeCone _aoeShape = new(40, 45.Degrees()); - - public HerosSundering(BossModule module) : base(module) - { - _target = WorldState.Actors.Find(Module.PrimaryActor.CastInfo?.TargetID ?? 0); - if (_target == null) - ReportError($"Failed to determine target for heros sundering: {Module.PrimaryActor.CastInfo?.TargetID:X}"); - } - - public override void Update() - { - _otherHit.Reset(); - if (_target != null) - { - var dir = Angle.FromDirection(_target.Position - Module.PrimaryActor.Position); - _otherHit = Raid.WithSlot().Exclude(_target).InShape(_aoeShape, Module.PrimaryActor.Position, dir).Mask(); - } - } - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (_target == null) - return; - - if (actor == _target) - { - if (_otherHit.Any()) - hints.Add("Turn boss away from raid!"); - } - else - { - if (_otherHit[slot]) - hints.Add("GTFO from tankbuster aoe!"); - } - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - if (_target != null) - _aoeShape.Draw(Arena, Module.PrimaryActor.Position, Angle.FromDirection(_target.Position - Module.PrimaryActor.Position)); - } - - public override void DrawArenaForeground(int pcSlot, Actor pc) - { - if (pc == _target) - { - foreach (var (slot, player) in Raid.WithSlot().Exclude(_target)) - Arena.Actor(player, _otherHit[slot] ? ArenaColor.PlayerInteresting : ArenaColor.PlayerGeneric); - } - else - { - Arena.Actor(_target, ArenaColor.Danger); - } - } -} diff --git a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/ParhelicCircle.cs b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/ParhelicCircle.cs index a8d902f3ac..5b0a59beaf 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/ParhelicCircle.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/ParhelicCircle.cs @@ -1,47 +1,33 @@ namespace BossMod.Endwalker.Extreme.Ex2Hydaelyn; -class ParhelicCircle(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Incandescence)) +class ParhelicCircle(BossModule module) : Components.GenericAOEs(module) { - private readonly List _positions = []; + private readonly List _aoes = []; + private static readonly AOEShapeCircle _circle = new(6); - private static readonly float _triRadius = 8; - private static readonly float _hexRadius = 17; - private static readonly AOEShapeCircle _aoeShape = new(6); + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; - public override void Update() + public override void OnActorCreated(Actor actor) { - if (_positions.Count == 0) + var activation = WorldState.FutureTime(7.6f); + var c = Module.Bounds.Center; + if ((OID)actor.OID == OID.RefulgenceHexagon) { - // there are 10 orbs: 1 in center, 3 in vertices of a triangle with radius=8, 6 in vertices of a hexagon with radius=17 - // note: i'm not sure how exactly orientation is determined, it seems to be related to eventobj rotations... - var hex = Module.Enemies(OID.RefulgenceHexagon).FirstOrDefault(); - var tri = Module.Enemies(OID.RefulgenceTriangle).FirstOrDefault(); - if (hex != null && tri != null) - { - var c = Module.Bounds.Center; - _positions.Add(c); - _positions.Add(c + _triRadius * (tri.Rotation + 60.Degrees()).ToDirection()); - _positions.Add(c + _triRadius * (tri.Rotation + 180.Degrees()).ToDirection()); - _positions.Add(c + _triRadius * (tri.Rotation - 60.Degrees()).ToDirection()); - _positions.Add(c + _hexRadius * hex.Rotation.ToDirection()); - _positions.Add(c + _hexRadius * (hex.Rotation + 60.Degrees()).ToDirection()); - _positions.Add(c + _hexRadius * (hex.Rotation + 120.Degrees()).ToDirection()); - _positions.Add(c + _hexRadius * (hex.Rotation + 180.Degrees()).ToDirection()); - _positions.Add(c + _hexRadius * (hex.Rotation - 120.Degrees()).ToDirection()); - _positions.Add(c + _hexRadius * (hex.Rotation - 60.Degrees()).ToDirection()); - } + _aoes.Add(new(_circle, c, default, activation)); + for (int i = 1; i < 7; ++i) + _aoes.Add(new(_circle, Helpers.RotateAroundOrigin(i * 60, c, c + 17 * actor.Rotation.ToDirection()), default, activation)); } + if ((OID)actor.OID == OID.RefulgenceTriangle) + for (int i = 1; i < 4; ++i) + _aoes.Add(new(_circle, Helpers.RotateAroundOrigin(-60 + i * 120, c, c + 8 * actor.Rotation.ToDirection()), default, activation)); } - public override void AddHints(int slot, Actor actor, TextHints hints) + public override void OnCastFinished(Actor caster, ActorCastInfo spell) { - if (_positions.Any(p => _aoeShape.Check(actor.Position, p))) - hints.Add("GTFO from aoe!"); - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - foreach (var p in _positions) - _aoeShape.Draw(Arena, p); + if ((AID)spell.Action.ID == AID.Incandescence) + { + ++NumCasts; + _aoes.Clear(); + } } } diff --git a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/WeaponTracker.cs b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/WeaponTracker.cs index 64d2626c00..7c925e8a4f 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/WeaponTracker.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex2Hydaelyn/WeaponTracker.cs @@ -1,76 +1,47 @@ namespace BossMod.Endwalker.Extreme.Ex2Hydaelyn; -// component tracking boss weapon/stance switches; also draws imminent aoe after each switch -// note: we could rely on invisible buff 2273 to select weapon (n/a for sword, 1B4 for staff, 1B5 for chakram), it appears slightly earlier than 'official' buff -class WeaponTracker(BossModule module) : BossComponent(module) +class WeaponTracker(BossModule module) : Components.GenericAOEs(module) { + public bool AOEImminent { get; private set; } + private AOEInstance? _aoe; public enum Stance { None, Sword, Staff, Chakram } public Stance CurStance { get; private set; } - public bool AOEImminent { get; private set; } - private static readonly AOEShapeRect _aoeSword = new(20, 5, 20); - private static readonly AOEShapeCircle _aoeStaff = new(10); - private static readonly AOEShapeDonut _aoeChakram = new(5, 40); + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); - public override void AddHints(int slot, Actor actor, TextHints hints) + public override void OnStatusGain(Actor actor, ActorStatus status) { - if (!AOEImminent) - return; - - bool inAOE = CurStance switch + if ((SID)status.ID == SID.HydaelynsWeapon && status.Extra == 0x1B4) { - Stance.Sword => _aoeSword.Check(actor.Position, Module.PrimaryActor.Position, 0.Degrees()) || _aoeSword.Check(actor.Position, Module.PrimaryActor.Position, 90.Degrees()), - Stance.Staff => _aoeStaff.Check(actor.Position, Module.PrimaryActor.Position), - Stance.Chakram => _aoeChakram.Check(actor.Position, Module.PrimaryActor.Position), - _ => false - }; - if (inAOE) - hints.Add("GTFO from weapon aoe!"); - } - - public override void DrawArenaBackground(int pcSlot, Actor pc) - { - if (!AOEImminent) - return; + _aoe = new(new AOEShapeCircle(10), Module.PrimaryActor.Position, default, WorldState.FutureTime(6)); + CurStance = Stance.Staff; + AOEImminent = true; + } - switch (CurStance) + if ((SID)status.ID == SID.HydaelynsWeapon && status.Extra == 0x1B5) { - case Stance.Sword: - _aoeSword.Draw(Arena, Module.PrimaryActor.Position, 0.Degrees()); - _aoeSword.Draw(Arena, Module.PrimaryActor.Position, 90.Degrees()); - break; - case Stance.Staff: - _aoeStaff.Draw(Arena, Module.PrimaryActor.Position); - break; - case Stance.Chakram: - _aoeChakram.Draw(Arena, Module.PrimaryActor.Position); - break; + _aoe = new(new AOEShapeDonut(5, 40), Module.PrimaryActor.Position, default, WorldState.FutureTime(6)); + AOEImminent = true; + CurStance = Stance.Chakram; } } - public override void OnStatusGain(Actor actor, ActorStatus status) + public override void OnStatusLose(Actor actor, ActorStatus status) { - if (actor != Module.PrimaryActor) - return; - - var newStance = (SID)status.ID switch + if ((SID)status.ID == SID.HydaelynsWeapon) { - SID.HerosMantle => Stance.Sword, - SID.MagosMantle => Stance.Staff, - SID.MousaMantle => Stance.Chakram, - _ => Stance.None - }; - - if (newStance == Stance.None || newStance == CurStance) - return; - - AOEImminent = CurStance != Stance.None; - CurStance = newStance; + _aoe = new(new AOEShapeCross(40, 5), Module.PrimaryActor.Position, default, WorldState.FutureTime(6.9f)); + AOEImminent = true; + CurStance = Stance.Sword; + } } public override void OnEventCast(Actor caster, ActorCastEvent spell) { if ((AID)spell.Action.ID is AID.WeaponChangeAOEChakram or AID.WeaponChangeAOEStaff or AID.WeaponChangeAOESword) + { AOEImminent = false; + _aoe = null; + } } } diff --git a/BossMod/Modules/Endwalker/Trial/T02Hydaelyn/ParhelicCircle.cs b/BossMod/Modules/Endwalker/Trial/T02Hydaelyn/ParhelicCircle.cs index 1b4826cdc1..7fda6f8552 100644 --- a/BossMod/Modules/Endwalker/Trial/T02Hydaelyn/ParhelicCircle.cs +++ b/BossMod/Modules/Endwalker/Trial/T02Hydaelyn/ParhelicCircle.cs @@ -2,50 +2,32 @@ namespace BossMod.Endwalker.Trial.T02Hydaelyn; class ParhelicCircle(BossModule module) : Components.GenericAOEs(module) { - private const float _triRadius = 8; - private const float _hexRadius = 17; + private readonly List _aoes = []; private static readonly AOEShapeCircle _circle = new(6); - private DateTime _activation; - public override IEnumerable ActiveAOEs(int slot, Actor actor) - { - if (_activation != default) - { - // there are 10 orbs: 1 in center, 3 in vertices of a triangle with radius=8, 6 in vertices of a hexagon with radius=17 - // note: i'm not sure how exactly orientation is determined, it seems to be related to eventobj rotations... - var hex = Module.Enemies(OID.RefulgenceHexagon).FirstOrDefault(); - var tri = Module.Enemies(OID.RefulgenceTriangle).FirstOrDefault(); - if (hex != null && tri != null) - { - var c = Module.Bounds.Center; - yield return new(_circle, c, default, _activation); - yield return new(_circle, c + _triRadius * (tri.Rotation + 60.Degrees()).ToDirection(), default, _activation); - yield return new(_circle, c + _triRadius * (tri.Rotation + 180.Degrees()).ToDirection(), default, _activation); - yield return new(_circle, c + _triRadius * (tri.Rotation - 60.Degrees()).ToDirection(), default, _activation); - yield return new(_circle, c + _hexRadius * hex.Rotation.ToDirection(), default, _activation); - yield return new(_circle, c + _hexRadius * (hex.Rotation + 60.Degrees()).ToDirection(), default, _activation); - yield return new(_circle, c + _hexRadius * (hex.Rotation + 120.Degrees()).ToDirection(), default, _activation); - yield return new(_circle, c + _hexRadius * (hex.Rotation + 180.Degrees()).ToDirection(), default, _activation); - yield return new(_circle, c + _hexRadius * (hex.Rotation - 120.Degrees()).ToDirection(), default, _activation); - yield return new(_circle, c + _hexRadius * (hex.Rotation - 60.Degrees()).ToDirection(), default, _activation); - } - } - } + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; public override void OnActorCreated(Actor actor) { - if ((OID)actor.OID is OID.RefulgenceHexagon) - _activation = WorldState.FutureTime(8.9f); + var activation = WorldState.FutureTime(8.9f); + var c = Module.Bounds.Center; + if ((OID)actor.OID == OID.RefulgenceHexagon) + { + _aoes.Add(new(_circle, c, default, activation)); + for (int i = 1; i < 7; ++i) + _aoes.Add(new(_circle, Helpers.RotateAroundOrigin(i * 60, c, c + 17 * actor.Rotation.ToDirection()), default, activation)); + } + if ((OID)actor.OID == OID.RefulgenceTriangle) + for (int i = 1; i < 4; ++i) + _aoes.Add(new(_circle, Helpers.RotateAroundOrigin(-60 + i * 120, c, c + 8 * actor.Rotation.ToDirection()), default, activation)); } public override void OnCastFinished(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.Incandescence) - ++NumCasts; - if (NumCasts == 10) { - NumCasts = 0; - _activation = default; + ++NumCasts; + _aoes.Clear(); } } } diff --git a/BossMod/Modules/Endwalker/Trial/T02Hydaelyn/T02HydaelynEnums.cs b/BossMod/Modules/Endwalker/Trial/T02Hydaelyn/T02HydaelynEnums.cs index 77a653d6f5..7cc42c3012 100644 --- a/BossMod/Modules/Endwalker/Trial/T02Hydaelyn/T02HydaelynEnums.cs +++ b/BossMod/Modules/Endwalker/Trial/T02Hydaelyn/T02HydaelynEnums.cs @@ -24,7 +24,7 @@ public enum AID : uint Teleport2 = 28282, // Boss->location, no cast, single-target Teleport3 = 26030, // 3503->location, no cast, single-target, Mystic Refulgence teleports DawnMantle = 27660, // Boss->self, 4,9s cast, single-target - Anthelion = 26056, // Boss->self, no cast, range 5-40 donut (not sure if 5.04 or 5, impossible to tell with naked eye) + Anthelion = 26056, // Boss->self, no cast, range 5-40 donut MousasScorn = 26070, // Boss->players, 5,0s cast, range 4 circle, shared tankbuster HighestHoly = 26055, // Boss->self, no cast, range 10 circle MagossRadiance = 26072, // Boss->self, 5,0s cast, range 40 circle diff --git a/BossMod/Util/ShapeDistance.cs b/BossMod/Util/ShapeDistance.cs index 45d818df1b..5484e2db2f 100644 --- a/BossMod/Util/ShapeDistance.cs +++ b/BossMod/Util/ShapeDistance.cs @@ -146,14 +146,14 @@ public static Func Cross(WPos origin, Angle direction, float length public static Func ConvexPolygon(IEnumerable vertices, bool cw, float offset = 0) { List<(WPos point, WDir normal)> edges = []; - Action addEdge = (p1, p2) => + void addEdge(WPos p1, WPos p2) { if (p1 != p2) { var dir = (p2 - p1).Normalized(); edges.Add((p1, cw ? dir.OrthoL() : dir.OrthoR())); } - }; + } var en = vertices.GetEnumerator(); if (!en.MoveNext())