diff --git a/BossMod/Autorotation/UIRotationWindow.cs b/BossMod/Autorotation/UIRotationWindow.cs index ac18e2690e..e899b5e4a2 100644 --- a/BossMod/Autorotation/UIRotationWindow.cs +++ b/BossMod/Autorotation/UIRotationWindow.cs @@ -84,7 +84,7 @@ public override void Draw() if (newSel >= 0 && _mgr.Preset != null) { ImGui.SameLine(); - using var style = ImRaii.PushColor(ImGuiCol.Text, 0xff00ffff); + using var style = ImRaii.PushColor(ImGuiCol.Text, Colors.TextColor2); UIMisc.HelpMarker(() => "You have a preset activated, which fully overrides the CD plan!", FontAwesomeIcon.ExclamationTriangle); } } diff --git a/BossMod/BossModule/AOEShapes.cs b/BossMod/BossModule/AOEShapes.cs index 93e1f9a4a2..0efc6b80bf 100644 --- a/BossMod/BossModule/AOEShapes.cs +++ b/BossMod/BossModule/AOEShapes.cs @@ -192,53 +192,86 @@ public enum OperandType // if the origin of the AOE can change, edit the origin default value to prevent cache issues public sealed record class AOEShapeCustom(IEnumerable Shapes1, IEnumerable? DifferenceShapes = null, IEnumerable? Shapes2 = null, bool InvertForbiddenZone = false, OperandType Operand = OperandType.Union, WPos Origin = default) : AOEShape { - private RelSimplifiedComplexPolygon? polygon; + public RelSimplifiedComplexPolygon? Polygon; private PolygonWithHolesDistanceFunction? shapeDistance; private readonly int hashkey = CreateCacheKey(Shapes1, Shapes2 ?? [], DifferenceShapes ?? [], Operand, Origin); - public static readonly Dictionary Cache = []; - public static readonly LinkedList CacheOrder = new(); + private static readonly Dictionary cache = []; + private static readonly LinkedList cacheOrder = new(); public void AddToCache(RelSimplifiedComplexPolygon value) { - if (Cache.Count >= 50) + if (cache.Count >= 50) { - var lruKey = CacheOrder.Last?.Value; + var lruKey = cacheOrder.Last?.Value; if (lruKey != null) { - Cache.Remove(lruKey.Value); - CacheOrder.RemoveLast(); + cache.Remove(lruKey.Value); + cacheOrder.RemoveLast(); } } - Cache[hashkey] = value; - CacheOrder.Remove(hashkey); - CacheOrder.AddFirst(hashkey); + cache[hashkey] = value; + cacheOrder.Remove(hashkey); + cacheOrder.AddFirst(hashkey); } + public override string ToString() => $"Custom AOE shape: hashkey={hashkey}, ifz={InvertForbiddenZone}"; - private RelSimplifiedComplexPolygon GetCombinedPolygon(WPos origin) + public RelSimplifiedComplexPolygon GetCombinedPolygon(WPos origin) { - if (Cache.TryGetValue(hashkey, out var cachedResult)) // for moving custom AOEs we don't want to recalculate the polygon every frame since they move at server ticks and not frame + if (cache.TryGetValue(hashkey, out var cachedResult)) { - CacheOrder.Remove(hashkey); - CacheOrder.AddFirst(hashkey); - return polygon = cachedResult; + cacheOrder.Remove(hashkey); + cacheOrder.AddFirst(hashkey); + return Polygon = cachedResult; } + var shapes1 = CreateOperandFromShapes(Shapes1, origin); - var shapes2 = CreateOperandFromShapes(Shapes2, origin); var differenceOperands = CreateOperandFromShapes(DifferenceShapes, origin); var clipper = new PolygonClipper(); - var combinedShapes = Operand switch + if (Shapes2 == null) { - OperandType.Xor => clipper.Xor(shapes1, shapes2), - OperandType.Intersection => clipper.Intersect(shapes1, shapes2), - _ => null - }; - - polygon = combinedShapes != null - ? clipper.Difference(new PolygonClipper.Operand(combinedShapes), differenceOperands) - : clipper.Difference(shapes1, differenceOperands); - AddToCache(polygon); - return polygon; + if (DifferenceShapes != null) + { + Polygon = clipper.Difference(shapes1, differenceOperands); + AddToCache(Polygon); + return Polygon; + } + else + { + Polygon = clipper.Simplify(shapes1); + AddToCache(Polygon); + return Polygon; + } + } + if (Shapes2 != null) + { + Polygon = clipper.Simplify(shapes1); + foreach (var shape in Shapes2) + { + var singleShapeOperand = CreateOperandFromShape(shape, origin); + + switch (Operand) + { + case OperandType.Intersection: + Polygon = clipper.Intersect(new PolygonClipper.Operand(Polygon), singleShapeOperand); + break; + case OperandType.Xor: + Polygon = clipper.Xor(new PolygonClipper.Operand(Polygon), singleShapeOperand); + break; + } + } + Polygon = DifferenceShapes != null ? clipper.Difference(new PolygonClipper.Operand(Polygon), differenceOperands) : Polygon; + AddToCache(Polygon); + return Polygon; + } + return new(); + } + + private static PolygonClipper.Operand CreateOperandFromShape(Shape shape, WPos origin) + { + var operand = new PolygonClipper.Operand(); + operand.AddPolygon(shape.ToPolygon(origin)); + return operand; } private static PolygonClipper.Operand CreateOperandFromShapes(IEnumerable? shapes, WPos origin) @@ -252,8 +285,8 @@ private static PolygonClipper.Operand CreateOperandFromShapes(IEnumerable public override bool Check(WPos position, WPos origin, Angle rotation) { - var relativePosition = position - origin; - var result = (polygon ?? GetCombinedPolygon(origin)).Contains(new(relativePosition.X, relativePosition.Z)); + var (x, z) = position - origin; + var result = (Polygon ?? GetCombinedPolygon(origin)).Contains(new(x, z)); return result; } @@ -269,12 +302,12 @@ private static int CreateCacheKey(IEnumerable shapes1, IEnumerable public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = 0) { - arena.ZoneRelPoly(hashkey, polygon ?? GetCombinedPolygon(origin), color); + arena.ZoneRelPoly(hashkey, Polygon ?? GetCombinedPolygon(origin), color); } public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = 0) { - var combinedPolygon = polygon ?? GetCombinedPolygon(origin); + var combinedPolygon = Polygon ?? GetCombinedPolygon(origin); for (var i = 0; i < combinedPolygon.Parts.Count; ++i) { var part = combinedPolygon.Parts[i]; @@ -305,7 +338,7 @@ public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint public override Func Distance(WPos origin, Angle rotation) { - shapeDistance ??= new PolygonWithHolesDistanceFunction(origin, polygon ?? GetCombinedPolygon(origin)); + shapeDistance ??= new PolygonWithHolesDistanceFunction(origin, Polygon ?? GetCombinedPolygon(origin)); var dis = shapeDistance.Value.Distance; return InvertForbiddenZone ? p => -dis(p) : dis; } @@ -313,15 +346,13 @@ public override Func Distance(WPos origin, Angle rotation) public sealed record class AOEShapeCustomAlt(RelSimplifiedComplexPolygon Poly, Angle DirectionOffset = default, bool InvertForbiddenZone = false) : AOEShape { - private PolygonWithHolesDistanceFunction? shapeDistance; public override string ToString() => $"Custom: off={DirectionOffset}, ifz={InvertForbiddenZone}"; public override bool Check(WPos position, WPos origin, Angle rotation) => Poly.Contains((position - origin).Rotate(-rotation - DirectionOffset)); public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = 0) => arena.ZoneComplex(origin, rotation + DirectionOffset, Poly, color); public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = 0) => arena.AddComplexPolygon(origin, (rotation + DirectionOffset).ToDirection(), Poly, color); public override Func Distance(WPos origin, Angle rotation) { - shapeDistance ??= new PolygonWithHolesDistanceFunction(origin, Poly); - var dis = shapeDistance.Value.Distance; - return InvertForbiddenZone ? p => -dis(p) : dis; + var shapeDistance = new PolygonWithHolesDistanceFunction(origin, Poly.Transform(default, (-rotation - DirectionOffset).ToDirection())).Distance; + return InvertForbiddenZone ? p => -shapeDistance(p) : shapeDistance; } } diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index 56f0eb5cce..368f56ea0f 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -128,7 +128,7 @@ public override WDir ClampToBounds(WDir offset) private Pathfinding.Map BuildMap() { var map = new Pathfinding.Map(MapResolution, default, Radius, Radius); - map.BlockPixelsInsideConvex(p => -ShapeDistance.Circle(default, Radius)(p), float.NegativeInfinity, 0); + map.BlockPixelsInsideConvex(p => -ShapeDistance.Circle(default, Radius)(p), -1, 0); return map; } } @@ -149,7 +149,7 @@ private static float CalculateScaleFactor(Angle Rotation) private Pathfinding.Map BuildMap() { var map = new Pathfinding.Map(MapResolution, default, HalfWidth, HalfHeight, Rotation); - map.BlockPixelsInsideConvex(p => -ShapeDistance.Rect(default, Rotation, HalfHeight, HalfHeight, HalfWidth)(p), float.NegativeInfinity, 0); + map.BlockPixelsInsideConvex(p => -ShapeDistance.Rect(default, Rotation, HalfHeight, HalfHeight, HalfWidth)(p), -1, 0); return map; } @@ -158,8 +158,6 @@ private Pathfinding.Map BuildMap() public override WDir ClampToBounds(WDir offset) { - if (offset.X == default) // if actor is almost in the center of the arena, do nothing - return offset; var offsetX = offset.Dot(Orientation.OrthoL()); var offsetY = offset.Dot(Orientation); if (Math.Abs(offsetX) > HalfWidth) @@ -325,7 +323,7 @@ private Pathfinding.Map BuildMap() } ref var pixel = ref pixels[rowOffset + x]; - pixel.MaxG = allInside ? float.MaxValue : float.NegativeInfinity; + pixel.MaxG = allInside ? float.MaxValue : -1; } }); diff --git a/BossMod/BossModule/Shapes.cs b/BossMod/BossModule/Shapes.cs index 98c780a6d6..13822bdff3 100644 --- a/BossMod/BossModule/Shapes.cs +++ b/BossMod/BossModule/Shapes.cs @@ -239,10 +239,10 @@ public override List Contour(WPos center) var angleIncrement = Angle.DoublePI / Edges; var initialRotation = Rotation.Rad; var vertices = new List(Edges); - for (var i = Edges - 1; i >= 0; --i) + for (var i = 0; i < Edges; ++i) { var (sin, cos) = ((float, float))Math.SinCos(i * angleIncrement + initialRotation); - vertices.Add(new(Center.X + Radius * cos, Center.Z + Radius * sin)); + vertices.Add(new(Center.X + Radius * sin, Center.Z + Radius * cos)); } Points = [.. vertices]; } @@ -303,7 +303,7 @@ public override List Contour(WPos center) public sealed record class DonutSegmentHA(WPos Center, float InnerRadius, float OuterRadius, Angle CenterDir, Angle HalfAngle) : DonutSegment(Center, InnerRadius, OuterRadius, CenterDir - HalfAngle, CenterDir + HalfAngle); -// Approximates a cone with a customizable number of edges for the circle arc +// Approximates a cone with a customizable number of edges for the circle arc - with 1 edge this turns into a triangle, 2 edges result in a parallelogram public sealed record class ConeV(WPos Center, float Radius, Angle CenterDir, Angle HalfAngle, int Edges) : Shape { public override List Contour(WPos center) @@ -313,11 +313,10 @@ public override List Contour(WPos center) var angleIncrement = 2 * HalfAngle.Rad / Edges; var startAngle = CenterDir.Rad - HalfAngle.Rad; var vertices = new List(Edges + 1); - for (var i = 0; i < Edges + 1; ++i) { var (sin, cos) = ((float, float))Math.SinCos(startAngle + i * angleIncrement); - vertices.Add(new(Center.X + Radius * cos, Center.Z + Radius * sin)); + vertices.Add(new(Center.X + Radius * sin, Center.Z + Radius * cos)); } vertices.Add(Center - new WPos()); Points = [.. vertices]; @@ -342,17 +341,15 @@ public override List Contour(WPos center) var angleIncrement = 2 * HalfAngle.Rad / Edges; var startAngle = CenterDir.Rad - HalfAngle.Rad; var vertices = new List(2 * (Edges + 1)); - - for (var i = Edges; i >= 0; --i) + for (var i = 0; i < Edges + 1; ++i) { var (sin, cos) = ((float, float))Math.SinCos(startAngle + i * angleIncrement); - vertices.Add(new(Center.X + OuterRadius * cos, Center.Z + OuterRadius * sin)); + vertices.Add(new(Center.X + OuterRadius * sin, Center.Z + OuterRadius * cos)); } - - for (var i = 0; i < Edges + 1; ++i) + for (var i = Edges; i >= 0; --i) { var (sin, cos) = ((float, float))Math.SinCos(startAngle + i * angleIncrement); - vertices.Add(new(Center.X + InnerRadius * cos, Center.Z + InnerRadius * sin)); + vertices.Add(new(Center.X + InnerRadius * sin, Center.Z + InnerRadius * cos)); } Points = [.. vertices]; } @@ -366,6 +363,7 @@ public override List Contour(WPos center) public override string ToString() => $"DonutSegmentV:{Center.X},{Center.Z},{InnerRadius},{OuterRadius},{CenterDir},{HalfAngle},{Edges}"; } +// Approximates a donut with a customizable number of edges per circle arc public sealed record class DonutV(WPos Center, float InnerRadius, float OuterRadius, int Edges) : Shape { public override List Contour(WPos center) @@ -378,13 +376,13 @@ public override List Contour(WPos center) for (var i = 0; i <= Edges; ++i) { var (sin, cos) = ((float, float))Math.SinCos(i * angleIncrement); - vertices.Add(new(Center.X + OuterRadius * cos, Center.Z + OuterRadius * sin)); + vertices.Add(new(Center.X + OuterRadius * sin, Center.Z + OuterRadius * cos)); } for (var i = Edges; i >= 0; --i) { var (sin, cos) = ((float, float))Math.SinCos(i * angleIncrement); - vertices.Add(new(Center.X + InnerRadius * cos, Center.Z + InnerRadius * sin)); + vertices.Add(new(Center.X + InnerRadius * sin, Center.Z + InnerRadius * cos)); } Points = [.. vertices]; } diff --git a/BossMod/Components/Gaze.cs b/BossMod/Components/Gaze.cs index 75faf83c6d..ff6abfd563 100644 --- a/BossMod/Components/Gaze.cs +++ b/BossMod/Components/Gaze.cs @@ -49,7 +49,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) foreach (var eye in ActiveEyes(pcSlot, pc)) { var danger = HitByEye(pc, eye) != Inverted; - var eyeCenter = Arena.WorldPositionToScreenPosition(eye.Position); + var eyeCenter = IndicatorScreenPos(eye.Position); DrawEye(eyeCenter, danger); if (pc.Position.InCircle(eye.Position, eye.Range)) @@ -71,6 +71,23 @@ public static void DrawEye(Vector2 eyeCenter, bool danger) } private static bool HitByEye(Actor actor, Eye eye) => (actor.Rotation + eye.Forward).ToDirection().Dot((eye.Position - actor.Position).Normalized()) >= 0.707107f; // 45-degree + + private Vector2 IndicatorScreenPos(WPos eye) + { + if (Arena.InBounds(eye) || Arena.Bounds is not ArenaBoundsCircle and not ArenaBoundsSquare) + { + return Arena.WorldPositionToScreenPosition(eye); + } + else if (Arena.Bounds is ArenaBoundsRect) + { + return Arena.WorldPositionToScreenPosition(Arena.ClampToBounds(eye) + 2 * (eye - Arena.Center).Normalized()); + } + else + { + var dir = (eye - Arena.Center).Normalized(); + return Arena.ScreenCenter + Arena.RotatedCoords(dir.ToVec2()) * (Arena.ScreenHalfSize + Arena.ScreenMarginSize * 0.5f); + } + } } // gaze that happens on cast end diff --git a/BossMod/Config/ColorConfig.cs b/BossMod/Config/ColorConfig.cs index c262de948a..4c3b6c28a9 100644 --- a/BossMod/Config/ColorConfig.cs +++ b/BossMod/Config/ColorConfig.cs @@ -44,7 +44,7 @@ public sealed class ColorConfig : ConfigNode public Color ArenaMeleeRangeIndicator = new(0xffff0000); [PropertyDisplay("Arena: other")] - public Color[] ArenaOther = [new(0xffff0080), new(0xff8080ff), new(0xff80ff80), new(0xffff8040), new(0xff40c0c0), new(0x40008080)]; + public Color[] ArenaOther = [new(0xffff0080), new(0xff8080ff), new(0xff80ff80), new(0xffff8040), new(0xff40c0c0), new(0x40008080), new(0xffffff00), new(0xffff8000)]; [PropertyDisplay("Arena: interesting player, important for a mechanic")] public Color ArenaPlayerInteresting = new(0xffc0c0c0); diff --git a/BossMod/Debug/DebugObstacles.cs b/BossMod/Debug/DebugObstacles.cs index eae95bc331..2144e26f8a 100644 --- a/BossMod/Debug/DebugObstacles.cs +++ b/BossMod/Debug/DebugObstacles.cs @@ -96,7 +96,7 @@ protected override void DrawSidebar() var py = (int)playerOffset.Z; var playerDeepInObstacle = px >= 0 && py >= 0 && px < Bitmap.Width && py < Bitmap.Height && Bitmap[px, py] && (px == 0 || Bitmap[px - 1, py]) && (py == 0 || Bitmap[px, py - 1]) && (px == (Bitmap.Width - 1) || Bitmap[px + 1, py]) && (py == (Bitmap.Height - 1) || Bitmap[px, py + 1]); - using var color = ImRaii.PushColor(ImGuiCol.Text, 0xff0000ff, playerDeepInObstacle); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TextColor3, playerDeepInObstacle); ImGui.TextUnformatted($"Player cell: {px}x{py}"); if (playerDeepInObstacle) { @@ -110,10 +110,10 @@ protected override void DrawSidebar() var y = player?.PosRot.Y ?? 0; var tl = e.Origin + new WDir(HoveredPixel.x, HoveredPixel.y) * Bitmap.PixelSize; var br = tl + new WDir(Bitmap.PixelSize, Bitmap.PixelSize); - Camera.Instance?.DrawWorldLine(new(tl.X, y, tl.Z), new(tl.X, y, br.Z), 0xff00ffff); - Camera.Instance?.DrawWorldLine(new(tl.X, y, br.Z), new(br.X, y, br.Z), 0xff00ffff); - Camera.Instance?.DrawWorldLine(new(br.X, y, br.Z), new(br.X, y, tl.Z), 0xff00ffff); - Camera.Instance?.DrawWorldLine(new(br.X, y, tl.Z), new(tl.X, y, tl.Z), 0xff00ffff); + Camera.Instance?.DrawWorldLine(new(tl.X, y, tl.Z), new(tl.X, y, br.Z), Colors.TextColor2); + Camera.Instance?.DrawWorldLine(new(tl.X, y, br.Z), new(br.X, y, br.Z), Colors.TextColor2); + Camera.Instance?.DrawWorldLine(new(br.X, y, br.Z), new(br.X, y, tl.Z), Colors.TextColor2); + Camera.Instance?.DrawWorldLine(new(br.X, y, tl.Z), new(tl.X, y, tl.Z), Colors.TextColor2); } } @@ -123,7 +123,7 @@ protected override void DrawSidebar() if (player != null) { var playerOffset = ((player.Position - e.Origin) / Bitmap.PixelSize).Floor(); - yield return ((int)playerOffset.X, (int)playerOffset.Z, new(0xff00ff00)); + yield return ((int)playerOffset.X, (int)playerOffset.Z, new(Colors.Safe)); } } } diff --git a/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/Fusefield.cs b/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/Fusefield.cs index c95126ae2a..50d0b051d7 100644 --- a/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/Fusefield.cs +++ b/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/Fusefield.cs @@ -13,8 +13,9 @@ public override void AddHints(int slot, Actor actor, TextHints hints) public override void DrawArenaForeground(int pcSlot, Actor pc) { - foreach (var s in _sparks) + for (var i = 0; i < _sparks.Count; ++i) { + var s = _sparks[i]; if (s.order == _orders[pcSlot]) { Arena.AddLine(s.spark.Position, s.target.Position, Colors.Safe); @@ -50,13 +51,31 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } -class FusefieldVoidzone(BossModule module) : Components.PersistentVoidzone(module, 5, m => m.Enemies(OID.Boss)) +class FusefieldVoidzone(BossModule module) : Components.GenericAOEs(module) { public bool Active; + private static readonly AOEShapeCircle circle = new(5); + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); public override void OnEventEnvControl(byte index, uint state) { - if (index == 19 && state is 0x00200010 or 0x00080004) - Active = state == 0x00200010; + if (index == 0x13) + switch (state) + { + case 0x00020001: + _aoe = new(circle, Arena.Center, default, WorldState.FutureTime(9.1f)); + break; + case 0x00200010: + Arena.Bounds = M03SBruteBomber.FuseFieldBounds; + _aoe = null; + Active = true; + break; + case 0x00080004: + Arena.Bounds = M03SBruteBomber.DefaultBounds; + Active = false; + break; + } } } diff --git a/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/M03SBruteBomber.cs b/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/M03SBruteBomber.cs index 997a226ee6..43dbe858a4 100644 --- a/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/M03SBruteBomber.cs +++ b/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/M03SBruteBomber.cs @@ -3,4 +3,9 @@ class BrutalImpact(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.BrutalImpactAOE)); [ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "Malediktus", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 990, NameID = 13356, PlanLevel = 100)] -public class M03SBruteBomber(WorldState ws, Actor primary) : Raid.M03NBruteBomber.M03NBruteBomber(ws, primary); +public class M03SBruteBomber(WorldState ws, Actor primary) : BossModule(ws, primary, arenaCenter, DefaultBounds) +{ + private static readonly WPos arenaCenter = new(100, 100); + public static readonly ArenaBoundsSquare DefaultBounds = new(15); + public static readonly ArenaBoundsComplex FuseFieldBounds = new([new Square(arenaCenter, 15)], [new Polygon(arenaCenter, 5, 60)]); +} diff --git a/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/M03SBruteBomberStates.cs b/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/M03SBruteBomberStates.cs index 3054cfc2f8..c8bf73ca4c 100644 --- a/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/M03SBruteBomberStates.cs +++ b/BossMod/Modules/Dawntrail/Savage/M03SBruteBomber/M03SBruteBomberStates.cs @@ -156,9 +156,9 @@ private void FinalFusedown(uint id, float delay) private void Fusefield(uint id, float delay) { Cast(id, AID.Fusefield, delay, 4) + .ActivateOnEnter() .ActivateOnEnter(); - Cast(id + 0x10, AID.BombarianFlame, 3.2f, 3) - .ActivateOnEnter(); + Cast(id + 0x10, AID.BombarianFlame, 3.2f, 3); ComponentCondition(id + 0x20, 3.9f, comp => comp.Active, "Fuses start"); ComponentCondition(id + 0x30, 40, comp => !comp.Active, "Fuses resolve") .DeactivateOnExit() diff --git a/BossMod/Modules/Endwalker/Alliance/A23Halone/A23Halone.cs b/BossMod/Modules/Endwalker/Alliance/A23Halone/A23Halone.cs index c6da92b8b4..f0347f9ba2 100644 --- a/BossMod/Modules/Endwalker/Alliance/A23Halone/A23Halone.cs +++ b/BossMod/Modules/Endwalker/Alliance/A23Halone/A23Halone.cs @@ -11,4 +11,8 @@ 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 = "Malediktus", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 911, NameID = 12064)] -public class A23Halone(WorldState ws, Actor primary) : BossModule(ws, primary, new(-700, 600), Octagons.arenaDefault); +public class A23Halone(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, DefaultBounds) +{ + public static readonly WPos ArenaCenter = new(-700, 600); + public static readonly ArenaBoundsCircle DefaultBounds = new(29.5f); +} diff --git a/BossMod/Modules/Endwalker/Alliance/A23Halone/A23HaloneStates.cs b/BossMod/Modules/Endwalker/Alliance/A23Halone/A23HaloneStates.cs index 71189bc974..f0164321cb 100644 --- a/BossMod/Modules/Endwalker/Alliance/A23Halone/A23HaloneStates.cs +++ b/BossMod/Modules/Endwalker/Alliance/A23Halone/A23HaloneStates.cs @@ -184,7 +184,7 @@ private void AddPhase(uint id, float delay) .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit() - .OnExit(() => Module.Arena.Bounds = Octagons.arenaDefault) + .OnExit(() => Module.Arena.Bounds = A23Halone.DefaultBounds) .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 8f86b59c5a..7b835504a7 100644 --- a/BossMod/Modules/Endwalker/Alliance/A23Halone/Octagons.cs +++ b/BossMod/Modules/Endwalker/Alliance/A23Halone/Octagons.cs @@ -10,15 +10,15 @@ class Octagons(BossModule module) : Components.GenericAOEs(module) private const float OuterRadius = 13.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 Angle[] angle = [-37.5f.Degrees(), 22.5f.Degrees(), 37.5f.Degrees()]; private static readonly Shape[] 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 Shape[] baseArena = [new Circle(new WPos(-700, 600), 29.5f)]; + private static readonly Shape[] baseArena = [new Circle(A23Halone.ArenaCenter, 29.5f)]; private readonly List octagonsInner = []; private readonly List octagonsOuter = []; - public static readonly ArenaBoundsCircle arenaDefault = new(29.5f); + private AOEInstance? _aoe; private static readonly AOEShapeCustom customShape = new(baseArena, [shapes[0], shapes[2], shapes[4]]); @@ -31,7 +31,7 @@ public override void OnEventEnvControl(byte index, uint state) switch (state) { case 0x00100008 when index == 0x07: - _aoe = new(customShape, Arena.Center); + _aoe = new(customShape, Arena.Center, default, WorldState.FutureTime(9)); break; case 0x00020001 when index == 0x07: AddOctagons(); diff --git a/BossMod/Modules/Endwalker/Savage/P4S2Hesperos/WreathOfThorns4.cs b/BossMod/Modules/Endwalker/Savage/P4S2Hesperos/WreathOfThorns4.cs index 6503c2aca2..d9f3924842 100644 --- a/BossMod/Modules/Endwalker/Savage/P4S2Hesperos/WreathOfThorns4.cs +++ b/BossMod/Modules/Endwalker/Savage/P4S2Hesperos/WreathOfThorns4.cs @@ -115,7 +115,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) return; // pc is not tethered anymore, nothing to draw... var pcIcon = _playerIcons[pcSlot]; - Arena.AddLine(pc.Position, pcTetherSource.Position, pcIcon == IconID.AkanthaiWater ? 0xffff8000 : 0xffff00ff); + Arena.AddLine(pc.Position, pcTetherSource.Position, pcIcon == IconID.AkanthaiWater ? Colors.Other8 : Colors.Vulnerable); if (_doneTowers < 4) { diff --git a/BossMod/Modules/Endwalker/Savage/P7SAgdistis/ForbiddenFruitCommon.cs b/BossMod/Modules/Endwalker/Savage/P7SAgdistis/ForbiddenFruitCommon.cs index 3db65cd844..408daec785 100644 --- a/BossMod/Modules/Endwalker/Savage/P7SAgdistis/ForbiddenFruitCommon.cs +++ b/BossMod/Modules/Endwalker/Savage/P7SAgdistis/ForbiddenFruitCommon.cs @@ -138,14 +138,14 @@ protected int TryAssignTether(Actor source, ActorTetherInfo tether) return -1; } - protected uint TetherColor(Actor source) => (OID)source.OID switch + protected static uint TetherColor(Actor source) => (OID)source.OID switch { - OID.ImmatureMinotaur => 0xffff00ff, - OID.BullTetherSource => 0xffffff00, - OID.ImmatureStymphalide => 0xff00ffff, + OID.ImmatureMinotaur => Colors.Vulnerable, + OID.BullTetherSource => Colors.Other7, + OID.ImmatureStymphalide => Colors.Danger, _ => 0 }; - protected int PlatformIDFromOffset(WDir offset) => offset.Z > 0 ? 1 : offset.X > 0 ? 2 : 0; - protected Angle PlatformDirection(int id) => (id - 1) * 120.Degrees(); + protected static int PlatformIDFromOffset(WDir offset) => offset.Z > 0 ? 1 : offset.X > 0 ? 2 : 0; + protected static Angle PlatformDirection(int id) => (id - 1) * 120.Degrees(); } diff --git a/BossMod/Modules/Endwalker/Unreal/Un2Sephirot/P1Ratzon.cs b/BossMod/Modules/Endwalker/Unreal/Un2Sephirot/P1Ratzon.cs index 256518fcea..7ef31ff01a 100644 --- a/BossMod/Modules/Endwalker/Unreal/Un2Sephirot/P1Ratzon.cs +++ b/BossMod/Modules/Endwalker/Unreal/Un2Sephirot/P1Ratzon.cs @@ -27,9 +27,9 @@ public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot public override void DrawArenaForeground(int pcSlot, Actor pc) { foreach (var (slot, actor) in Raid.WithSlot().IncludedInMask(_greenTargets)) - Arena.AddCircle(actor.Position, _greenRadius, 0xff00ff00, slot == pcSlot ? 2 : 1); + Arena.AddCircle(actor.Position, _greenRadius, Colors.Safe, slot == pcSlot ? 2 : 1); foreach (var (slot, actor) in Raid.WithSlot().IncludedInMask(_purpleTargets)) - Arena.AddCircle(actor.Position, _purpleRadius, 0xffff00ff, slot == pcSlot ? 2 : 1); + Arena.AddCircle(actor.Position, _purpleRadius, Colors.Vulnerable, slot == pcSlot ? 2 : 1); } public override void OnEventCast(Actor caster, ActorCastEvent spell) diff --git a/BossMod/Modules/Endwalker/Unreal/Un4Zurvan/P2BrokenSeal.cs b/BossMod/Modules/Endwalker/Unreal/Un4Zurvan/P2BrokenSeal.cs index a701375de8..efd5e8dc10 100644 --- a/BossMod/Modules/Endwalker/Unreal/Un4Zurvan/P2BrokenSeal.cs +++ b/BossMod/Modules/Endwalker/Unreal/Un4Zurvan/P2BrokenSeal.cs @@ -47,7 +47,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) var partner = state.Color != Color.None && state.Partner >= 0 ? Raid[state.Partner] : null; if (partner != null) { - Arena.AddLine(pc.Position, partner.Position, state.Color == Color.Fire ? 0xff0080ff : 0xffff8000, state.TooFar ? 2 : 1); + Arena.AddLine(pc.Position, partner.Position, state.Color == Color.Fire ? Colors.Object : Colors.Other8, state.TooFar ? 2 : 1); } foreach (var t in _fireTowers) diff --git a/BossMod/Modules/Global/MaskedCarnivale/Stage32AGoldenOpportunity/Stage32Act2.cs b/BossMod/Modules/Global/MaskedCarnivale/Stage32AGoldenOpportunity/Stage32Act2.cs index afcc7d24ed..0f7dad1b52 100644 --- a/BossMod/Modules/Global/MaskedCarnivale/Stage32AGoldenOpportunity/Stage32Act2.cs +++ b/BossMod/Modules/Global/MaskedCarnivale/Stage32AGoldenOpportunity/Stage32Act2.cs @@ -7,12 +7,13 @@ public enum OID : uint GildedGolem = 0x3FA9, //R=2.1 GildedMarionette = 0x3FA8, //R=1.2 GildedCyclops = 0x3FAA, //R=3.2 - Helper = 0x233C, + Helper = 0x233C } public enum AID : uint { AutoAttack = 34444, // Boss->player, no cast, single-target + ShiningSummon = 34461, // Boss->self, 4.0s cast, single-target Teleport = 34129, // Helper->location, no cast, single-target GoldorAeroIII = 34460, // Boss->self, 4.0s cast, range 50 circle, raidwide, knockback 10 away from source @@ -34,7 +35,7 @@ public enum AID : uint GoldorRush = 34468, // Boss->self, 4.0s cast, range 50 circle, knockback 10 away from source, raidwide GoldorFireIII = 34449, // Helper->location, 2.5s cast, range 8 circle GoldorBlizzardIIIVisual = 34589, // Boss->self, 6.0s cast, single-target, interruptible, freezes player - GoldorBlizzardIII = 34590, // Helper->player, no cast, range 6 circle + GoldorBlizzardIII = 34590 // Helper->player, no cast, range 6 circle } public enum SID : uint @@ -42,13 +43,60 @@ public enum SID : uint Heavy = 240, // Helper->player, extra=0x63 Electrocution = 3779, // Helper->player, extra=0x0 MagicDamageUp = 3707, // none->Boss, extra=0x0 - MagicResistance = 3621, // none->Boss, extra=0x0 + MagicResistance = 3621 // none->Boss, extra=0x0 } class GoldorFireIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.GoldorFireIII), 8); class GoldorBlast(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GoldorBlast), new AOEShapeRect(60, 5)); class GoldenCross(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GoldenCross), new AOEShapeCross(100, 3.5f)); -class GoldenBeam(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.GoldenBeam), new AOEShapeCone(40, 60.Degrees())); + +class GoldenBeam(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCone cone = new(40, 60.Degrees()); + private AOEInstance? _aoe; + private readonly List cones = []; + + 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.GoldenBeam) + { + if (Module.Enemies(OID.GildedMarionette).Count == 1) + _aoe = new(cone, caster.Position, spell.Rotation, Module.CastFinishAt(spell)); + else + { + cones.Add(new(caster.Position, cone.Radius, spell.Rotation, cone.HalfAngle, 2)); + if (cones.Count == 4) + { + AOEShapeCustom intersect = new([cones[0]], Shapes2: cones.Skip(1), Operand: OperandType.Intersection); + AOEShapeCustom xor = new([cones[0]], Shapes2: cones.Skip(1), Operand: OperandType.Xor); + var clipper = new PolygonClipper(); + var combinedShapes = clipper.Union(new PolygonClipper.Operand(intersect.GetCombinedPolygon(Arena.Center)), + new PolygonClipper.Operand(xor.GetCombinedPolygon(Arena.Center))); + _aoe = new(intersect with { Polygon = combinedShapes }, Arena.Center, default, Module.CastFinishAt(spell)); + } + } + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.GoldenBeam) + { + _aoe = null; + cones.Clear(); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + base.AddHints(slot, actor, hints); + if (_aoe != null && Module.Enemies(OID.GildedMarionette).Count > 1) + hints.Add("Use Diamondback outside of marked area!"); + } +} + class TwentyFourCaratSwing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TwentyFourCaratSwing), new AOEShapeCircle(12)); class GoldorQuake(BossModule module) : Components.ConcentricAOEs(module, _shapes) @@ -58,7 +106,7 @@ class GoldorQuake(BossModule module) : Components.ConcentricAOEs(module, _shapes public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.GoldorQuake1) - AddSequence(Module.PrimaryActor.Position, Module.CastFinishAt(spell)); + AddSequence(Arena.Center, Module.CastFinishAt(spell)); } public override void OnEventCast(Actor caster, ActorCastEvent spell) @@ -72,7 +120,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) AID.GoldorQuake3 => 2, _ => -1 }; - AdvanceSequence(order, Module.PrimaryActor.Position, WorldState.FutureTime(1.5f)); + AdvanceSequence(order, Arena.Center, WorldState.FutureTime(1.5f)); } } } @@ -108,9 +156,6 @@ public override void AddHints(int slot, Actor actor, TextHints hints) var heavy = actor.FindStatus(SID.Heavy); if (heavy != null) hints.Add("Use Loom to dodge AOEs!"); - var marionettes = Module.Enemies(OID.GildedMarionette).Count > 1; - if (marionettes) - hints.Add("Use Diamondback behind + between 2 marionettes!"); } } diff --git a/BossMod/Modules/Heavensward/Alliance/A31DeathgazeHollow/A31DeathgazeHollow.cs b/BossMod/Modules/Heavensward/Alliance/A31DeathgazeHollow/A31DeathgazeHollow.cs index 50a757d7de..5640ebe9c5 100644 --- a/BossMod/Modules/Heavensward/Alliance/A31DeathgazeHollow/A31DeathgazeHollow.cs +++ b/BossMod/Modules/Heavensward/Alliance/A31DeathgazeHollow/A31DeathgazeHollow.cs @@ -29,7 +29,7 @@ public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot public override void DrawArenaForeground(int pcSlot, Actor pc) { foreach (var (slot, actor) in Raid.WithSlot().IncludedInMask(_greenTargets)) - Arena.AddCircle(actor.Position, _greenRadius, 0xff00ff00, slot == pcSlot ? 2 : 1); + Arena.AddCircle(actor.Position, _greenRadius, Colors.Safe, slot == pcSlot ? 2 : 1); } public override void OnEventCast(Actor caster, ActorCastEvent spell) diff --git a/BossMod/Pathfinding/MapVisualizer.cs b/BossMod/Pathfinding/MapVisualizer.cs index fb23159fdf..36a0d830bf 100644 --- a/BossMod/Pathfinding/MapVisualizer.cs +++ b/BossMod/Pathfinding/MapVisualizer.cs @@ -153,7 +153,7 @@ public void Draw() } foreach (var l in Lines) { - dl.AddLine(tl + Map.WorldToGridFrac(l.origin) * ScreenPixelSize, tl + Map.WorldToGridFrac(l.dest) * ScreenPixelSize, 0xff0000ff); + dl.AddLine(tl + Map.WorldToGridFrac(l.origin) * ScreenPixelSize, tl + Map.WorldToGridFrac(l.dest) * ScreenPixelSize, Colors.TextColor3); } ImGui.SetCursorPos(cursorEnd); diff --git a/BossMod/Pathfinding/NavigationDecision.cs b/BossMod/Pathfinding/NavigationDecision.cs index 5abf22e3a3..90ec904957 100644 --- a/BossMod/Pathfinding/NavigationDecision.cs +++ b/BossMod/Pathfinding/NavigationDecision.cs @@ -47,12 +47,6 @@ public static NavigationDecision Build(Context ctx, WorldState ws, AIHints hints { if (targetRadius < 1) targetRadius = 1; // ensure targetRadius is at least 1 to prevent game from freezing - hints.PathfindMapBounds.PathfindMap(ctx.Map, hints.PathfindMapCenter); - - if (!Service.Config.Get().AllowAIToBeOutsideBounds && IsOutsideBounds(player.Position, ctx)) - { - return FindPathFromOutsideBounds(ctx, player.Position, playerSpeed); - } (Func shapeDistance, DateTime activation)[] localForbiddenZones = [.. hints.ForbiddenZones]; var imminent = ImminentExplosionTime(ws.CurrentTime); @@ -71,6 +65,18 @@ public static NavigationDecision Build(Context ctx, WorldState ws, AIHints hints } } + hints.PathfindMapBounds.PathfindMap(ctx.Map, hints.PathfindMapCenter); + + if (!Service.Config.Get().AllowAIToBeOutsideBounds && IsOutsideBounds(player.Position, ctx)) + { + for (var i = 0; i < len; ++i) + { + var zf = localForbiddenZones[i]; + AddBlockerZone(ctx.Map, imminent, zf.activation, zf.shapeDistance, forbiddenZoneCushion); + } + return FindPathFromOutsideBounds(ctx, player.Position, playerSpeed); + } + // Check whether player is inside each forbidden zone var inZone = new bool[len]; var inImminentForbiddenZone = false; @@ -316,9 +322,8 @@ public static NavigationDecision FindPathFromOutsideBounds(Context ctx, WPos sta foreach (var p in ctx.Map.EnumeratePixels()) { var px = ctx.Map[p.x, p.y]; - if (px.Priority == 0 && px.MaxG == float.MaxValue) + if (px.MaxG > 0) // assume any pixel not marked as blocked is better than being outside of bounds { - // safe pixel, candidate var distance = (p.center - startPos).LengthSq(); if (distance < closestDistance) { @@ -436,11 +441,10 @@ public static NavigationDecision FindPathFromImminent(ThetaStar pathfind, Map ma public static bool IsOutsideBounds(WPos position, Context ctx) { - if (ctx.Map.Pixels.Length == 0) - return false; - var gridPos = ctx.Map.WorldToGrid(position); - if (gridPos.x < 0 || gridPos.x >= ctx.Map.Width || gridPos.y < 0 || gridPos.y >= ctx.Map.Height) + var map = ctx.Map; + var (x, y) = map.WorldToGrid(position); + if (x < 0 || x >= map.Width || y < 0 || y >= map.Height) return true; // outside current pathfinding map - return ctx.Map.Pixels[gridPos.y * ctx.Map.Width + gridPos.x].MaxG == float.NegativeInfinity; // inside pathfinding map, but outside actual walkable bounds + return map.Pixels[y * map.Width + x].MaxG == -1; // inside pathfinding map, but outside actual walkable bounds } } diff --git a/BossMod/Replay/Analysis/StateTransitionTimings.cs b/BossMod/Replay/Analysis/StateTransitionTimings.cs index acef642754..35d116f6cb 100644 --- a/BossMod/Replay/Analysis/StateTransitionTimings.cs +++ b/BossMod/Replay/Analysis/StateTransitionTimings.cs @@ -108,9 +108,9 @@ UITree.NodeProperties map(KeyValuePair kv) var value = kv.Value.Instances.Count > 0 ? $"avg={kv.Value.AvgTime:f2}-{from.ExpectedTime:f2}={kv.Value.AvgTime - from.ExpectedTime:f2} +- {kv.Value.StdDev:f2}, [{kv.Value.MinTime:f2}, {kv.Value.MaxTime:f2}] range, {kv.Value.Instances.Count} seen" : $"never seen ({from.ExpectedTime:f2} expected)"; - var color = !kv.Value.Expected ? 0xff0080ff - : kv.Value.Instances.Count > 0 && Math.Abs(from.ExpectedTime - kv.Value.AvgTime) > Math.Ceiling(kv.Value.StdDev * 10) / 10 ? 0xff00ffff - : 0xffffffff; + var color = !kv.Value.Expected ? Colors.TextColor5 + : kv.Value.Instances.Count > 0 && Math.Abs(from.ExpectedTime - kv.Value.AvgTime) > Math.Ceiling(kv.Value.StdDev * 10) / 10 ? Colors.TextColor2 + : Colors.TextColor1; //bool warn = from.ExpectedTime < Math.Round(m.MinTime, 1) || from.ExpectedTime > Math.Round(m.MaxTime, 1); return new($"{name}: {value}###{name}", kv.Value.Instances.Count == 0, color); } diff --git a/BossMod/Replay/Analysis/TOPSpecific.cs b/BossMod/Replay/Analysis/TOPSpecific.cs index 342603de35..497f958d8d 100644 --- a/BossMod/Replay/Analysis/TOPSpecific.cs +++ b/BossMod/Replay/Analysis/TOPSpecific.cs @@ -42,7 +42,7 @@ public void Draw(UITree tree) { _plotFlamethrowers.Begin(); foreach (var i in _flamethrowers) - _plotFlamethrowers.Point(new(i.Difference.Deg, 0.5f), 0xff00ffff, () => $"{i.Replay.Path} @ {i.Timestamp:O}"); + _plotFlamethrowers.Point(new(i.Difference.Deg, 0.5f), Colors.TextColor2, () => $"{i.Replay.Path} @ {i.Timestamp:O}"); _plotFlamethrowers.End(); } } diff --git a/BossMod/Util/Color.cs b/BossMod/Util/Color.cs index 69ddf740bc..5654b830f7 100644 --- a/BossMod/Util/Color.cs +++ b/BossMod/Util/Color.cs @@ -70,6 +70,8 @@ public static class Colors public static uint Other4 => _config.ArenaOther[3].ABGR; public static uint Other5 => _config.ArenaOther[4].ABGR; public static uint Other6 => _config.ArenaOther[5].ABGR; + public static uint Other7 => _config.ArenaOther[6].ABGR; + public static uint Other8 => _config.ArenaOther[7].ABGR; public static uint Shadows => _config.Shadows.ABGR; public static uint CardinalN => _config.CardinalN.ABGR; public static uint CardinalE => _config.CardinalE.ABGR;