diff --git a/BossMod/Components/BaitAway.cs b/BossMod/Components/BaitAway.cs index 4ab6aca34..bf84bc0ee 100644 --- a/BossMod/Components/BaitAway.cs +++ b/BossMod/Components/BaitAway.cs @@ -132,8 +132,8 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) if (target == null) return (null, null); - var (player, enemy) = source.Type == ActorType.Player ? (source, target) : (target, source); - if (player.Type != ActorType.Player || enemy.Type == ActorType.Player) + var (player, enemy) = source.Type is ActorType.Player or ActorType.DutySupport ? (source, target) : (target, source); + if (!(player.Type is ActorType.Player or ActorType.DutySupport) || enemy.Type == ActorType.Player) { ReportError($"Unexpected tether pair: {source.InstanceID:X} -> {target.InstanceID:X}"); return (null, null); diff --git a/BossMod/Config/ModuleViewer.cs b/BossMod/Config/ModuleViewer.cs index a3b5e4af4..31deee6df 100644 --- a/BossMod/Config/ModuleViewer.cs +++ b/BossMod/Config/ModuleViewer.cs @@ -1,6 +1,5 @@ using BossMod.Autorotation; using Dalamud.Interface; -using Dalamud.Interface.Internal; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using Lumina.Excel.GeneratedSheets; diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index edd6fb693..81389428b 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -9,6 +9,7 @@ public enum ActorType : ushort Pet = 0x202, Chocobo = 0x203, Enemy = 0x205, + DutySupport = 0x209, EventNpc = 0x300, Treasure = 0x400, Aetheryte = 0x500, diff --git a/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D011PrimePunutiy.cs b/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D011PrimePunutiy.cs new file mode 100644 index 000000000..cefa69398 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D011PrimePunutiy.cs @@ -0,0 +1,189 @@ +namespace BossMod.Dawntrail.Dungeon.D01Ihuykatumu.D011PrimePunutiy; + +public enum OID : uint +{ + Helper = 0x233C, // R0.500, x16 (spawn during fight), 523 type + Boss = 0x4190, // R7.990, x1 + ProdigiousPunutiy = 0x4191, // R4.230, x0 (spawn during fight) + Punutiy = 0x4192, // R2.820, x0 (spawn during fight) + PetitPunutiy = 0x4193, // R2.115, x0 (spawn during fight) + IhuykatumuFlytrap = 0x4194, // R1.600, x0 (spawn during fight) +} + +public enum AID : uint +{ + PunutiyPress = 36492, // Boss->self, 5.0s cast, range 60 circle + Hydrowave = 36493, // Boss->self, 4.0s cast, range 60 30-degree cone + AddsHydrowave = 36509, + Resurface = 36494, // Boss->self, 5.0s cast, range 100 60-degree cone + Resurface2 = 36495, // Boss->self, 7.0s cast, single-target + Bury1 = 36497, // 233C->self, 4.0s cast, range 12 circle + Bury2 = 36498, // 233C->self, 4.0s cast, range 8 circle + Bury3 = 36499, // 233C->self, 4.0s cast, range 25 width 6 rect + Bury4 = 36500, // 233C->self, 4.0s cast, range 35 width 10 rect + Bury5 = 36501, // 233C->self, 4.0s cast, range 4 circle + Bury6 = 36502, // 233C->self, 4.0s cast, range 6 circle + Bury7 = 36503, // 233C->self, 4.0s cast, range 25 width 6 rect + Bury8 = 36504, // 233C->self, 4.0s cast, range 35 width 10 rect + Decay = 36505, // 4194->self, 7.0s cast, range ?-40 donut + ShoreShaker = 36514, // Boss->self, 4.0+1.0s cast, single-target + ShoreShaker1 = 36515, // 233C->self, 5.0s cast, range 10 circle + ShoreShaker2 = 36516, // 233C->self, 7.0s cast, range ?-20 donut + ShoreShaker3 = 36517, // 233C->self, 9.0s cast, range ?-30 donut +} + +class PunutiyFlop(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.PunutiyPress)); +class Hydrowave(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Hydrowave), new AOEShapeCone(60, 15.Degrees())); + +class Bury(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(Actor Caster, AOEInstance AOE)> _activeAOEs = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _activeAOEs.Select(x => x.AOE); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + AOEShape? toAdd = (AID)spell.Action.ID switch + { + AID.Bury1 => new AOEShapeCircle(12), + AID.Bury2 => new AOEShapeCircle(8), + AID.Bury3 or AID.Bury7 => new AOEShapeRect(25, 3), + AID.Bury4 or AID.Bury8 => new AOEShapeRect(35, 5), + AID.Bury5 => new AOEShapeCircle(4), + AID.Bury6 => new AOEShapeCircle(6), + _ => null + }; + if (toAdd != null) + _activeAOEs.Add((caster, new AOEInstance(toAdd, caster.Position, spell.Rotation, spell.NPCFinishAt))); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + _activeAOEs.RemoveAll(x => x.Caster == caster); + } +} + +class Resurface(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Resurface) + _aoe = new AOEInstance(new AOEShapeCone(100, 32.Degrees()), caster.Position, spell.Rotation, spell.NPCFinishAt); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.Resurface2) + _aoe = null; + } +} + +class Decay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Decay), new AOEShapeDonut(5, 40)) +{ + public override void DrawArenaForeground(int pcSlot, Actor pc) + => Arena.Actors(Module.Enemies(OID.IhuykatumuFlytrap).Where(x => !x.IsDead), ArenaColor.Object, allowDeadAndUntargetable: true); +} + +abstract class TetherBait(BossModule module, bool centerAtTarget = false) : Components.GenericBaitAway(module, default, true, centerAtTarget) +{ + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + base.DrawArenaForeground(pcSlot, pc); + foreach (var b in ActiveBaits) + { + if (Arena.Config.ShowOutlinesAndShadows) + Arena.AddLine(b.Source.Position, b.Target.Position, 0xFF000000, 2); + Arena.AddLine(b.Source.Position, b.Target.Position, ArenaColor.Danger); + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID != 17) + return; + + var tar = WorldState.Actors.Find(tether.Target); + if (tar == null) + return; + + OnTetherCreated(source, tar); + } + + public override void OnUntethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == 17) + CurrentBaits.Clear(); + } + + protected abstract void OnTetherCreated(Actor source, Actor target); +} + +class FlopBait(BossModule module) : TetherBait(module, true) +{ + protected override void OnTetherCreated(Actor source, Actor target) + { + switch ((OID)source.OID) + { + case OID.ProdigiousPunutiy: + CurrentBaits.Add(new(source, target, new AOEShapeCircle(14))); + break; + case OID.PetitPunutiy: + CurrentBaits.Add(new(source, target, new AOEShapeCircle(6))); + break; + } + } +} + +class HydroBait(BossModule module) : TetherBait(module, false) +{ + protected override void OnTetherCreated(Actor source, Actor target) + { + if ((OID)source.OID == OID.Punutiy) + CurrentBaits.Add(new(source, target, new AOEShapeCone(60, 15.Degrees()))); + } +} + +class ShoreShaker(BossModule module) : Components.ConcentricAOEs(module, [new AOEShapeCircle(10), new AOEShapeDonut(10, 20), new AOEShapeDonut(20, 30)]) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.ShoreShaker) + AddSequence(Module.Center, spell.NPCFinishAt); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var order = (AID)spell.Action.ID switch + { + AID.ShoreShaker1 => 0, + AID.ShoreShaker2 => 1, + AID.ShoreShaker3 => 2, + _ => -1 + }; + if (!AdvanceSequence(order, caster.Position, WorldState.FutureTime(2f))) + ReportError($"unexpected order {order}"); + } +} + +class D011PrimePunutiyStates : StateMachineBuilder +{ + public D011PrimePunutiyStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "xan", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 826, NameID = 12723)] +public class D011PrimePunutiy(WorldState ws, Actor primary) : BossModule(ws, primary, new(35, -95), new ArenaBoundsSquare(20)); diff --git a/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D012Drowsie.cs b/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D012Drowsie.cs new file mode 100644 index 000000000..07a0e4cc2 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D012Drowsie.cs @@ -0,0 +1 @@ +// there is nothing here. boss 2 can trivially be solved by AI diff --git a/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D013Apollyon.cs b/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D013Apollyon.cs new file mode 100644 index 000000000..d6b90e57b --- /dev/null +++ b/BossMod/Modules/Dawntrail/Dungeon/D01Ihuykatumu/D013Apollyon.cs @@ -0,0 +1,130 @@ +namespace BossMod.Dawntrail.Dungeon.D01Ihuykatumu.D013Apollyon; + +public enum OID : uint +{ + Helper = 0x233C, // R0.500, x?, 523 type + Boss = 0x4165, // R7.000, x? + LightningAOE = 0x1EBA21, // R0.500, x?, EventObj type + Whirlwind = 0x416C, // R1.000, x? +} + +public enum AID : uint +{ + RazorZephyr = 36340, // 4165->self, 4.0s cast, range 50 width 12 rect + BladeST = 36347, // 4165->none, 4.5s cast, single-target + HighWind = 36341, // 4165->self, 5.0s cast, range 60 circle + BladesOfFamine = 36346, // 233C->self, 3.0s cast, range 50 width 12 rect + LevinsickleSpark = 36349, // 233C->location, 5.0s cast, range 4 circle + Levinsickle = 36350, // 233C->location, 5.0s cast, range 4 circle + WingOfLightning = 36351, // 233C->self, 8.0s cast, range 40 ?-degree cone + ThunderIII = 36353, // 233C->player, 5.0s cast, range 6 circle + BladeAOE = 36357, // 233C->none, 5.0s cast, range 6 circle + WindSickle = 36358, // 233C->self, 4.0s cast, range ?-60 donut + RazorStorm = 36355, // 4165->self, 5.0s cast, range 40 width 40 rect + Windwhistle = 36359, // 4165->self, 4.0s cast, single-target + Cuttingwind = 36360, // 233C->self, no cast, range 72 width 8 rect +} + +class RazorZephyr(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RazorZephyr), new AOEShapeRect(50, 6)); +class Blade(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.BladeST)); +class HighWind(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.HighWind)); +class BladesOfFamine(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BladesOfFamine), new AOEShapeRect(50, 6)); +class WingOfLightning(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WingOfLightning), new AOEShapeCone(40, 22.5f.Degrees()), maxCasts: 8); +class LightningHelper(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.LightningAOE)); +class ThunderIII(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.ThunderIII), 6); +class BladeAOE(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.BladeAOE), new AOEShapeCircle(6), centerAtTarget: true); +class WindSickle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WindSickle), new AOEShapeDonut(6, 60)); +class RazorStorm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RazorStorm), new AOEShapeRect(40, 20)); +class Levinsickle(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Levinsickle), 4); +class LevinsickleSpark(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.LevinsickleSpark), 4); + +// first aoe is 10 seconds after windwhistle +// rest are 8 seconds after previous +class Whirlwind(BossModule module) : Components.GenericAOEs(module) +{ + private int _activations; + private DateTime _nextActivation; + + private static readonly List Rotations = [0.Degrees(), 45.Degrees(), 90.Degrees(), 135.Degrees()]; + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Windwhistle) + _nextActivation = WorldState.FutureTime(10); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + foreach (var c in ActiveAOEs(pcSlot, pc)) + { + c.Shape.Draw(Arena, c.Origin, c.Rotation, c.Color); + c.Shape.Outline(Arena, c.Origin, c.Rotation, ArenaColor.AOE); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.Cuttingwind) + { + _activations += 1; + _nextActivation = WorldState.FutureTime(8); + } + } + + public override void OnActorDestroyed(Actor actor) + { + _activations = 0; + _nextActivation = default; + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (_activations >= 12) + yield break; + + var whirlwind = Module.Enemies(OID.Whirlwind).FirstOrDefault(); + if (whirlwind == null) + yield break; + + var whirlyHelper = Module.Enemies(OID.Helper).FirstOrDefault(x => x.NameID == 12715); + if (whirlyHelper == null) + yield break; + + foreach (var angle in Rotations) + { + yield return new AOEInstance(new AOEShapeRect(72, 4, 72), whirlwind.Position, angle, _nextActivation, Shade(_nextActivation), _nextActivation < WorldState.FutureTime(4)); + } + } + + private uint Shade(DateTime activation) + { + var clampedETA = Math.Clamp((activation - WorldState.CurrentTime).TotalSeconds, 0, 4); + var opacity = 1 - clampedETA / 4; + var alpha = (uint)(opacity * 96) + 32; + return 0x008080 + alpha * 0x1000000; + } +} + +class D013ApollyonStates : StateMachineBuilder +{ + public D013ApollyonStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "xan", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 826, NameID = 12711)] +public class D013Apollyon(WorldState ws, Actor primary) : BossModule(ws, primary, new(-107, 265), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D021RyoqorTerteh.cs b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D021RyoqorTerteh.cs new file mode 100644 index 000000000..f7e7a2c69 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D021RyoqorTerteh.cs @@ -0,0 +1,94 @@ +namespace BossMod.Dawntrail.Dungeon.D02WorqorZormor.D021RyoqorTerteh; + +public enum OID : uint +{ + Boss = 0x4159, // R5.280, x1 + Helper = 0x233C, +} + +public enum AID : uint +{ + FrostingFracas = 36280, // 233C->self, 5.0s cast, range 60 circle + IceScream = 36270, // 415B->self, 12.0s cast, range 20 width 20 rect + FrozenSwirl = 36272, // 43A2->self, 12.0s cast, range 15 circle + SnowBoulder = 36278, // 415C->self, 4.0s cast, range 50 width 6 rect + SparklingSprinkling = 36281, // 233C->player, 5.0s cast, range 5 circle +} + +public enum SID : uint +{ + // frozen adds always get both 3944 and 3445, no idea what the difference is. i just picked my favorite + Frozen = 3944, // none->_Gen_RorrlohTeh/_Gen_QorrlohTeh1, extra=0x0 +} + +class FrostingFracas(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.FrostingFracas)); + +abstract class FreezableAOEs(BossModule module, ActionID action, AOEShape shape) : Components.GenericAOEs(module) +{ + protected Dictionary _casters = []; + protected byte _numFrozen; + protected bool _anyCastFinished; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (_numFrozen < 2) + yield break; + + foreach ((var caster, var isFrozen) in _casters) + { + var isCastReal = !isFrozen || _anyCastFinished; + + if (isCastReal) + yield return new AOEInstance(shape, caster.Position, caster.Rotation, caster.CastInfo!.NPCFinishAt + (isFrozen ? TimeSpan.FromSeconds(8) : default)); + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action == action) + _casters[caster] = false; + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (spell.Action == action) + if (_casters.Remove(caster)) + _anyCastFinished = true; + + if (_casters.Count == 0) + { + _anyCastFinished = false; + _numFrozen = 0; + } + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID is SID.Frozen && _casters.ContainsKey(actor)) + { + _casters[actor] = true; + _numFrozen += 1; + } + } +} + +class IceScream(BossModule module) : FreezableAOEs(module, ActionID.MakeSpell(AID.IceScream), new AOEShapeRect(20, 10)); +class FrozenSwirl(BossModule module) : FreezableAOEs(module, ActionID.MakeSpell(AID.FrozenSwirl), new AOEShapeCircle(15)); +class SparklingSprinkling(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SparklingSprinkling), 5); +class SnowBoulder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SnowBoulder), new AOEShapeRect(50, 3), maxCasts: 6); + +class D021RyoqorTertehStates : StateMachineBuilder +{ + public D021RyoqorTertehStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "xan", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 824, NameID = 12699)] +public class D021RyoqorTerteh(WorldState ws, Actor primary) : BossModule(ws, primary, new(-108, 119), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D022Kahderyor.cs b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D022Kahderyor.cs new file mode 100644 index 000000000..1befec0dc --- /dev/null +++ b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D022Kahderyor.cs @@ -0,0 +1,181 @@ + +namespace BossMod.Dawntrail.Dungeon.D02WorqorZormor.D022Kahderyor; + +public enum OID : uint +{ + Boss = 0x415D, // R7.000, x1 + Helper = 0x233C, // R0.500, x20, 523 type + CrystallineDebris = 0x415E, // R1.400, x0 (spawn during fight) +} + +public enum AID : uint +{ + WindUnbound = 36282, // Boss->self, 5.0s cast, range 60 circle + CrystallineCrush = 36153, // 233C->self, 6.3s cast, range 6 circle + WindShot = 36284, // Boss->self, 5.5s cast, single-target + WindShotHelper = 36296, // 233C->player, 6.0s cast, range ?-10 donut + EarthenShot = 36283, // Boss->self, 5.0+0.5s cast, single-target + EarthenShotHelper = 36295, // 233C->player, 6.0s cast, range 6 circle + CrystallineStorm = 36290, // 233C->self, 4.0s cast, range 50 width 2 rect + SeedCrystals = 36298, // 233C->player, 5.0s cast, range 6 circle + CyclonicRing = 36294, // 233C->self, 5.0s cast, range ?-40 donut + EyeOfTheFierce = 36297, // 233C->self, 5.0s cast, range 60 circle + StalagmiteCircle = 36293, // 233C->self, 5.0s cast, range 15 circle +} + +class CrystalInout(BossModule module) : Components.GenericAOEs(module) +{ + private enum Active + { + None = 0, + In = 1, + Out = 2 + } + + private record struct Inout(AOEShape InShape, AOEShape OutShape, WPos Center, Angle Rotation); + + private DateTime _finishAt; + private readonly List _aoes = []; + private Active _active = Active.None; + private byte _castsWhileActive; + + private void Reset() + { + _castsWhileActive = 0; + _aoes.Clear(); + _finishAt = default; + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (_active == Active.None) + yield break; + + foreach (var x in _aoes) + { + if (_active == Active.In) + yield return new AOEInstance(x.InShape, x.Center, x.Rotation, _finishAt, ArenaColor.SafeFromAOE, Risky: false); + else + yield return new AOEInstance(x.OutShape, x.Center, x.Rotation, _finishAt); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + base.AddHints(slot, actor, hints); + if (_active == Active.In && ActiveAOEs(slot, actor).All(c => !c.Check(actor.Position))) + hints.Add("Get to safe zone!"); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var aoes = ActiveAOEs(slot, actor); + var shapes = aoes.Select(s => s.Shape.Distance(s.Origin, s.Rotation)).ToList(); + if (shapes.Count == 0) + return; + + float distance(WPos p) + { + var dist = shapes.Select(s => s(p)).Min(); + return _active == Active.Out ? dist : -dist; + } + hints.AddForbiddenZone(distance, _finishAt); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.EarthenShotHelper) + { + _active = Active.Out; + _finishAt = spell.NPCFinishAt; + } + if ((AID)spell.Action.ID == AID.WindShotHelper) + { + _active = Active.In; + _finishAt = spell.NPCFinishAt; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.EarthenShot or AID.WindShot) + _castsWhileActive++; + + if ((AID)spell.Action.ID is AID.EarthenShotHelper or AID.WindShotHelper) + { + _active = Active.None; + if (_castsWhileActive >= 2) + Reset(); + } + + if ((AID)spell.Action.ID == AID.CrystallineCrush) + _aoes.Add(new Inout( + new AOEShapeCircle(8), + new AOEShapeCircle(15), + caster.Position, + spell.Rotation + )); + + if ((AID)spell.Action.ID == AID.CrystallineStorm) + _aoes.Add(new Inout( + new AOEShapeRect(25, 1, 25), + new AOEShapeRect(25, 7, 25), + caster.Position, + spell.Rotation + )); + } +} + +class WindUnbound(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.WindUnbound)); +class WindShot(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.WindShotHelper), new AOEShapeDonut(5, 10), true); +class EarthenShot(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.EarthenShotHelper), 6, true); +class CrystallineCrush(BossModule module) : Components.CastTowers(module, ActionID.MakeSpell(AID.CrystallineCrush), 6, maxSoakers: 4) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Towers.Count > 0) + hints.AddForbiddenZone(new AOEShapeDonut(6, 40), Towers[0].Position); + } +} + +class CrystallineStorm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CrystallineStorm), new AOEShapeRect(25, 1, 25)); +class CrystallineDebris(BossModule module) : Components.Adds(module, (uint)OID.CrystallineDebris) +{ + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var c in Actors.Where(x => !x.IsDead)) + Arena.AddCircle(c.Position, 1.4f, ArenaColor.Danger); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Actors.Any(x => !x.IsDead)) + hints.Add("Break debris!"); + } +} +class SeedCrystals(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.SeedCrystals), 6); +class CyclonicRing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CyclonicRing), new AOEShapeDonut(15, 40)); +class StalagmiteCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.StalagmiteCircle), new AOEShapeCircle(15)); +class EyeOfTheFierce(BossModule module) : Components.CastGaze(module, ActionID.MakeSpell(AID.EyeOfTheFierce)); + +class D022KahderyorStates : StateMachineBuilder +{ + public D022KahderyorStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "xan", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 824, NameID = 12703)] +public class D022Kahderyor(WorldState ws, Actor primary) : BossModule(ws, primary, new(-53, -57), new ArenaBoundsCircle(20)); diff --git a/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs new file mode 100644 index 000000000..9d4f0dc57 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Dungeon/D02WorqorZormor/D023Gurfurlur.cs @@ -0,0 +1,158 @@ +namespace BossMod.Dawntrail.Dungeon.D02WorqorZormor.D023Gurfurlur; + +public enum OID : uint +{ + Helper = 0x233C, // R0.500, x32, 523 type + Boss = 0x415F, // R7.000, x1 + AuraSphere = 0x4162, // R1.000, x0 (spawn during fight) + BitingWind = 0x4160, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + HeavingHaymaker = 36375, // Helper->self, 5.3s cast, range 60 circle + LithicImpact = 36302, // Helper->self, 6.8s cast, range 4 width 4 rect + GreatFlood = 36307, // Helper->self, 7.0s cast, range 80 width 60 rect + Allfire1 = 36303, // Helper->self, 7.0s cast, range 10 width 10 rect + Allfire2 = 36304, // Helper->self, 8.5s cast, range 10 width 10 rect + Allfire3 = 36305, // Helper->self, 10.0s cast, range 10 width 10 rect + VolcanicDrop = 36306, // Helper->player, 5.0s cast, range 6 circle + Sledgehammer = 36313, // Boss->self, 5.0s cast, range 60 width 8 rect + SledgehammerEnd = 39260, // Boss->self, no cast, range 60 width 8 rect + EnduringGlory = 36320, // Boss->self, 6.0s cast, range 60 circle + Windswrath1 = 36310, // Helper->self, 7.0s cast, range 40 circle + Windswrath2 = 39074, // Helper->self, 15.0s cast, range 40 circle +} + +class HeavingHaymaker(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.HeavingHaymaker)); +class LithicImpact(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.LithicImpact), new AOEShapeRect(2, 2, 2)); +class GreatFlood(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.GreatFlood), distance: 25, kind: Kind.DirForward) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var caster = Casters.FirstOrDefault(); + if (caster?.CastInfo == null) + return; + + var knockbackTime = caster.CastInfo.NPCFinishAt; + + if (IsImmune(slot, knockbackTime)) + return; + + hints.AddForbiddenZone(new AOEShapeRect(20, 20, 5), Arena.Center, caster.Rotation, knockbackTime); + } +} + +class Allfire(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List[] _aoes = [[], [], []]; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes.FirstOrDefault(x => x.Count != 0) ?? []; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + var castStage = GetStage(spell.Action.ID); + if (castStage >= 0) + _aoes[castStage].Add(new AOEInstance(new AOEShapeRect(5, 5, 5), caster.Position, default, spell.NPCFinishAt)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + var castStage = GetStage(spell.Action.ID); + if (castStage >= 0) + _aoes[castStage].Clear(); + } + + private int GetStage(uint id) => GetStage((AID)id); + + private int GetStage(AID id) => id switch + { + AID.Allfire1 => 0, + AID.Allfire2 => 1, + AID.Allfire3 => 2, + _ => -1 + }; +} + +class VolcanicDrop(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.VolcanicDrop), 6); + +class Sledgehammer(BossModule module) : Components.GenericWildCharge(module, 4, ActionID.MakeSpell(AID.Sledgehammer), 60) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Sledgehammer) + { + Source = caster; + foreach (var (i, p) in Raid.WithSlot(true)) + PlayerRoles[i] = p.InstanceID == spell.TargetID ? PlayerRole.Target : PlayerRole.Share; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.SledgehammerEnd) + Source = null; + } +} + +class AuraSpheres : Components.PersistentInvertibleVoidzone +{ + private bool IsActive => Sources(Module).Any(); + + public AuraSpheres(BossModule module) : base(module, 2.5f, m => m.Enemies(OID.AuraSphere).Where(x => !x.IsDead)) + { + InvertResolveAt = DateTime.MaxValue; + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!IsActive) + return; + + if (!Sources(Module).Any(x => Shape.Check(actor.Position, x))) + hints.Add("Touch the balls!"); + } +} + +class EnduringGlory(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.EnduringGlory)); +abstract class Windswrath(BossModule module, ActionID aid) : Components.KnockbackFromCastTarget(module, aid, 15) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var caster = Casters.FirstOrDefault(); + if (caster?.CastInfo == null) + return; + + var knockbackTime = caster.CastInfo.NPCFinishAt; + + if (IsImmune(slot, knockbackTime)) + return; + + hints.AddForbiddenZone(new AOEShapeDonut(5, 60), caster.Position, activation: knockbackTime); + } +} +class Windswrath1(BossModule module) : Windswrath(module, ActionID.MakeSpell(AID.Windswrath1)); +class Windswrath2(BossModule module) : Windswrath(module, ActionID.MakeSpell(AID.Windswrath2)); +class BitingWind(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.BitingWind)); + +class D023GurfurlurStates : StateMachineBuilder +{ + public D023GurfurlurStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "xan", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 824, NameID = 12705)] +public class D023Gurfurlur(WorldState ws, Actor primary) : BossModule(ws, primary, new(-54, -195), new ArenaBoundsSquare(20));