diff --git a/BossMod/AI/AIBehaviour.cs b/BossMod/AI/AIBehaviour.cs index 5edc33412c..55f0253529 100644 --- a/BossMod/AI/AIBehaviour.cs +++ b/BossMod/AI/AIBehaviour.cs @@ -35,7 +35,7 @@ public void Execute(Actor player, Actor master) if (_config.FocusTargetLeader) FocusMaster(master); - _afkMode = !master.InCombat && (WorldState.CurrentTime - _masterLastMoved).TotalSeconds > 10; + _afkMode = _config.AutoAFK && !master.InCombat && (WorldState.CurrentTime - _masterLastMoved).TotalSeconds > _config.AFKModeTimer; var gazeImminent = autorot.Hints.ForbiddenDirections.Count > 0 && autorot.Hints.ForbiddenDirections[0].activation <= WorldState.FutureTime(0.5f); var pyreticImminent = autorot.Hints.ImminentSpecialMode.mode == AIHints.SpecialMode.Pyretic && autorot.Hints.ImminentSpecialMode.activation <= WorldState.FutureTime(1); var forbidActions = _config.ForbidActions || _afkMode || gazeImminent || pyreticImminent; diff --git a/BossMod/AI/AIConfig.cs b/BossMod/AI/AIConfig.cs index 02a12d69c3..aff7cf9173 100644 --- a/BossMod/AI/AIConfig.cs +++ b/BossMod/AI/AIConfig.cs @@ -46,5 +46,11 @@ sealed class AIConfig : ConfigNode [PropertyDisplay("Max distance to target")] public float MaxDistanceToTarget = 2.6f; + [PropertyDisplay("Enable auto AFK", tooltip: "Enables auto AFK if out of combat. While AFK AI will not use autorotation or target anything")] + public bool AutoAFK = false; + + [PropertyDisplay("Enable out of combat AFK mode", tooltip: "Time in seconds out of combat until AFK mode enables. Any movement will reset timer or disable AFK mode if already active.")] + public float AFKModeTimer = 10; + public string? AIAutorotPresetName; } diff --git a/BossMod/BossModule/Shapes.cs b/BossMod/BossModule/Shapes.cs index 7217050732..a2172eb30a 100644 --- a/BossMod/BossModule/Shapes.cs +++ b/BossMod/BossModule/Shapes.cs @@ -1,3 +1,5 @@ +using Clipper2Lib; + namespace BossMod; public abstract record class Shape @@ -23,6 +25,24 @@ public record class PolygonCustom(IEnumerable Vertices) : Shape public override string ToString() => $"{nameof(PolygonCustom)}:{string.Join(",", Vertices.Select(v => $"{v.X},{v.Z}"))}"; } +// for custom polygons defined by an IEnumerable of vertices with an offset, eg to account for hitbox radius +public record class PolygonCustomO(IEnumerable Vertices, float Offset) : Shape +{ + public override List Contour(WPos center) + { + var originalPath = new Path64(Vertices.Select(v => new Point64((long)(v.X * PolygonClipper.Scale), (long)(v.Z * PolygonClipper.Scale)))); + ClipperOffset co = new(); + co.AddPath(originalPath, JoinType.Miter, EndType.Polygon); + Paths64 solution = []; + co.Execute(Offset * PolygonClipper.Scale, solution); + var offsetPath = solution[0]; + var offsetContour = offsetPath.Select(p => new WDir((float)(p.X * PolygonClipper.InvScale - center.X), (float)(p.Y * PolygonClipper.InvScale - center.Z))).ToList(); + return offsetContour; + } + + public override string ToString() => $"{nameof(PolygonCustomO)}:{string.Join(",", Vertices.Select(v => $"{v.X},{v.Z}"))},{Offset}"; +} + public record class Donut(WPos Center, float InnerRadius, float OuterRadius) : Shape { public override List Contour(WPos center) => CurveApprox.Donut(InnerRadius, OuterRadius, MaxApproxError).Select(p => p + (Center - center)).ToList(); diff --git a/BossMod/Modules/Dawntrail/Dungeon/D05Origenics/D052Deceiver.cs b/BossMod/Modules/Dawntrail/Dungeon/D05Origenics/D052Deceiver.cs index 5b8b823bcf..15ce47ee75 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D05Origenics/D052Deceiver.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D05Origenics/D052Deceiver.cs @@ -36,7 +36,7 @@ public enum AID : uint SurgeNPCs = 39736, // Helper->self, 8.5s cast, range 40 width 40 rect, knockback 15 dir left/right, only seems to apply to NPCs Surge = 36367, // Boss->location, 8.0s cast, range 40 width 40 rect, knockback 30 dir left/right - Electray = 38320, // Helper->player, 8.0s cast, range 5 circle + Electray = 38320 // Helper->player, 8.0s cast, range 5 circle } class ArenaChanges(BossModule module) : Components.GenericAOEs(module) diff --git a/BossMod/Modules/Dawntrail/Hunt/RankS/ArchAethereater.cs b/BossMod/Modules/Dawntrail/Hunt/RankS/ArchAethereater.cs index 0131cea897..8db6c93817 100644 --- a/BossMod/Modules/Dawntrail/Hunt/RankS/ArchAethereater.cs +++ b/BossMod/Modules/Dawntrail/Hunt/RankS/ArchAethereater.cs @@ -8,6 +8,7 @@ public enum OID : uint public enum AID : uint { AutoAttack = 39517, // Boss->player, no cast, single-target + Aethermodynamics1 = 39840, // Boss->self, 5.0s cast, range 40 circle Aethermodynamics2 = 39512, // Boss->self, 5.0s cast, range 40 circle Aethermodynamics3 = 39511, // Boss->self, 5.0s cast, range 40 circle @@ -131,9 +132,10 @@ class SoullessStreamFireBlizzardCombo(BossModule module) : Components.GenericAOE public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_aoes.Count > 0) + var count = _aoes.Count; + if (count > 0) yield return _aoes[0] with { Color = Colors.Danger }; - if (_aoes.Count > 1) + if (count > 1) yield return _aoes[1]; } diff --git a/BossMod/Modules/Dawntrail/Hunt/RankS/AtticusThePrimogenitor.cs b/BossMod/Modules/Dawntrail/Hunt/RankS/AtticusThePrimogenitor.cs index 7f58793cb9..ad31600b47 100644 --- a/BossMod/Modules/Dawntrail/Hunt/RankS/AtticusThePrimogenitor.cs +++ b/BossMod/Modules/Dawntrail/Hunt/RankS/AtticusThePrimogenitor.cs @@ -8,6 +8,7 @@ public enum OID : uint public enum AID : uint { AutoAttack = 39015, // Boss->player, no cast, single-target + HeadSpeaks = 39883, // Boss->self, no cast, single-target HeadSpeaks2 = 39884, // Boss->self, no cast, single-target BreathSequenceFirstFront = 39003, // Boss->self, 5.0s cast, range 60 120-degree cone @@ -84,9 +85,10 @@ class BreathSequence(BossModule module) : Components.GenericAOEs(module) public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_aoes.Count > 0) + var count = _aoes.Count; + if (count > 0) yield return _aoes[0] with { Color = Colors.Danger }; - if (_aoes.Count > 1) + if (count > 1) yield return _aoes[1]; } @@ -103,14 +105,15 @@ public override void OnActorNpcYell(Actor actor, ushort id) public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if (_aoes.Count > 0) + var count = _aoes.Count; + if (count > 0) switch ((AID)spell.Action.ID) { case AID.BreathSequenceFirstFront: case AID.BreathSequenceFirstLeft: case AID.BreathSequenceFirstRight: - for (var i = 0; i < _aoes.Count; ++i) - _aoes[i] = new(_aoes[i].Shape, _aoes[i].Origin, _aoes[i].Rotation, Module.CastFinishAt(spell, 2.3f * i)); + for (var i = 0; i < count; ++i) + _aoes[i] = _aoes[i] with { Activation = Module.CastFinishAt(spell, 2.3f * i) }; break; } } diff --git a/BossMod/Modules/Dawntrail/Hunt/RankS/Ihnuxokiy.cs b/BossMod/Modules/Dawntrail/Hunt/RankS/Ihnuxokiy.cs index 1b2609fd79..5cda7a7329 100644 --- a/BossMod/Modules/Dawntrail/Hunt/RankS/Ihnuxokiy.cs +++ b/BossMod/Modules/Dawntrail/Hunt/RankS/Ihnuxokiy.cs @@ -41,9 +41,10 @@ private enum Aetherspark { None, Thunderspark, CyclonicRing } public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_aoes.Count > 0) + var count = _aoes.Count; + if (count > 0) yield return _aoes[0] with { Color = Colors.Danger }; - if (_aoes.Count > 1) + if (count > 1) yield return _aoes[1]; } diff --git a/BossMod/Modules/Dawntrail/Hunt/RankS/Neyoozoteel.cs b/BossMod/Modules/Dawntrail/Hunt/RankS/Neyoozoteel.cs index f8d178e907..af152dac4f 100644 --- a/BossMod/Modules/Dawntrail/Hunt/RankS/Neyoozoteel.cs +++ b/BossMod/Modules/Dawntrail/Hunt/RankS/Neyoozoteel.cs @@ -68,10 +68,11 @@ class SapSpiller(BossModule module) : Components.GenericAOEs(module) public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_aoes.Count > 0) + var count = _aoes.Count; + if (count > 0) { yield return _aoes[0] with { Color = Colors.Danger }; - foreach (var a in _aoes.Skip(1).Take(_aoes.Count - 1)) + foreach (var a in _aoes.Skip(1).Take(count - 1)) yield return a; } } diff --git a/BossMod/Modules/Dawntrail/Hunt/RankS/Sansheya.cs b/BossMod/Modules/Dawntrail/Hunt/RankS/Sansheya.cs index 41ab40e2ba..3ae21a7b3d 100644 --- a/BossMod/Modules/Dawntrail/Hunt/RankS/Sansheya.cs +++ b/BossMod/Modules/Dawntrail/Hunt/RankS/Sansheya.cs @@ -8,6 +8,7 @@ public enum OID : uint public enum AID : uint { AutoAttack = 870, // Boss/4379->player, no cast, single-target + CullingBlade = 39295, // Boss->self, 4.0s cast, range 80 circle PyreOfRebirth = 39288, // Boss->self, 4.0s cast, range 32 circle, status effect boiling, turns into pyretic FiresDomain = 39285, // Boss->players, 5.0s cast, width 6 rect charge @@ -77,10 +78,11 @@ class TwinscorchedHaloVeil(BossModule module) : Components.GenericAOEs(module) public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_aoes.Count > 0) + var count = _aoes.Count; + if (count > 0) yield return _aoes[0] with { Color = Colors.Danger }; - if (_aoes.Count > 1) - yield return _aoes[1] with { Risky = _aoes.Count == 2 }; + if (count > 1) + yield return _aoes[1] with { Risky = _aoes[1].Shape != cone }; } public override void OnCastStarted(Actor caster, ActorCastInfo spell) diff --git a/BossMod/Modules/Dawntrail/Hunt/RankS/TheForecaster.cs b/BossMod/Modules/Dawntrail/Hunt/RankS/TheForecaster.cs index 0ab63f6ca4..0994898eaa 100644 --- a/BossMod/Modules/Dawntrail/Hunt/RankS/TheForecaster.cs +++ b/BossMod/Modules/Dawntrail/Hunt/RankS/TheForecaster.cs @@ -8,6 +8,7 @@ public enum OID : uint public enum AID : uint { AutoAttack = 872, // Boss->player, no cast, single-target + GaleForceWinds = 38534, // Boss->self, 4.0s cast, range 40 width 40 rect BlizzardConditions = 38535, // Boss->self, 4.0s cast, range 40 width 5 cross Hyperelectricity = 38533, // Boss->self, 4.0s cast, range 10 circle @@ -58,9 +59,10 @@ private enum ClimateChange { None, G2B, B2W, H2G, W2H } public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_aoes.Count > 0) + var count = _aoes.Count; + if (count > 0) yield return _aoes[0] with { Color = Colors.Danger }; - if (_aoes.Count > 1) + if (count > 1) yield return _aoes[1] with { Risky = false }; } diff --git a/BossMod/Modules/Heavensward/Dungeon/D11Antitower/D111ZuroRoggo.cs b/BossMod/Modules/Heavensward/Dungeon/D11Antitower/D111ZuroRoggo.cs index 90bb37217d..9ab7915a15 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D11Antitower/D111ZuroRoggo.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D11Antitower/D111ZuroRoggo.cs @@ -166,8 +166,7 @@ protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRoles var e = hints.PotentialTargets[i]; e.Priority = (OID)e.Actor.OID switch { - OID.PoroggoChoirtoad => 2, - OID.Boss => 1, + OID.PoroggoChoirtoad => 1, _ => 0 }; } diff --git a/BossMod/Modules/Heavensward/Dungeon/D11Antitower/D113Calcabrina.cs b/BossMod/Modules/Heavensward/Dungeon/D11Antitower/D113Calcabrina.cs index fde1dac8ea..89afae4509 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D11Antitower/D113Calcabrina.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D11Antitower/D113Calcabrina.cs @@ -138,4 +138,17 @@ protected override void DrawEnemies(int pcSlot, Actor pc) Arena.Actors(Enemies(OID.Brina).Concat(Enemies(OID.Boss)).Concat(Enemies(OID.Calcabrina))); Arena.Actors(Enemies(OID.CalcaPlayer1).Concat(Enemies(OID.CalcaPlayer2)).Concat(Enemies(OID.BrinaPlayer1)).Concat(Enemies(OID.BrinaPlayer2)), Colors.Vulnerable); } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + e.Priority = (OID)e.Actor.OID switch + { + OID.BrinaPlayer1 or OID.BrinaPlayer2 or OID.CalcaPlayer1 or OID.CalcaPlayer2 => 1, + _ => 0 + }; + } + } } diff --git a/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D151NuzalHueloc.cs b/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D151NuzalHueloc.cs new file mode 100644 index 0000000000..9a0522a717 --- /dev/null +++ b/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D151NuzalHueloc.cs @@ -0,0 +1,117 @@ +namespace BossMod.Heavensward.Dungeon.D15Xelphatol.D151NuzalHueloc; + +public enum OID : uint +{ + Boss = 0x179B, // R1.5 + FloatingTurret = 0x179E, // R1.0 + IxaliStitcher = 0x179C, // R1.08 + Airstone = 0x179D // R1.5 +} + +public enum AID : uint +{ + AutoAttack1 = 872, // Boss->player, no cast, single-target + AutoAttack2 = 6605, // FloatingTurret->player, no cast, single-target + AutoAttack3 = 870, // IxaliStitcher->player, no cast, single-target + ShortBurst1 = 6598, // Boss->player, no cast, single-target + ShortBurst2 = 6603, // FloatingTurret->player, 3.0s cast, single-target + + WindBlast = 6599, // Boss->self, 3.0s cast, range 60+R width 8 rect + Lift = 6601, // Boss->self, 3.0s cast, single-target + AirRaid = 6602, // Boss->location, no cast, range 50 circle + HotBlast = 6604, // FloatingTurret->self, 6.0s cast, range 25 circle + LongBurst = 6600 // Boss->player, 3.0s cast, single-target +} + +public enum SID : uint +{ + Invincibility = 775 // none->Boss/FloatingTurret, extra=0x0 +} + +class Airstone(BossModule module) : BossComponent(module) +{ + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Module.Enemies(OID.Airstone).Any(x => !x.IsDead)) + hints.Add("Destroy the airstones to remove invincibility!"); + } +} + +class WindBlast(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WindBlast), new AOEShapeRect(61.5f, 4)); + +class HotBlast(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCircle circle = new(4, true); + private AOEInstance? _aoe; + private const string RiskHint = "Go under boss!"; + + 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.HotBlast) + _aoe = new(circle, Module.PrimaryActor.Position, default, Module.CastFinishAt(spell), Colors.SafeFromAOE); + } + + public override void Update() + { + if (_aoe != null && (WorldState.CurrentTime - _aoe.Value.Activation).TotalSeconds >= 1) + _aoe = null; + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + var activeAOEs = ActiveAOEs(slot, actor).ToList(); + if (activeAOEs.Any(c => !c.Check(actor.Position))) + hints.Add(RiskHint); + else if (activeAOEs.Any(c => c.Check(actor.Position))) + hints.Add(RiskHint, false); + } +} + +class D151NuzalHuelocStates : StateMachineBuilder +{ + public D151NuzalHuelocStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 182, NameID = 5265)] +public class D151NuzalHueloc(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) +{ + private static readonly WPos[] vertices = [new(-73.36f, -91.53f), new(-67.17f, -90.22f), new(-66.55f, -89.97f), new(-64.94f, -89.05f), new(-64.45f, -88.60f), + new(-53.36f, -75.39f), new(-53.26f, -74.73f), new(-52.97f, -69.07f), new(-54.26f, -62.76f), new(-54.54f, -62.01f), + new(-57.75f, -56.44f), new(-62.76f, -51.91f), new(-68.83f, -49.18f), new(-75.29f, -48.72f), new(-75.94f, -48.76f), + new(-79.79f, -49.43f), new(-90.22f, -55.45f), new(-92.52f, -57.81f), new(-95.38f, -64.42f), new(-96.08f, -70.82f), + new(-96.01f, -71.49f), new(-94.72f, -77.66f), new(-91.42f, -83.42f), new(-86.42f, -88.01f), new(-80.32f, -90.77f), + new(-73.80f, -91.52f)]; + private static readonly ArenaBoundsComplex arena = new([new PolygonCustom(vertices)]); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(Enemies(OID.IxaliStitcher).Concat([PrimaryActor]).Concat(Enemies(OID.FloatingTurret)).Concat(Enemies(OID.Airstone))); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + if (e.Actor.FindStatus(SID.Invincibility) != null) + { + e.Priority = -2; + continue; + } + e.Priority = (OID)e.Actor.OID switch + { + OID.Airstone => 2, + OID.FloatingTurret => 1, + _ => 0 + }; + } + } +} diff --git a/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D152DotoliCiloc.cs b/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D152DotoliCiloc.cs new file mode 100644 index 0000000000..6b853ddd7e --- /dev/null +++ b/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D152DotoliCiloc.cs @@ -0,0 +1,211 @@ +namespace BossMod.Heavensward.Dungeon.D11Antitower.D152DotoliCiloc; + +public enum OID : uint +{ + Boss = 0x179F, // R1.98 + ArenaVoidzone = 0x1EA187, // R2.0 + Whirlwind = 0x17A0 // R1.0 +} + +public enum AID : uint +{ + AutoAttack = 872, // Boss->player, no cast, single-target + + OnLow = 6606, // Boss->self, 4.0s cast, range 9+R 120-degree cone + OnHigh = 6607, // Boss->self, 3.0s cast, range 50+R circle, knockback 30, away from source + DarkWings = 32556, // Boss->player, no cast, range 6 circle, spread + Swiftfeather = 6609, // Boss->self, 3.0s cast, single-target, applies Haste to boss + Stormcoming = 32557, // Boss->location, 4.0s cast, range 6 circle + TerribleFlurry = 6610 // Whirlwind->self, no cast, range 6 circle +} + +public enum IconID : uint +{ + Spreadmarker = 139 // player +} + +class ArenaChange(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCustom donut = new(D152DotoliCiloc.StartingBoundsP, D152DotoliCiloc.DefaultBoundsP); + private AOEInstance? _aoe; + private bool begin; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnActorEAnim(Actor actor, uint state) + { + if (state == 0x00010002 && (OID)actor.OID == OID.ArenaVoidzone) + { + Arena.Bounds = D152DotoliCiloc.DefaultBounds; + _aoe = null; + begin = true; + } + } + + public override void Update() + { + if (!begin && _aoe == null) + _aoe = new(donut, Arena.Center, default, WorldState.FutureTime(4)); + } +} + +class DarkWings(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.Spreadmarker, ActionID.MakeSpell(AID.DarkWings), 6, 5.1f); +class Whirlwind(BossModule module) : Components.PersistentVoidzone(module, 6, m => m.Enemies(OID.Whirlwind)); +class Stormcoming(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Stormcoming), 6); +class OnLow(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.OnLow), new AOEShapeCone(10.98f, 60.Degrees())); + +class OnLowHaste(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.Swiftfeather), new AOEShapeCone(10.98f, 60.Degrees())) +{ + private bool active; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Swiftfeather) + active = true; + else if ((AID)spell.Action.ID == AID.OnLow) + active = false; + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (active) + base.AddHints(slot, actor, hints); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (active) + base.AddAIHints(slot, actor, assignment, hints); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + if (active) + base.DrawArenaForeground(pcSlot, pc); + } +} + +class OnHigh(BossModule module) : Components.Knockback(module) +{ + private Source? _source; + private static readonly SafeWall[] safeWallsW = [new(new(227.487f, 16.825f), new(226.567f, 13.39f)), new(new(226.567f, 13.39f), new(227.392f, 10.301f))]; + private static readonly SafeWall[] safeWallsN = GenerateRotatedSafeWalls(safeWallsW, 90); + private static readonly SafeWall[] safeWallsE = GenerateRotatedSafeWalls(safeWallsW, 180); + private static readonly SafeWall[] safeWallsS = GenerateRotatedSafeWalls(safeWallsW, 270); + private static readonly SafeWall[] allSafeWalls = [.. safeWallsW, .. safeWallsN, .. safeWallsE, .. safeWallsS]; + + private static SafeWall[] GenerateRotatedSafeWalls(SafeWall[] baseWalls, float angle) + => baseWalls.Select(wall => new SafeWall(GenerateRotatedVertice(wall.Vertex1, angle), GenerateRotatedVertice(wall.Vertex2, angle))).ToArray(); + + private static WPos GenerateRotatedVertice(WPos vertex, float rotationAngle) => WPos.RotateAroundOrigin(rotationAngle, D152DotoliCiloc.ArenaCenter, vertex); + + public override IEnumerable Sources(int slot, Actor actor) => Utils.ZeroOrOne(_source); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.OnHigh) + _source = new(caster.Position, 30, Module.CastFinishAt(spell), SafeWalls: allSafeWalls); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.OnHigh) + _source = null; + } +} + +class OnHighHint(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List cones = []; + private AOEInstance? _aoe; + private const string RiskHint = "Use safewalls for knockback!"; + private static readonly Angle angle = 11.25f.Degrees(); + private DateTime activation; + + 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.OnHigh) + { + activation = Module.CastFinishAt(spell); + GenerateHints(); + } + } + + private void GenerateHints() + { + for (var i = 0; i < 4; ++i) + { + var deg = (i * 90).Degrees(); + if (!Module.Enemies(OID.Whirlwind).Any(x => x.Position.InCone(D152DotoliCiloc.ArenaCenter, deg, angle))) + cones.Add(new(D152DotoliCiloc.ArenaCenter, 20, deg, angle)); + } + _aoe = new(new AOEShapeCustom(cones, InvertForbiddenZone: true), D152DotoliCiloc.ArenaCenter, default, activation, Colors.SafeFromAOE); + } + + public override void OnActorCreated(Actor actor) + { + if (cones.Count > 0 && (OID)actor.OID == OID.Whirlwind) // sometimes the creation of whirlwinds is delayed + { + cones.Clear(); + GenerateHints(); + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.OnHigh) + { + cones.Clear(); + _aoe = null; + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + var activeAOEs = ActiveAOEs(slot, actor).ToList(); + if (activeAOEs.Any(c => !c.Check(actor.Position))) + hints.Add(RiskHint); + else if (activeAOEs.Any(c => c.Check(actor.Position))) + hints.Add(RiskHint, false); + } +} + +class D152DotoliCilocStates : StateMachineBuilder +{ + public D152DotoliCilocStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 182, NameID = 5269)] +public class D152DotoliCiloc(WorldState ws, Actor primary) : BossModule(ws, primary, ArenaCenter, StartingBounds) +{ + public static readonly WPos ArenaCenter = new(245.289f, 13.626f); + private static readonly float multi = 1 / MathF.Cos(MathF.PI / 16); + private const float offset = 0.42f; + public static readonly Polygon[] StartingBoundsP = [new Polygon(ArenaCenter, 29.45f * multi, 16, 11.25f.Degrees())]; + public static readonly Polygon[] DefaultBoundsP = [new Polygon(ArenaCenter, 20 * multi, 16, 11.25f.Degrees())]; + private static readonly WPos[] verticesW = [new(227.1f, 17.333f), new(226.122f, 13.411f), new(227, 10.126f), new(225.087f, 9.583f), new(224.016f, 13.541f), new(225.124f, 17.756f)]; + private static readonly WPos[] verticesN = GenerateRotatedVertices(verticesW, 90); + private static readonly WPos[] verticesE = GenerateRotatedVertices(verticesW, 180); + private static readonly WPos[] verticesS = GenerateRotatedVertices(verticesW, 270); + private static readonly PolygonCustomO[] difference = [new PolygonCustomO(verticesW, offset), new PolygonCustomO(verticesN, offset), + new PolygonCustomO(verticesE, offset), new PolygonCustomO(verticesS, offset)]; + public static readonly ArenaBoundsComplex StartingBounds = new(StartingBoundsP, difference); + public static readonly ArenaBoundsComplex DefaultBounds = new(DefaultBoundsP, difference); + + public static WPos[] GenerateRotatedVertices(WPos[] vertices, float rotationAngle) + => vertices.Select(vertex => WPos.RotateAroundOrigin(rotationAngle, ArenaCenter, vertex)).ToArray(); +} diff --git a/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D153TozolHuatotl.cs b/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D153TozolHuatotl.cs new file mode 100644 index 0000000000..5c2543c502 --- /dev/null +++ b/BossMod/Modules/Heavensward/Dungeon/D15Xelphatol/D153TozolHuatotl.cs @@ -0,0 +1,65 @@ +namespace BossMod.Heavensward.Dungeon.D15Xelphatol.D153TozolHuatotl; + +public enum OID : uint +{ + Boss = 0x17A2, // R3.0 + AbalathianHornbill = 0x17A3, // R1.08 + Garuda = 0x17A4, // R2.89 + Helper = 0xD25 +} + +public enum AID : uint +{ + AutoAttack = 872, // Boss->player, no cast, single-target + + IxaliAero = 6611, // Boss->player, no cast, single-target + IxaliAeroII = 6612, // Boss->self, 3.0s cast, range 40+R width 6 rect + IxaliAeroIII = 6613, // Boss->self, 3.0s cast, range 50+R circle + + Hawk = 6614, // Boss->self, 5.0s cast, single-target + Bill = 6618, // AbalathianHornbill->player, 5.0s cast, range 5 circle, spread + IngurgitateVisual = 6616, // AbalathianHornbill->player, 5.0s cast, single-target + Ingurgitate = 6617, // Helper->self, no cast, range 5 circle, stack + + SummonGaruda = 6615, // Boss->location, 4.0s cast, single-target + EyeOfTheStorm = 6619, // Helper->self, 6.0s cast, range 10-20 donut + MistralSong = 6620, // Garuda->self, 5.0s cast, range 30+R 120-degree cone + WickedWheel = 6621, // Garuda->self, 6.0s cast, range 7 circle + AerialBlast = 6622 // Garuda->self, 4.0s cast, range 50+R circle +} + +public enum IconID : uint +{ + Stackmarker = 62 // player +} + +class AerialBlast(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AerialBlast)); +class IxaliAeroII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.IxaliAeroII), new AOEShapeRect(43, 3)); +class IxaliAeroIII(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.IxaliAeroIII)); +class Bill(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Bill), 5); +class Ingurgitate(BossModule module) : Components.StackWithIcon(module, (uint)IconID.Stackmarker, ActionID.MakeSpell(AID.Ingurgitate), 5, 5.5f, 4, 4); +class EyeOfTheStorm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.EyeOfTheStorm), new AOEShapeDonut(10, 20)); +class WickedWheel(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WickedWheel), new AOEShapeCircle(7)); +class MistralSong(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MistralSong), new AOEShapeCone(32.89f, 60.Degrees())); + +class D153TozolHuatotlStates : StateMachineBuilder +{ + public D153TozolHuatotlStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 182, NameID = 5272)] +public class D153TozolHuatotl(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(317.8f, -416.19f), 19.5f / MathF.Cos(MathF.PI / 48), 48)], [new Rectangle(new(336.69f, -409.415f), 20, 1, -70.Degrees())]); +} diff --git a/BossMod/Modules/Heavensward/Dungeon/D17BaelsarsWall/D171MagitekPredator.cs b/BossMod/Modules/Heavensward/Dungeon/D17BaelsarsWall/D171MagitekPredator.cs index 6054f68a9b..7ddea3d2c3 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D17BaelsarsWall/D171MagitekPredator.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D17BaelsarsWall/D171MagitekPredator.cs @@ -56,12 +56,12 @@ protected override void DrawEnemies(int pcSlot, Actor pc) protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var e in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) { + var e = hints.PotentialTargets[i]; e.Priority = (OID)e.Actor.OID switch { - OID.SkyArmorReinforcement => 2, - OID.Boss => 1, + OID.SkyArmorReinforcement => 1, _ => 0 }; } diff --git a/BossMod/Modules/Heavensward/Dungeon/D17BaelsarsWall/D173TheGriffin.cs b/BossMod/Modules/Heavensward/Dungeon/D17BaelsarsWall/D173TheGriffin.cs index 85f50fa2f1..4eb507fb28 100644 --- a/BossMod/Modules/Heavensward/Dungeon/D17BaelsarsWall/D173TheGriffin.cs +++ b/BossMod/Modules/Heavensward/Dungeon/D17BaelsarsWall/D173TheGriffin.cs @@ -172,8 +172,11 @@ protected override void DrawEnemies(int pcSlot, Actor pc) protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - foreach (var e in hints.PotentialTargets) + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; e.Priority = e.Actor.OID == (uint)OID.BladeOfTheGriffin ? e.Actor.Position.AlmostEqual(Arena.Center, 5) ? 2 : -1 : e.Actor.OID == (uint)OID.RestraintCollar ? 2 : 1; + } } } diff --git a/BossMod/Modules/Stormblood/Dungeon/D15GhimlytDark/D150ScholaMarkIIColossus.cs b/BossMod/Modules/Stormblood/Dungeon/D15GhimlytDark/D150ScholaMarkIIColossus.cs index 169d93857a..e0091d30b8 100644 --- a/BossMod/Modules/Stormblood/Dungeon/D15GhimlytDark/D150ScholaMarkIIColossus.cs +++ b/BossMod/Modules/Stormblood/Dungeon/D15GhimlytDark/D150ScholaMarkIIColossus.cs @@ -18,7 +18,7 @@ public enum AID : uint ElementalBlessing = 14472, // KanESenna->self, no cast, ??? UnbreakableCermetBlade = 14470, // ScholaColossusRubricatus->self, 9.0s cast, range 30 circle GrandSword = 14967, // ScholaColossusRubricatus->self, 3.0s cast, range 15+R 120-degree cone - SelfDetonate = 14574, // ScholaColossusRubricatus->self, 35.0s cast, range 30 circle, enrage + SelfDetonate = 14574 // ScholaColossusRubricatus->self, 35.0s cast, range 30 circle, enrage } class MagitekMissile(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MagitekMissile), 15); diff --git a/BossMod/Modules/Stormblood/Dungeon/D15GhimlytDark/D151MarkIIIBMagitekColossus.cs b/BossMod/Modules/Stormblood/Dungeon/D15GhimlytDark/D151MarkIIIBMagitekColossus.cs index 455d38a2ae..c68c361059 100644 --- a/BossMod/Modules/Stormblood/Dungeon/D15GhimlytDark/D151MarkIIIBMagitekColossus.cs +++ b/BossMod/Modules/Stormblood/Dungeon/D15GhimlytDark/D151MarkIIIBMagitekColossus.cs @@ -22,7 +22,7 @@ public enum AID : uint MagitekSlashRest = 14671, // Helper->self, no cast, range 20+R 60-degree cone Exhaust = 14192, // Boss->self, 3.0s cast, range 40+R width 10 rect - CeruleumVent = 14195, // Boss->self, 4.0s cast, range 40 circle + CeruleumVent = 14195 // Boss->self, 4.0s cast, range 40 circle } public enum IconID : uint