diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D021AencThon.cs b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D021AencThon.cs new file mode 100644 index 0000000000..a618e22615 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D021AencThon.cs @@ -0,0 +1,78 @@ +namespace BossMod.Shadowbringers.Dungeon.D02DohnMheg.D021AencThon; + +public enum OID : uint +{ + Boss = 0x3F2, + Helper = 0x233C, +} + +public enum AID : uint +{ + _AutoAttack_Attack = 872, // Boss->player, no cast, single-target + _Weaponskill_CandyCane = 8857, // Boss->player, 4.0s cast, single-target + _Weaponskill_Hydrofall = 8871, // Boss->self, 3.0s cast, single-target + _Weaponskill_Hydrofall1 = 8893, // Helper->location, 3.0s cast, range 6 circle + _Weaponskill_LaughingLeap = 8852, // Boss->location, 4.0s cast, range 4 circle + _Weaponskill_LaughingLeap1 = 8840, // Boss->players, no cast, range 4 circle + _Weaponskill_Landsblood = 7822, // Boss->self, 3.0s cast, range 40 circle + _Weaponskill_Landsblood1 = 7899, // Boss->self, no cast, range 40 circle + _Weaponskill_Geyser = 8800, // Helper->self, no cast, range 6 circle +} + +class CandyCane(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID._Weaponskill_CandyCane)); +class Hydrofall(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Hydrofall1), 6); +class LaughingLeap(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_LaughingLeap), 4); +class LaughingLeap2(BossModule module) : Components.StackWithIcon(module, 62, ActionID.MakeSpell(AID._Weaponskill_LaughingLeap1), 4, 5.15f); +class Landsblood(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_Landsblood)); + +class Geyser(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List Geysers = []; + + private readonly List Geysers1 = [new(0, -16), new(-9, 15)]; + private readonly List Geysers2 = [new(0, 5), new(-9, -15), new(7, -7)]; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Geysers; + + public override void OnActorEAnim(Actor actor, uint state) + { + if (state == 0x100020) + { + var geysers = actor.OID switch + { + 0x1EAAA1 => Geysers1, + 0x1EAAA2 => Geysers2, + _ => [] + }; + Geysers.AddRange(geysers.Select(d => + { + var center = d.Rotate(-actor.Rotation) + actor.Position; + return new AOEInstance(new AOEShapeCircle(6), center, default, WorldState.FutureTime(5.1f)); + })); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID._Weaponskill_Geyser) + Geysers.RemoveAll(g => g.Origin.AlmostEqual(caster.Position, 1)); + } +} + +class AencThonLordOfTheLingeringGazeStates : StateMachineBuilder +{ + public AencThonLordOfTheLingeringGazeStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 649, NameID = 8141)] +public class AencThonLordOfTheLingeringGaze(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 30), new ArenaBoundsCircle(19.5f)); + diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs index 54556ab245..3c4a4e2135 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D02DohnMheg/D023AencThon.cs @@ -130,5 +130,5 @@ public AencThonLordOfTheLengthsomeGaitStates(BossModule module) : base(module) } } -[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "xan, Malediktus", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 649, NameID = 8146)] +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "xan, Malediktus", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 649, NameID = 8146)] public class AencThonLordOfTheLengthsomeGait(WorldState ws, Actor primary) : BossModule(ws, primary, new(-128.5f, -244), new ArenaBoundsCircle(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D053ForgivenWhimsy.cs b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D053ForgivenWhimsy.cs index fa82ac3986..d0c92af744 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D053ForgivenWhimsy.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D05MtGulg/D053ForgivenWhimsy.cs @@ -49,14 +49,6 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) if ((AID)spell.Action.ID is AID.Judged or AID.FoundWanting && Towers.Count > 0) Towers.RemoveAt(0); } - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - if (Towers.Count > 0) - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Towers[0].Position, 5)); - if (Towers.Count > 1) - hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Sprint), actor, ActionQueue.Priority.High); - } } class Exegesis(BossModule module) : Components.GenericAOEs(module) diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D061TheFirstBeast.cs b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D061TheFirstBeast.cs new file mode 100644 index 0000000000..41f900bbd5 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D061TheFirstBeast.cs @@ -0,0 +1,108 @@ +namespace BossMod.Shadowbringers.Dungeon.D06Amaurot.D061FirstBeast; + +public enum OID : uint +{ + Boss = 0x27B6, // R=5.4 + FallenStar = 0x29DC, // R=2.4 + FallingTower = 0x18D6, // R=0.5 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 870, // Boss->player, no cast, single-target + + VenomousBreath = 15566, // Boss->self, 3.0s cast, range 9 120-degree cone + MeteorRainVisual = 15556, // Boss->self, 3.0s cast, single-target + MeteorRain = 15558, // Helper->location, 3.0s cast, range 6 circle + + TheFallingSkyVisual = 15561, // Boss->self, 3.0s cast, single-target + TheFallingSky = 15562, // Helper->location, 4.5s cast, range 10 circle + TheFinalSky = 15563, // Boss->self, 12.0s cast, range 70 circle, meteor if failed to LoS + CosmicKiss = 17108, // FallenStar->self, 4.5s cast, range 50 circle, meteor, damage fall off AOE + CosmicShrapnel = 17110, // FallenStar->self, no cast, range 8 circle, meteor explodes after final sky, can be ignored since it only does like 500 dmg + + Towerfall = 15564, // FallingTower->self, 8.0s cast, range 35 width 40 rect + Earthquake = 15565, // Boss->self, 4.0s cast, range 10 circle + TheBurningSkyVisual = 15559, // Boss->self, 5.2s cast, single-target + TheBurningSky1 = 13642, // FallingTower->location, 3.5s cast, range 6 circle + TheBurningSky2 = 15560, // Helper->player, 5.2s cast, range 6 circle, spread +} + +public enum IconID : uint +{ + Meteor = 57, // player + Spreadmarker = 139, // player +} + +class VenomousBreath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.VenomousBreath), new AOEShapeCone(9, 60.Degrees())); +class MeteorRain(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MeteorRain), 6); +class TheFallingSky(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.TheFallingSky), 10); +class CosmicKiss(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CosmicKiss), new AOEShapeCircle(10)); +class Towerfall(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Towerfall), new AOEShapeRect(35, 20)); +class Earthquake(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Earthquake), new AOEShapeCircle(10)); +class TheBurningSky1(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.TheBurningSky1), 6); +class TheBurningSky2(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.TheBurningSky2), 6); + +class Meteors(BossModule module) : Components.GenericBaitAway(module) +{ + public List targets = []; + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if (iconID == (uint)IconID.Meteor) + { + CurrentBaits.Add(new(actor, actor, new AOEShapeCircle(10))); + targets.Add(actor); + } + } + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.FallenStar) + { + CurrentBaits.Clear(); + targets.Clear(); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + base.AddHints(slot, actor, hints); + if (targets.Contains(actor)) + hints.Add("Place meteor!"); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + if (targets.Contains(actor)) + hints.AddForbiddenZone(ShapeDistance.InvertedRect(new(-80, 97), new(-80, 67), 15)); + } +} + +class TheFinalSky(BossModule module) : Components.CastLineOfSightAOE(module, ActionID.MakeSpell(AID.TheFinalSky), 70, false) +{ + public override IEnumerable BlockerActors() => Module.Enemies(OID.FallenStar); +} + +class D061FirstBeastStates : StateMachineBuilder +{ + public D061FirstBeastStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 652, NameID = 8201)] +public class D061FirstBeast(WorldState ws, Actor primary) : BossModule(ws, primary, new(-80, 82), new ArenaBoundsSquare(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D062Bellwether.cs b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D062Bellwether.cs new file mode 100644 index 0000000000..192d17ca32 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D062Bellwether.cs @@ -0,0 +1,59 @@ +namespace BossMod.Shadowbringers.Dungeon.D06Amaurot.D062Bellwether; + +public enum OID : uint +{ + Boss = 0x27B8, // R=6.0-12.0 + TerminusFlesher = 0x27B9, // R=3.3 + TerminusCrier = 0x27BD, // R=1.0 + TerminusSprinter = 0x2879, // R=1.96 + TerminusDetonator = 0x27BA, // R=2.0 + TerminusShriver = 0x27BC, // R=1.35 + TerminusRoiler = 0x27BB, // R=1.2 + TerminusBeholder = 0x27BE, // R=1.32 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 870, // Boss/TerminusSprinter/TerminusCrier->player, no cast, single-target + AutoAttack2 = 6499, // TerminusFlesher/TerminusBeholder->player, no cast, single-target + AutoAttack3 = 18013, // TerminusDetonator->player, no cast, single-target + ShrillShriek = 15567, // Boss->self, 3.0s cast, range 50 circle + Aetherspike = 15571, // TerminusSprinter->self, 4.0s cast, range 40 width 8 rect + SelfDestruct = 15570, // TerminusDetonator->self, no cast, range 6 circle + IllWill = 15573, // TerminusRoiler->player, no cast, single-target + SicklyFlame = 15588, // TerminusShriver->player, 2.0s cast, single-target + Comet = 15572, // 18D6->location, 4.0s cast, range 4 circle + SicklyInferno = 16765, // TerminusShriver->location, 3.0s cast, range 5 circle + Burst = 15569, // Boss->self, no cast, range 50 circle, raidwide on boss death + BurstEnrage = 15568, // Boss->self, 45.0s cast, single-target, enrage cast + ExplosionEnrage = 15919, // Boss->self, no cast, range 50 circle, enrage +} + +class ShrillShriek(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ShrillShriek)); +class Aetherspike(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Aetherspike), new AOEShapeRect(40, 4)); +class Comet(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Comet), 4); +class SicklyInferno(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SicklyInferno), 5); +class Burst(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.BurstEnrage), "Enrage!", true); + +class D062BellwetherStates : StateMachineBuilder +{ + public D062BellwetherStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 652, NameID = 8202)] +public class D062Bellwether(WorldState ws, Actor primary) : BossModule(ws, primary, new(60, -361), new ArenaBoundsCircle(19.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(a => !a.IsAlly), ArenaColor.Enemy); + } +} diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D063Therion.cs b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D063Therion.cs new file mode 100644 index 0000000000..68a2c8eafc --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D06Amaurot/D063Therion.cs @@ -0,0 +1,291 @@ +namespace BossMod.Shadowbringers.Dungeon.D06Amaurot.D063Therion; + +public enum OID : uint +{ + Boss = 0x27C1, // R=25.84 + TheFaceOfTheBeast = 0x27C3, // R=2.1 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 15574, // Boss->player, no cast, single-target + ShadowWreck = 15587, // Boss->self, 4.0s cast, range 100 circle + ApokalypsisFirst = 15575, // Boss->self, 6.0s cast, range 76 width 20 rect + ApokalypsisRest = 15577, // Helper->self, no cast, range 76 width 20 rect + TherionCharge = 15578, // Boss->location, 7.0s cast, range 100 circle, damage fall off AOE + + DeathlyRayVisualFaces1 = 15579, // Boss->self, 3.0s cast, single-target + DeathlyRayVisualFaces2 = 16786, // Boss->self, no cast, single-target + DeathlyRayVisualThereion1 = 17107, // Helper->self, 5.0s cast, range 80 width 6 rect + DeathlyRayVisualThereion2 = 15582, // Boss->self, 3.0s cast, single-target + DeathlyRayVisualThereion3 = 16785, // Boss->self, no cast, single-target + + DeathlyRayFacesFirst = 15580, // TheFaceOfTheBeast->self, no cast, range 60 width 6 rect + DeathlyRayFacesRest = 15581, // Helper->self, no cast, range 60 width 6 rect + DeathlyRayThereionFirst = 15583, // Helper->self, no cast, range 60 width 6 rect + DeathlyRayThereionRest = 15585, // Helper->self, no cast, range 60 width 6 rect + Misfortune = 15586, // Helper->location, 3.0s cast, range 6 circle +} + +class ShadowWreck(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ShadowWreck)); +class Misfortune(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Misfortune), 6); + +class Apokalypsis(BossModule module) : Components.GenericAOEs(module) +{ + private DateTime _activation; + private static readonly AOEShapeRect _rect = new(76, 10); + private Border? BorderComponent; + private List UnsafePlatforms = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (_activation != default) + { + yield return new(_rect, Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, _activation); + foreach (var pos in UnsafePlatforms) + yield return new(new AOEShapeRect(2, 2, 2), pos, default, _activation); + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.ApokalypsisFirst) + { + _activation = Module.CastFinishAt(spell); + BorderComponent ??= Module.FindComponent(); + UnsafePlatforms = BorderComponent?.UnsafePlatformPositions.ToList() ?? []; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.ApokalypsisFirst: + case AID.ApokalypsisRest: + if (++NumCasts == 5) + { + _activation = default; + UnsafePlatforms.Clear(); + NumCasts = 0; + } + break; + } + } +} + +class TherionCharge(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShape _rect = new AOEShapeRect(35, 20); + private AOEInstance? _aoe; + + 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.TherionCharge) + _aoe = new(_rect, caster.Position.Rounded(), default, Module.CastFinishAt(spell)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TherionCharge) + { + ++NumCasts; + _aoe = null; + } + } +} + +class DeathlyRayTherion(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 ((AID)spell.Action.ID == AID.DeathlyRayVisualThereion1) + _aoe = new(new AOEShapeRect(60, 3), caster.Position, spell.Rotation, Module.CastFinishAt(spell)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.DeathlyRayThereionFirst: + case AID.DeathlyRayThereionRest: + if (++NumCasts >= 5) + { + _aoe = null; + NumCasts = 0; + } + break; + } + } +} + +class DeathlyRayFaces(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeRect _rect = new(60, 3); + private readonly List Casters = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var c in Casters) + yield return new(_rect, c.Position, c.Rotation); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.DeathlyRayFacesFirst) + { + Casters.Add(caster); + } + if ((AID)spell.Action.ID is AID.DeathlyRayFacesRest) + { + if (++NumCasts >= Casters.Count * 4) + { + Casters.Clear(); + NumCasts = 0; + } + } + } +} + +class Border : Components.GenericAOEs +{ + private static readonly List SidePlatforms = [new(-12, -8), new(12, -8), new(-12, 12), new(12, 12), new(-12, 32), new(12, 32), new(-12, 46), new(12, 46)]; + private static readonly int[] PlatformHalfLen = [48, 30, 20, 10]; + + public static readonly WPos OriginalCenter = new(0, -63); + + public int Stage { get; private set; } + + private BitMask MissingPlatforms = new(); + private BitMask UnsafePlatforms = new(); + + public IEnumerable UnsafePlatformPositions => SidePlatforms.Where((_, i) => UnsafePlatforms[i]).Select(p => OriginalCenter + p); + + public Border(BossModule module) : base(module) + { + WorldState.Actors.EventObjectAnimation.Subscribe(OnEventObjectAnimation); + } + + // TODO this really should be in a separate component lol + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (Stage == 0 && Module.PrimaryActor.CastInfo is { Action.ID: 15587 } ci) + yield return new AOEInstance(new AOEShapeRect(36, 20), new(0, -111), default, Module.CastFinishAt(ci)); + } + + public static (WPos Center, ArenaBoundsCustom Bounds) BuildBounds(PolygonClipper clipper, int stage = 0, BitMask missingPlatforms = default) + { + var platLength = PlatformHalfLen[stage]; + + WDir centerOffset = new(0, 48 - platLength); + var mainPlatContour = CurveApprox.Rect(new(10, 0), new(0, platLength)); + var mainplat = new PolygonClipper.Operand(mainPlatContour); + foreach (var (center, i) in SidePlatforms.Select((p, i) => (p, i))) + { + if (missingPlatforms[i]) + continue; + + mainplat.AddContour(CurveApprox.Rect(center - centerOffset, new WDir(2, 0), new(0, 2))); + } + + return (OriginalCenter + centerOffset, new(MathF.Max(14, platLength), clipper.Simplify(mainplat))); + } + + private void UpdateBounds() + { + var b = BuildBounds(Module.Arena.Bounds.Clipper, Stage, MissingPlatforms); + Module.Arena.Bounds = b.Bounds; + Module.Arena.Center = b.Center; + } + + private void Advance(int expectedStage = -1) + { + if (expectedStage >= 0 && expectedStage != Stage) + { + Module.ReportError(this, $"expected bounds stage {expectedStage}, but got {Stage} - doing nothing"); + } + else + { + Stage++; + UpdateBounds(); + } + } + + private int FindPlatform(Actor act) => SidePlatforms.FindIndex(p => (OriginalCenter + p).AlmostEqual(act.Position, 1)); + + private void OnEventObjectAnimation(Actor act, ushort p1, ushort p2) + { + if (act.OID == 0x1EA1A1) + { + switch ((p1, p2)) + { + case (1, 2): + if (Stage == 0) + Advance(0); + break; + case (16, 32): + var tile = FindPlatform(act); + if (tile < 0) + Module.ReportError(this, $"unmatched tile for {act} @ {act.Position}"); + else + UnsafePlatforms.Set(tile); + break; + case (4, 8): + var tile2 = FindPlatform(act); + if (tile2 < 0) + Module.ReportError(this, $"unmatched tile for {act} @ {act.Position}"); + else + { + UnsafePlatforms.Clear(tile2); + MissingPlatforms.Set(tile2); + UpdateBounds(); + } + break; + } + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.TherionCharge) + { + switch (Stage) + { + case 1: + Advance(1); + break; + case 2: + Advance(2); + break; + default: + Module.ReportError(this, "unexpected third charge from Therion, doing nothing"); + break; + } + } + } +} + +class D063TherionStates : StateMachineBuilder +{ + public D063TherionStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "xan", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 652, NameID = 8210)] +public class D063Therion(WorldState ws, Actor primary) : BossModule(ws, primary, Border.OriginalCenter, Border.BuildBounds(new()).Bounds); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D091SeekerOfSolitude.cs b/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D091SeekerOfSolitude.cs new file mode 100644 index 0000000000..99a3bc2fb4 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D091SeekerOfSolitude.cs @@ -0,0 +1,119 @@ +namespace BossMod.Shadowbringers.Dungeon.D09GrandCosmos.D091SeekerOfSolitude; + +public enum AID : uint +{ + _AutoAttack_ = 18280, // Boss->player, no cast, single-target + _Spell_Shadowbolt = 18281, // Boss->player, 4.0s cast, single-target + _Spell_ImmortalAnathema = 18851, // Boss->self, 4.0s cast, range 60 circle + _Spell_Tribulation1 = 18283, // Boss->self, 3.0s cast, single-target + _Spell_Tribulation = 18852, // Helper->location, 3.0s cast, range 3 circle + _Spell_DarkShock = 18286, // Boss->self, 3.0s cast, single-target + _Spell_DarkShock1 = 18287, // Helper->location, 3.0s cast, range 6 circle + _Ability_Sweep = 18288, // Helper->player, no cast, single-target + _Ability_DeepClean = 18289, // Helper->player, no cast, single-target + _Spell_DarkPulse = 18282, // Boss->players, 5.0s cast, range 6 circle + _Spell_DarkWell = 18285, // Helper->player, 5.0s cast, range 5 circle + _Spell_DarkWell1 = 18284, // Boss->self, no cast, single-target + _Spell_MovementMagick = 18713, // Boss->self, 3.0s cast, single-target +} + +public enum OID : uint +{ + Boss = 0x2C1A, + Helper = 0x233C, + MagickedBroom = 0x2C1B, + DirtPile = 0x1EAEAE +} + +class Tribulation(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 3, ActionID.MakeSpell(AID._Spell_Tribulation), m => m.Enemies(OID.DirtPile).Where(x => x.EventState != 7), 0); +class ImmortalAnathema(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Spell_ImmortalAnathema)); +class DarkPulse(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID._Spell_DarkPulse), 6); +class DarkWell(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID._Spell_DarkWell), 5); +class DarkShock(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Spell_DarkShock1), 6); +class Shadowbolt(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID._Spell_Shadowbolt)); + +// not sure about radius, sweep trigger is incredibly janky +// filter out brooms who are too far outside the arena since they don't affect players and the AOE lingering on minimap is annoying +class Sweep(BossModule module) : Components.PersistentVoidzone(module, 4, m => m.Enemies(OID.MagickedBroom).Where(b => MathF.Abs(b.Position.X) <= 23.5f)) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var t in Sources(Module)) + { + hints.AddForbiddenZone(ShapeDistance.Capsule(t.Position, t.Rotation, 2, 4)); + hints.AddForbiddenZone(ShapeDistance.Capsule(t.Position, t.Rotation, 6, 4), WorldState.FutureTime(2)); + } + } +} + +class DeepClean(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly float[] CleaningTime = [11f, 12.5f, 15.5f, 17f, 18.5f]; + private const float CleanLinger = 2f; // estimate + private readonly List Cleanings = []; + + private record DirtPile(Actor Actor, DateTime CleaningPredicted, DateTime? CleanedAt) + { + public DateTime? CleanedAt = CleanedAt; + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + Cleanings.RemoveAll(c => c.CleanedAt is DateTime dt && dt.AddSeconds(CleanLinger) < WorldState.CurrentTime); + + return Cleanings.Select(p => new AOEInstance(new AOEShapeCircle(6), p.Actor.Position, default, p.CleaningPredicted)) + .Where(a => (a.Activation - WorldState.CurrentTime).TotalSeconds < 5); + } + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.DirtPile && Module.Enemies(OID.DirtPile).Count == 5) + ScheduleAOEs(); + } + + public override void OnActorModelStateChange(Actor actor, byte modelState, byte animState1, byte animState2) + { + if (modelState == 27 && animState1 == 1) + { + var c = Cleanings.MinBy(e => (e.Actor.Position - actor.Position).Length()); + if (c != null) + c.CleanedAt = WorldState.CurrentTime; + } + } + + private void ScheduleAOEs() + { + var dirtOrdered = Module.Enemies(OID.DirtPile).OrderBy(d => d.Position.Z).ToList(); + var brooms = Module.Enemies(OID.MagickedBroom).ToList(); + + float DistanceToBroom(Actor dirt) + { + var closestBroom = brooms.MinBy(b => MathF.Abs(b.Position.Z - dirt.Position.Z))!; + return MathF.Abs(closestBroom.Position.X - dirt.Position.X); + } + + dirtOrdered.SortBy(DistanceToBroom); + foreach (var (dirt, delay) in dirtOrdered.Zip(CleaningTime)) + Cleanings.Add(new(dirt, WorldState.FutureTime(delay), null)); + } +} + +class SeekerOfSolitudeStates : StateMachineBuilder +{ + public SeekerOfSolitudeStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => module.PrimaryActor.IsDeadOrDestroyed || module.PrimaryActor.HPMP.CurHP == 1 && !module.PrimaryActor.IsTargetable; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 692, NameID = 9041)] +public class SeekerOfSolitude(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, 187), new ArenaBoundsRect(20.5f, 14.5f)); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D092LeannanSith.cs b/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D092LeannanSith.cs new file mode 100644 index 0000000000..ffc54afd0b --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D092LeannanSith.cs @@ -0,0 +1,163 @@ +namespace BossMod.Shadowbringers.Dungeon.D09GrandCosmos.D092LeannanSith; + +public enum OID : uint +{ + Boss = 0x2C04, // R2.4 + EnslavedLove = 0x2C06, // R3.6 + LoversRing = 0x2C05, // R2.04 + LeannanSeed1 = 0x1EAE9E, // R0.5 + LeannanSeed2 = 0x1EAE9F, // R0.5 + LeannanSeed3 = 0x1EAEA0, // R0.5 + LeannanSeed4 = 0x1EAEA1, // R0.5 + DirtTiles = 0x1EAEC6, // R0.5 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 870, // Boss->player, no cast, single-target + AutoAttack2 = 872, // LoversRing->player, no cast, single-target + Teleport = 18207, // Boss->location, no cast, ??? + + StormOfColor = 18203, // Boss->player, 4.0s cast, single-target + OdeToLostLove = 18204, // Boss->self, 4.0s cast, range 60 circle + DirectSeeding = 18205, // Boss->self, 4.0s cast, single-target + GardenersHymn = 18206, // Boss->self, 14.0s cast, single-target + + ToxicSpout = 18208, // LoversRing->self, 8.0s cast, range 60 circle + OdeToFarWinds = 18210, // Boss->self, 3.0s cast, single-target + FarWind = 18211, // Helper->location, 5.0s cast, range 8 circle + FarWindSpread = 18212, // Helper->player, 5.0s cast, range 5 circle + OdeToFallenPetals = 18768, // Boss->self, 4.0s cast, range 5-60 donut + IrefulWind = 18209 // 2C06->self, 13.0s cast, range 40+R width 40 rect, knockback 10, source forward +} + +public enum SID : uint +{ + Transporting = 404 // none->player, extra=0x15 +} + +class OdeToLostLove(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.OdeToLostLove)); +class StormOfColor(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.StormOfColor)); +class FarWindSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.FarWindSpread), 5); +class FarWind(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FarWind), 8); +class OdeToFallenPetals(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.OdeToFallenPetals), new AOEShapeDonut(5, 60)); +class IrefulWind(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.IrefulWind), 10, kind: Kind.DirForward, stopAtWall: true); + +class DirectSeeding(BossModule module) : BossComponent(module) +{ + private IrefulWind? Wind; + private static readonly WDir[] Tileset = [ + new(-5, -15), new(5, -15), new(-15, -5), new(15, -5), + new(-15, 5), new(5, 5), new(-5, 15), new(15, 15) + ]; + private Angle? CurrentTileset = null; + private IEnumerable Seeds => WorldState.Actors.Where(x => (OID)x.OID is OID.LeannanSeed1 or OID.LeannanSeed2 or OID.LeannanSeed3 or OID.LeannanSeed4); + private IEnumerable TileCenters => CurrentTileset == null ? [] : Tileset.Select(t => t.Rotate(CurrentTileset.Value) + Arena.Center); + + public override void OnEventEnvControl(byte index, uint state) + { + if (index != 0x0B) + return; + + CurrentTileset = state switch + { + 0x00020001 => default(Angle), + 0x00200010 => 180.Degrees(), + 0x01000080 => 270.Degrees(), + 0x08000400 => 90.Degrees(), + _ => null + }; + } + + public override void Update() + { + Wind ??= Module.FindComponent(); + } + + private WDir WindOffset => Wind?.Casters.FirstOrDefault() is Actor helper ? helper.Rotation.ToDirection() * 10 : default; + + private IEnumerable GetDangerSeeds() + { + var centers = TileCenters.ToList(); + if (centers.Count == 0) + return []; + + var off = WindOffset; + + return Seeds.Where(s => + { + var projected = Module.Arena.ClampToBounds(s.Position + off); + return centers.Any(c => projected.AlmostEqual(c, 5)); + }); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (GetDangerSeeds().Any()) + hints.Add("Move seeds!"); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (actor.FindStatus(SID.Transporting) == null) + hints.InteractWithTarget = GetDangerSeeds().MinBy(actor.DistanceToHitbox); + else + { + var off = WindOffset; + List> tiledist = []; + foreach (var t in TileCenters) + { + tiledist.Add(ShapeDistance.Rect(t - off, default(Angle), 5, 5, 5)); + // tile is at edge of arena; seed can't be pushed out of it, it will just hit the wall + if (!Module.Arena.InBounds(t + off)) + tiledist.Add(ShapeDistance.Rect(t, default(Angle), 5, 5, 5)); + } + var zone = ShapeDistance.Union(tiledist); + + if (zone(actor.Position) > 0) + { + // normally the position of the seed we're carrying will lag behind our actual position in accordance with standard server latency + // jumping forces the server to acknowledge our current position, so we jump as soon as we enter a safe tile and then drop the seed + hints.WantJump = true; + if (actor.PosRot.Y > -11) + hints.StatusesToCancel.Add(((uint)SID.Transporting, default)); + } + hints.AddForbiddenZone(zone, WorldState.FutureTime(5)); + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (CurrentTileset == null) + return; + + foreach (var t in TileCenters) + Arena.ZoneRect(t, default(Angle), 5, 5, 5, ArenaColor.AOE); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + var danger = GetDangerSeeds(); + Arena.Actors(danger, ArenaColor.Danger); + Arena.Actors(Seeds.Except(danger), ArenaColor.PlayerGeneric); + } +} + +class LeannanSithStates : StateMachineBuilder +{ + public LeannanSithStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 692, NameID = 9044)] +public class LeannanSith(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, -60), new ArenaBoundsSquare(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D093Lugus.cs b/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D093Lugus.cs new file mode 100644 index 0000000000..6d00febee8 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D09GrandCosmos/D093Lugus.cs @@ -0,0 +1,294 @@ +namespace BossMod.Shadowbringers.Dungeon.D09GrandCosmos; + +public enum OID : uint +{ + Boss = 0x2C13, + Helper = 0x233C, +} + +public enum AID : uint +{ + _AutoAttack_Attack = 870, // Boss->player, no cast, single-target + _Weaponskill_ScorchingRight = 18274, // Boss->self, 5.0s cast, range 40 180-degree cone + _Spell_BlackFlame = 18269, // Helper->players, no cast, range 6 circle + _Weaponskill_OtherworldlyHeat = 18267, // Boss->self, 5.0s cast, single-target + _Weaponskill_OtherworldlyHeat1 = 18268, // Helper->self, 2.5s cast, range 10 width 4 cross + _Weaponskill_CaptiveBolt = 18276, // Boss->player, 5.0s cast, single-target + _Weaponskill_MortalFlame = 18265, // Boss->self, 5.0s cast, single-target + _Weaponskill_MortalFlame1 = 18266, // Helper->player, 5.5s cast, single-target + _Weaponskill_FiresDomain = 18270, // Boss->self, 8.0s cast, single-target + FiresDomain1 = 18272, // Boss->player, no cast, width 4 rect charge + FiresDomain2 = 18271, // Boss->player, no cast, width 4 rect charge + _Weaponskill_FiresIre = 18273, // Boss->self, 2.0s cast, range 20 90-degree cone + _Weaponskill_CullingBlade = 18277, // Boss->self, 6.0s cast, range 80 circle + _Ability_ = 18278, // Helper->self, no cast, range 80 circle + _Weaponskill_Plummet = 18279, // Helper->self, 1.6s cast, range 3 circle + _Weaponskill_ScorchingLeft = 18275, // Boss->self, 5.0s cast, range 40 180-degree cone +} + +public enum SID : uint +{ + _Gen_VulnerabilityUp = 1789, // Helper/Boss->player, extra=0x1/0x2/0x3/0x4/0x5/0x6/0x7/0x8 + _Gen_MortalFlame = 2136, // Helper->player, extra=0x0/0x50/0xA0/0xF0/0x140 +} + +public enum IconID : uint +{ + BlackFlame = 25, // player + MortalFlame = 195, // player + Target1 = 50, // player + Target2 = 51, // player + Target3 = 52, // player + Target4 = 53, // player + Tankbuster = 218 // player +} + +class CullingBlade(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_CullingBlade)); +class CaptiveBolt(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID._Weaponskill_CaptiveBolt)); +class Plummet(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Plummet), new AOEShapeCircle(3)); +class ScorchingRight(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_ScorchingRight), new AOEShapeCone(40, 90.Degrees())); +class ScorchingLeft(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_ScorchingLeft), new AOEShapeCone(40, 90.Degrees())); +class OtherworldlyHeat(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_OtherworldlyHeat1), new AOEShapeCross(10, 2)); +class FiresIre(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_FiresIre), new AOEShapeCone(20, 45.Degrees())); +class BlackFlame(BossModule module) : BossComponent(module) +{ + private BitMask targets; + private DateTime activation; + + private static readonly AOEShapeCross Shape = new(10, 2); + + private IEnumerable Furniture => Raid.WithoutSlot().Where(x => x.Type == ActorType.Enemy); + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if (iconID == 25) + { + targets.Set(Raid.FindSlot(actor.InstanceID)); + if (activation == default) + activation = WorldState.FutureTime(4.2f); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID._Spell_BlackFlame) + { + targets.Clear(Raid.FindSlot(spell.MainTargetID)); + if (!targets.Any()) + activation = default; + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + foreach (var (slot, player) in Raid.WithSlot().IncludedInMask(targets)) + { + if (slot == pcSlot) + Shape.Outline(Arena, player.Position, default, ArenaColor.Danger); + else + Shape.Draw(Arena, player.Position, default, ArenaColor.AOE); + } + + if (targets[pcSlot]) + foreach (var furniture in Furniture) + Arena.ZoneCircle(furniture.Position, furniture.HitboxRadius, ArenaColor.AOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + if (targets[slot]) + foreach (var ally in Furniture) + hints.AddForbiddenZone(p => IntersectFurniture(ally, p) ? -1 : 1, activation); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (targets[slot]) + hints.Add("Bait away from furniture!", Furniture.Any(f => IntersectFurniture(f, actor.Position))); + } + + private bool IntersectFurniture(Actor furniture, WPos player) => IntersectBubble(furniture, player, 2, 10) || IntersectBubble(furniture, player, 10, 2); + + private bool IntersectBubble(Actor furniture, WPos rectCenter, float halfWidth, float halfHeight) + { + var radius = furniture.HitboxRadius; + + var circleCenter = furniture.Position; + var off1 = (rectCenter - circleCenter).Abs(); + if (off1.X > halfWidth + radius || off1.Z > halfHeight + radius) + return false; + + if (off1.X <= halfWidth || off1.Z <= halfHeight) + return true; + + return (off1 - new WDir(halfWidth, halfHeight)).Length() <= radius; + } +} + +class MortalFlame(BossModule module) : BossComponent(module) +{ + private readonly float[] Timers = Utils.MakeArray(PartyState.MaxAllies, 0f); + + private IEnumerable Furniture => Raid.WithoutSlot().Where(x => x.Type == ActorType.Enemy); + + private void SetTimer(int slot, float timer) + { + if (slot < 0) + return; + + Timers[slot] = timer; + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID._Gen_MortalFlame && actor.Type != ActorType.Enemy) + SetTimer(Raid.FindSlot(actor.InstanceID), (float)(status.ExpireAt - WorldState.CurrentTime).TotalSeconds); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID._Gen_MortalFlame) + SetTimer(Raid.FindSlot(actor.InstanceID), 0); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Timers[slot] > 0) + { + if (Furniture.Any()) + hints.Add("Pass flame to furniture!"); + else + hints.Add("RIP"); + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Timers[slot] > 0) + { + var furnitures = Furniture.Select(f => ShapeDistance.InvertedCircle(f.Position, 1)).ToList(); + if (furnitures.Count > 0) + hints.AddForbiddenZone(ShapeDistance.Intersection(furnitures), WorldState.FutureTime(Timers[slot])); + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (Timers[pcSlot] > 0) + foreach (var f in Furniture) + Arena.ZoneCircle(f.Position, 2, ArenaColor.SafeFromAOE); + } +} + +class FiresDomain(BossModule module) : BossComponent(module) +{ + public const float TetherLength = 16f; + private readonly List Baits = []; + private DateTime NextCharge; + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if ((IconID)iconID is >= IconID.Target1 and <= IconID.Target4) + { + Baits.Add(actor); + if ((IconID)iconID == IconID.Target1) + NextCharge = WorldState.FutureTime(8.4f); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.FiresDomain1 or AID.FiresDomain2) + { + Baits.RemoveAt(0); + NextCharge = WorldState.FutureTime(4.4f); + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (Baits.Count == 0) + return; + + var baitOrder = Baits.IndexOf(pc); + + DrawCharge(Module.PrimaryActor, Baits[0], baitOrder == 0); + if (baitOrder > 0) + DrawCharge(Baits[baitOrder - 1], pc, true); + } + + private void DrawCharge(Actor from, Actor to, bool isPlayer) + { + if (isPlayer) + Arena.AddRect(from.Position, from.DirectionTo(to), (from.Position - to.Position).Length(), 0, 2, ArenaColor.Danger); + else + Arena.ZoneRect(from.Position, to.Position, 2, ArenaColor.AOE); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Baits.Count == 0) + return; + + var baitAOE = ShapeDistance.Rect(Module.PrimaryActor.Position, Baits[0].Position, 2); + + var order = Baits.IndexOf(actor); + if (order == 0) + { + hints.Add("Stretch tether!", (actor.Position - Module.PrimaryActor.Position).Length() < TetherLength); + if (Raid.WithoutSlot().Exclude(actor).Any(a => baitAOE(a.Position) < 0)) + hints.Add("GTFO from raid!"); + } + else + { + if (baitAOE(actor.Position) < 0) + hints.Add("GTFO from charge!"); + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var baitOrder = Baits.IndexOf(actor); + if (baitOrder < 0) + return; + + if (baitOrder == 0) + { + var source = Module.PrimaryActor; + // stretch tether + hints.AddForbiddenZone(ShapeDistance.Circle(source.Position, TetherLength), NextCharge); + // don't clip any other party member with charge + foreach (var p in Raid.WithoutSlot(excludeNPCs: true).Exclude(actor)) + hints.AddForbiddenZone(ShapeDistance.Cone(source.Position, 100, source.AngleTo(p), Angle.Asin(2f / (p.Position - source.Position).Length())), NextCharge); + } + else + { + // try to preposition away from previous party member in line + hints.AddForbiddenZone(ShapeDistance.Circle(Baits[baitOrder - 1].Position, TetherLength), NextCharge.AddSeconds(4.4f * baitOrder)); + // stay out of boss's charge aoe + hints.AddForbiddenZone(ShapeDistance.Rect(Module.PrimaryActor.Position, Baits[0].Position, 2), NextCharge); + } + } +} + +class LugusStates : StateMachineBuilder +{ + public LugusStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 692, NameID = 9046)] +public class Lugus(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, -340), new ArenaBoundsSquare(24.5f)); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D101Unknown.cs b/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D101Unknown.cs new file mode 100644 index 0000000000..28bb9a9a48 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D101Unknown.cs @@ -0,0 +1,116 @@ +namespace BossMod.Shadowbringers.Dungeon.D10AnamnesisAnyder.D101Unknown; + +public enum OID : uint +{ + Boss = 0x2CD9, // R4.900, x1 + Unknown = 0x2CDA, // R4.900, x1 + Helper = 0x233C, // R0.500, x2, Helper type + SinisterBubble = 0x2CDB, // R1.500, x12 +} + +public enum AID : uint +{ + _AutoAttack_Attack = 870, // Boss/Unknown->player, no cast, single-target + _Ability_NursedGrudge = 19309, // Boss->self, no cast, single-target + _Ability_Scrutiny = 20005, // Boss->self, 13.0s cast, single-target + _Weaponskill_FetidFang = 19305, // Boss->player, 4.0s cast, single-target + _Weaponskill_FetidFang1 = 19314, // Unknown->player, 4.0s cast, single-target + _Weaponskill_Reflection = 19311, // Boss->self, 1.2s cast, range 40 45-degree cone + _Weaponskill_Explosion = 19310, // SinisterBubble->self, 14.0s cast, range 8 circle + _Weaponskill_LuminousRay = 20006, // Boss->self, 5.0s cast, range 50 width 8 rect + _Weaponskill_LuminousRay1 = 20007, // Unknown->self, 5.0s cast, range 50 width 8 rect + _Weaponskill_Inscrutability = 19306, // Boss->self, 4.0s cast, range 40 circle + _Weaponskill_Inscrutability1 = 19315, // Unknown->self, 4.0s cast, range 40 circle + EctoplasmMark1 = 19319, // Helper->player, no cast, single-target + EctoplasmMark2 = 19312, // Helper->player, no cast, single-target + _Ability_EctoplasmicRay = 19321, // Boss->self, 5.0s cast, single-target + _Ability_EctoplasmicRay1 = 19322, // Unknown->self, 5.0s cast, single-target + EctoplasmicRay1 = 19320, // Unknown->self, no cast, range 50 width 8 rect + EctoplasmicRay2 = 19313, // Boss->self, no cast, range 50 width 8 rect + _Weaponskill_PlainWeirdness = 20043, // Unknown->self, 3.0s cast, single-target + _Weaponskill_Clearout = 19307, // Boss->self, 3.0s cast, range 9 120-degree cone + _Weaponskill_Clearout1 = 19316, // Unknown->self, 3.0s cast, range 9 120-degree cone + _Weaponskill_Setback = 19308, // Boss->self, 3.0s cast, range 9 120-degree cone + _Weaponskill_Setback1 = 19317, // Unknown->self, 3.0s cast, range 9 120-degree cone +} + +class Clearout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Clearout), new AOEShapeCone(9, 60.Degrees())); +class Clearout1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Clearout1), new AOEShapeCone(9, 60.Degrees())); +class Setback(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Setback), new AOEShapeCone(9, 60.Degrees())); +class Setback1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Setback1), new AOEShapeCone(9, 60.Degrees())); +class FetidFang(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID._Weaponskill_FetidFang)); +class FetidFang1(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID._Weaponskill_FetidFang1)); +class LuminousRay(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_LuminousRay), new AOEShapeRect(50, 4)); +class LuminousRay1(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_LuminousRay1), new AOEShapeRect(50, 4)); +class Inscrutability(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_Inscrutability)); +class Inscrutability1(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_Inscrutability1)); +class Explosion(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Explosion), new AOEShapeCircle(8)); +class EctoplasmicRay(BossModule module) : Components.SimpleLineStack(module, 4, 50, ActionID.MakeSpell(AID.EctoplasmMark1), ActionID.MakeSpell(AID.EctoplasmicRay1), 5.2f); +class EctoplasmicRay2(BossModule module) : Components.SimpleLineStack(module, 4, 50, ActionID.MakeSpell(AID.EctoplasmMark2), ActionID.MakeSpell(AID.EctoplasmicRay2), 5.2f); + +class Reflection(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void Update() + { + if (Module.PrimaryActor.IsDeadOrDestroyed) + _aoe = null; + } + + public override void OnActorEAnim(Actor actor, uint state) + { + Angle? angle = state switch + { + 0x00041000 => -135.Degrees(), + 0x00042000 => 135.Degrees(), + 0x00044000 => 0.Degrees(), + _ => null + }; + + if (angle != null) + _aoe = new AOEInstance(new AOEShapeCone(40, 22.5f.Degrees()), actor.Position, angle.Value, WorldState.FutureTime(14.4f)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_Reflection) + _aoe = null; + } +} + +class UnknownStates : StateMachineBuilder +{ + public UnknownStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.PrimaryActor.IsDeadOrDestroyed && Module.Enemies(OID.Unknown).All(s => s.IsDeadOrDestroyed); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 714, NameID = 9261)] +public class Unknown(WorldState ws, Actor primary) : BossModule(ws, primary, new(-40, 290), new ArenaBoundsCircle(19.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actor(PrimaryActor, ArenaColor.Enemy); + Arena.Actors(Enemies(OID.Unknown), ArenaColor.Enemy); + } +} + diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D102Kyklops.cs b/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D102Kyklops.cs new file mode 100644 index 0000000000..a888e5d036 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D102Kyklops.cs @@ -0,0 +1,100 @@ +namespace BossMod.Shadowbringers.Dungeon.D10AnamnesisAnyder.D102Kyklops; + +public enum OID : uint +{ + Boss = 0x2CFE, + Helper = 0x233C, +} + +public enum AID : uint +{ + _AutoAttack_ = 19283, // Boss->player, no cast, single-target + _Weaponskill_TheFinalVerse = 19288, // Boss->self, 4.0s cast, range 40 circle + _Weaponskill_2000MinaSwing = 19285, // Boss->self, 4.0s cast, range 12 circle + _Weaponskill_TerribleHammer = 19289, // Boss->self, 3.0s cast, single-target + _Weaponskill_TerribleBlade = 19290, // Boss->self, 3.0s cast, single-target + _Weaponskill_EyeOfTheCyclone = 19287, // Boss->self, 4.0s cast, range 8-25 donut + _Weaponskill_TerribleHammer1 = 19293, // Helper->self, no cast, range 10 width 10 rect + _Weaponskill_TerribleBlade1 = 19294, // Helper->self, no cast, range 10 width 10 rect + _Weaponskill_RagingGlower = 19286, // Boss->self, 3.0s cast, range 45 width 6 rect + _Weaponskill_2000MinaSwipe = 19284, // Boss->self, 4.0s cast, range 12 120-degree cone + _Weaponskill_OpenHearth = 19292, // Boss->self, no cast, single-target + _Weaponskill_OpenHearth1 = 19296, // Helper->player, 5.0s cast, range 6 circle + _Weaponskill_WanderersPyre = 19291, // Boss->self, no cast, single-target + _Weaponskill_WanderersPyre1 = 19295, // Helper->player, 5.0s cast, range 5 circle +} + +class FinalVerse(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_TheFinalVerse)); +class C2000MinaSwing(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_2000MinaSwing), new AOEShapeCircle(12)); +class WanderersPyre(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID._Weaponskill_WanderersPyre1), 5); +class OpenHearth(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID._Weaponskill_OpenHearth1), 6); +class RagingGlower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_RagingGlower), new AOEShapeRect(45, 3)); +class C2000MinaSwipe(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_2000MinaSwipe), new AOEShapeCone(12, 60.Degrees())); +class EyeOfTheCyclone(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_EyeOfTheCyclone), new AOEShapeDonut(8, 25)); +class Terrible(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List> aoes = []; + + private static readonly WDir[] X = [new(-10, 10), new(10, 10), new(0, 0), new(-10, -10), new(10, -10)]; + private static readonly WDir[] Plus = [new(0, 10), new(10, 0), new(0, -10), new(-10, 0)]; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => aoes.FirstOrDefault([]); + + public override void OnEventEnvControl(byte index, uint state) + { + if (aoes.Count > 0) + return; + + switch (state) + { + case 0x08000400: + case 0x02000100: + AddPattern(X, Plus); + break; + case 0x20001000: + case 0x00800040: + AddPattern(Plus, X); + break; + } + } + + private void AddPattern(WDir[] first, WDir[] second) + { + aoes.Add(first.Select(d => new AOEInstance(new AOEShapeRect(5, 5, 5), Arena.Center + d, default, WorldState.FutureTime(16f))).ToList()); + aoes.Add(second.Select(d => new AOEInstance(new AOEShapeRect(5, 5, 5), Arena.Center + d, default, WorldState.FutureTime(18.2f))).ToList()); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID._Weaponskill_TerribleHammer1 or AID._Weaponskill_TerribleBlade1) + { + Service.Log($"bonk (aoe groups = {aoes.Count})"); + if (aoes.Count > 0) + { + aoes[0].RemoveAt(0); + if (aoes[0].Count == 0) + aoes.RemoveAt(0); + } + } + } +} + +class KyklopsStates : StateMachineBuilder +{ + public KyklopsStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 714, NameID = 9263)] +public class Kyklops(WorldState ws, Actor primary) : BossModule(ws, primary, new(20, -80), new ArenaBoundsSquare(14.5f)); + diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D103RukshsDheem.cs b/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D103RukshsDheem.cs new file mode 100644 index 0000000000..ff37d63df0 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D10AnamnesisAnyder/D103RukshsDheem.cs @@ -0,0 +1,338 @@ +namespace BossMod.Shadowbringers.Dungeon.D10AnamnesisAnyder.D103RukshsDheem; + +public enum OID : uint +{ + Boss = 0x2CFF, // R4.0 + QueensHarpooner = 0x2D01, // R1.56 + DepthGrip = 0x2D00, // R5.0 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 870, // Boss/QueensHarpooner->player, no cast, single-target + SwiftShift = 19331, // Boss->location, no cast, single-target, teleport + + Bonebreaker = 19340, // Boss->player, 4.0s cast, single-target, tankbuster + + SeabedCeremonyVisual = 19323, // Boss->self, 4.0s cast, single-target + SeabedCeremony = 19324, // Helper->self, 4.0s cast, range 60 circle + + DepthGrip = 19332, // Boss->self, 4.0s cast, single-target + Arise = 19333, // DepthGrip->self, no cast, single-target + WavebreakerVisual1 = 19334, // DepthGrip->self, no cast, single-target + WavebreakerVisual2 = 19335, // DepthGrip->self, no cast, single-target + Wavebreaker1 = 13268, // Helper->self, no cast, range 36 width 8 rect + Wavebreaker2 = 13269, // Helper->self, no cast, range 21 width 10 rect + + FallingWaterVisual = 19325, // Boss->self, 5.0s cast, single-target, spread + FallingWater = 19326, // Helper->player, 5.0s cast, range 8 circle + + RisingTide = 19339, // Boss->self, 3.0s cast, range 50 width 6 cross + + Meatshield = 19338, // QueensHarpooner->Boss, no cast, single-target + CoralTrident = 19337, // QueensHarpooner->self, 5.0s cast, range 6 90-degree cone + Seafoam = 19336, // QueensHarpooner->self, 7.0s cast, range 60 circle + + FlyingFountVisual = 19327, // Boss->self, 5.0s cast, single-target, stack + FlyingFount = 19328, // Helper->player, 5.0s cast, range 6 circle + + CommandCurrentVisual = 19329, // Boss->self, 4.9s cast, single-target + CommandCurrent = 19330 // Helper->self, 5.0s cast, range 40 30-degree cone +} + +public enum SID : uint +{ + MeatShield = 2267 +} + +class SeabedCeremony(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.SeabedCeremony)); +class Seafoam(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Seafoam)); +class Bonebreaker(BossModule module) : Components.SingleTargetDelayableCast(module, ActionID.MakeSpell(AID.Bonebreaker)); +class FallingWater(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.FallingWater), 8); +class FlyingFount(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.FlyingFount), 6); +class CommandCurrent(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CommandCurrent), new AOEShapeCone(40, 15.Degrees())); +class CoralTrident(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CoralTrident), new AOEShapeCone(6, 45.Degrees())); +class RisingTide(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RisingTide), new AOEShapeCross(50, 3)); + +class Voidzones(BossModule module) : BossComponent(module) +{ + public enum Configuration + { + Default, + Split, + Narrow + } + + public Configuration Current { get; private set; } + + public override void OnEventEnvControl(byte index, uint state) + { + bool activate; + if (state == 0x00020001) + activate = true; + else if (state == 0x00080004) + activate = false; + else + return; + + if (index == 0x17) + Current = activate ? Configuration.Narrow : Configuration.Default; + else if (index == 0x18) + Current = activate ? Configuration.Split : Configuration.Default; + + Arena.Bounds = Current switch + { + Configuration.Split => RukshsDheem.SplitBounds, + Configuration.Narrow => RukshsDheem.NarrowBounds, + _ => RukshsDheem.DefaultBounds, + }; + } +} + +class Wavebreaker(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List> aoes = []; + private Voidzones? vz; + private bool drawExas; + + public List Casters = []; + + private static readonly AOEShapeRect rectNarrow = new(36, 4); + private static readonly AOEShapeRect rectWide = new(21, 5); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (aoes.Count == 0) + yield break; + + if (drawExas) + { + var highlight = true; + foreach (var aoe in aoes[0]) + { + yield return aoe with { Color = highlight ? ArenaColor.Danger : aoe.Color }; + highlight = false; + } + } + else + foreach (var aoe in aoes[0]) + yield return aoe; + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Arise: + Casters.Add(caster); + AddAOEs(caster); + break; + case AID.Wavebreaker1: + case AID.Wavebreaker2: + Casters.RemoveAt(0); + if (aoes.Count > 0) + { + aoes[0].RemoveAt(0); + if (aoes[0].Count == 0) + aoes.RemoveAt(0); + } + break; + } + } + + private void AddAOEs(Actor caster) + { + vz ??= Module.FindComponent(); + if (vz == null) + return; + + if (aoes.Count == 0) + aoes.Add([]); + if (aoes[0].Count > 3) + aoes.Add([]); + + drawExas = false; + + DateTime activation; + var shape = rectNarrow; + if (vz.Current == Voidzones.Configuration.Narrow) + { + activation = WorldState.FutureTime(aoes[0].Count > 3 ? 11.6f : 8); + drawExas = true; + } + else if (vz.Current == Voidzones.Configuration.Split) + activation = WorldState.FutureTime(7.6f); + else + { + activation = WorldState.FutureTime(9.8f); + shape = rectWide; + } + var toAdd = aoes[0].Count > 3 ? aoes[1] : aoes[0]; + toAdd.Add(new(shape, caster.Position, caster.Rotation, activation)); + } +} + +class Drains(BossModule module) : BossComponent(module) +{ + enum DrainState + { + Inactive, + Covered, + Active + } + private readonly DrainState[] DrainStates = Utils.MakeArray(8, DrainState.Inactive); + private static readonly WPos[] Positions = new WPos[8]; + private int NumActiveDrains => DrainStates.Count(d => d != DrainState.Inactive); + private DateTime activation; + + private Wavebreaker? wavebreaker; + + static Drains() + { + int[] xPositions = [-11, 11]; + var zStart = -465; + var zStep = 10; + var index = 0; + for (var i = 0; i < 2; i++) + for (var j = 0; j < 4; j++) + Positions[index++] = new(xPositions[i], zStart + j * zStep); + } + + private void Reset() + { + activation = default; + Array.Fill(DrainStates, DrainState.Inactive); + } + + public override void OnEventEnvControl(byte index, uint state) + { + if (index is not (>= 0x0F and <= 0x16)) + return; + + var ix = index - 0x0F; + var prev = DrainStates[ix]; + DrainStates[ix] = state switch + { + 0x00020001 => DrainState.Active, + 0x00080004 => DrainState.Covered, + _ => DrainState.Inactive, + }; + if (prev == DrainState.Inactive) + activation = WorldState.FutureTime(NumActiveDrains > 4 ? 16 : 12); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.SeabedCeremony) + Reset(); + } + + private IEnumerable<(WPos Position, bool Blocked)> GetActiveDrains() + { + wavebreaker ??= Module.FindComponent(); + + List handBlockers = []; + if (NumActiveDrains > 4) + { + handBlockers = wavebreaker?.Casters ?? []; + if (handBlockers.Count == 0) + yield break; + } + + for (var i = 0; i < 8; i++) + { + var st = DrainStates[i]; + if (st == DrainState.Inactive) + continue; + + var pos = Positions[i]; + + if (handBlockers.Any(b => pos.InRect(b.Position, b.Rotation, 21, 0, 5))) + continue; + + yield return (pos, st == DrainState.Covered); + } + } + + private IEnumerable GetUnblockedDrains() => GetActiveDrains().Where(d => !d.Blocked).Select(d => d.Position); + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + foreach (var (pos, blocked) in GetActiveDrains()) + Arena.AddRect(pos, new WDir(1, 0), 1.25f, 1.25f, 1.25f, blocked ? ArenaColor.PlayerGeneric : ArenaColor.Safe); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (GetUnblockedDrains().Any()) + hints.Add("Block drains!"); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + List> zones = []; + foreach (var drain in GetActiveDrains()) + { + bool inDrain(WPos p) => p.AlmostEqual(drain.Position, 1.25f); + var numBlockers = Raid.WithoutSlot().Count(p => inDrain(p.Position)); + if (numBlockers == 0 || numBlockers == 1 && inDrain(actor.Position)) + zones.Add(ShapeDistance.Rect(drain.Position, default(Angle), 1.25f, 1.25f, 1.25f)); + } + if (zones.Count == 0) + return; + + var zunion = ShapeDistance.Union(zones); + hints.AddForbiddenZone(p => -zunion(p), activation); + } +} + +class RukshsDheemStates : StateMachineBuilder +{ + public RukshsDheemStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "Malediktus, xan", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 714, NameID = 9264)] +public class RukshsDheem(WorldState ws, Actor primary) : BossModule(ws, primary, new(0, -450), DefaultBounds) +{ + private static ArenaBoundsCustom MakeSplitBounds() + { + var rect1 = CurveApprox.Rect(new WDir(0, -1), 15.5f, 19.5f); + var split = CurveApprox.Rect(new WDir(0, -1), 20, 4); + var clipper = new PolygonClipper(); + var output = clipper.Difference(new(rect1), new(split)); + return new ArenaBoundsCustom(19.5f, output); + } + + private const float X = 15.5f; + private const float Z = 19.5f; + public static readonly ArenaBoundsRect DefaultBounds = new(X, Z); + public static readonly ArenaBoundsRect NarrowBounds = new(X - 7.5f, Z); + public static readonly ArenaBoundsCustom SplitBounds = MakeSplitBounds(); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(Enemies(OID.QueensHarpooner), ArenaColor.Enemy); + Arena.Actor(PrimaryActor, ArenaColor.Enemy); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var h in hints.PotentialTargets) + h.Priority = h.Actor.FindStatus(SID.MeatShield) == null ? 1 : 0; + } +} diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D11HeroesGauntlet/D112SpectralNecromancer.cs b/BossMod/Modules/Shadowbringers/Dungeon/D11HeroesGauntlet/D112SpectralNecromancer.cs index 07b375c2a8..af06b647b7 100644 --- a/BossMod/Modules/Shadowbringers/Dungeon/D11HeroesGauntlet/D112SpectralNecromancer.cs +++ b/BossMod/Modules/Shadowbringers/Dungeon/D11HeroesGauntlet/D112SpectralNecromancer.cs @@ -1,205 +1,175 @@ namespace BossMod.Shadowbringers.Dungeon.D11HeroesGauntlet.D112SpectralNecromancer; + public enum OID : uint { - Boss = 0x2DF1, // R2.300, x? - Necrobomb1 = 0x2DF2, // R0.750, x? - Necrobomb2 = 0x2DF3, // R0.750, x? - Necrobomb3 = 0x2DF4, // R0.750, x? - Necrobomb4 = 0x2DF5, // R0.750, x? - Necrobomb5 = 0x2DF6, // R0.750, x? - Necrobomb6 = 0x2DF7, // R0.750, x? - Necrobomb7 = 0x2DF8, // R0.750, x? - Necrobomb8 = 0x2DF9, // R0.750, x? - Voidzone = 0x1EB02C, - FetterZombieObject = 0x1EB07A, - Helper = 0x233C, + Boss = 0x2DF1, // R2.3 + Necrobomb1 = 0x2DF2, // R0.75 + Necrobomb2 = 0x2DF3, // R0.75 + Necrobomb3 = 0x2DF4, // R0.75 + Necrobomb4 = 0x2DF5, // R0.75 + Necrobomb5 = 0x2DF6, // R0.75 + Necrobomb6 = 0x2DF7, // R0.75 + Necrobomb7 = 0x2DF8, // R0.75 + Necrobomb8 = 0x2DF9, // R0.75 + BleedVoidzone = 0x1EB02C, + NecroPortal = 0x1EB07A, + Helper = 0x233C } + public enum AID : uint { - FellForces = 20305, // 2DF1->player, no cast, single-target - AbsoluteDarkII = 20321, // 2DF1->self, 5.0s cast, range 40 120-degree cone - TwistedTouch = 20318, // 2DF1->player, 4.0s cast, single-target - PainMire = 20387, // 2DF1->self, no cast, single-target - PainMireLocation = 20388, // 233C->location, 5.5s cast, range 9 circle - ChaosStorm = 20320, // 2DF1->self, 4.0s cast, range 40 circle - DarkDeluge = 20316, // 2DF1->self, 4.0s cast, single-target - DarkDelugeLocation = 20317, // 233C->location, 5.0s cast, range 5 circle - - Attack = 6499, // 2DF3/2DF2/2DF5/2DF4->player, no cast, single-target - Necromancy = 20311, // 2DF1->self, 3.0s cast, single-target - Necromancy2 = 20312, // 2DF1->self, 3.0s cast, single-target - DeathThroes = 20323, // 2DF9/2DF6/2DF7/2DF8->player, no cast, single-target - Necroburst = 20313, // 2DF1->self, 4.3s cast, single-target - Necroburst2 = 20314, // 2DF1->self, 4.3s cast, single-target - Burst1 = 20322, // 2DF2->self, 4.0s cast, range 8 circle - Burst2 = 21429, // 2DF3->self, 4.0s cast, range 8 circle - Burst3 = 21430, // 2DF4->self, 4.0s cast, range 8 circle - Burst4 = 21431, // 2DF5->self, 4.0s cast, range 8 circle - Burst5 = 20324, // 2DF6->self, 4.0s cast, range 8 circle - Burst6 = 21432, // 2DF7->self, 4.0s cast, range 8 circle - Burst7 = 21433, // 2DF8->self, 4.0s cast, range 8 circle - Burst8 = 21434, // 2DF9->self, 4.0s cast, range 8 circle + AutoAttack = 6499, // Necrobomb3/Necrobomb4/Necrobomb1/Necrobomb2->player, no cast, single-target + FellForces = 20305, // Boss->player, no cast, single-target + + AbsoluteDarkII = 20321, // Boss->self, 5.0s cast, range 40 120-degree cone + + TwistedTouch = 20318, // Boss->player, 4.0s cast, single-target + Necromancy1 = 20311, // Boss->self, 3.0s cast, single-target + Necromancy2 = 20312, // Boss->self, 3.0s cast, single-target + Necroburst1 = 20313, // Boss->self, 4.3s cast, single-target + Necroburst2 = 20314, // Boss->self, 4.3s cast, single-target + + Burst1 = 20322, // Necrobomb1->self, 4.0s cast, range 8 circle + Burst2 = 21429, // Necrobomb2->self, 4.0s cast, range 8 circle + Burst3 = 21430, // Necrobomb3->self, 4.0s cast, range 8 circle + Burst4 = 21431, // Necrobomb4->self, 4.0s cast, range 8 circle + Burst5 = 20324, // Necrobomb5->self, 4.0s cast, range 8 circle + Burst6 = 21432, // Necrobomb6->self, 4.0s cast, range 8 circle + Burst7 = 21433, // Necrobomb7->self, 4.0s cast, range 8 circle + Burst8 = 21434, // Necrobomb8->self, 4.0s cast, range 8 circle + + PainMireVisual = 20387, // Boss->self, no cast, single-target + PainMire = 20388, // Helper->location, 5.5s cast, range 9 circle, spawns voidzone smaller than AOE + DeathThroes = 20323, // Necrobomb5/Necrobomb6/Necrobomb7/Necrobomb8->player, no cast, single-target + + ChaosStorm = 20320, // Boss->self, 4.0s cast, range 40 circle, raidwide + DarkDelugeVisual = 20316, // Boss->self, 4.0s cast, single-target + DarkDeluge = 20317 // Helper->location, 5.0s cast, range 5 circle } -public enum SID : uint + +public enum IconID : uint { - ZombieActive = 2056, + Baitaway = 23, // player + Tankbuster = 198 // player } -public enum IconID : uint + +public enum SID : uint { - ZombieFetter = 23, - Tankbuster = 198, + Doom = 910 // Boss->player, extra=0x0 } + public enum TetherID : uint { - ZombieChase = 17, - ZombieFetterTether = 79, + WalkingNecrobombs = 17, // Necrobomb3/Necrobomb1/Necrobomb2/Necrobomb4->2753/player/2757/2752 + CrawlingNecrobombs = 79 // Necrobomb7/Necrobomb8/Necrobomb5/Necrobomb6->player/2753/2757/2752 } + class AbsoluteDarkII(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AbsoluteDarkII), new AOEShapeCone(40, 60.Degrees())); -class PainMireLocation(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.PainMireLocation), 9); -class PainMireVoidzone(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 9, ActionID.MakeSpell(AID.PainMireLocation), m => m.Enemies(OID.Voidzone).Where(z => z.EventState == 7), 0.5f); -class ChaosStorm(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ChaosStorm)); -class DarkDelugeLocation(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.DarkDelugeLocation), 5); -class Necromancy(BossModule module) : Components.GenericAOEs(module) +class PainMire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.PainMire), 9) { - private readonly List _necroAOEs = []; - private DateTime delay; - public override IEnumerable ActiveAOEs(int slot, Actor actor) - { - if (_necroAOEs.Count > 0) - { - for (var i = 0; i < _necroAOEs.Count; i++) - { - yield return _necroAOEs[i] with { Color = ArenaColor.AOE }; - } - } - } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (_necroAOEs.Count > 0) - { - hints.AddForbiddenZone(ShapeDistance.Circle(WorldState.Party.Player()!.Position, 4), delay); - } - } - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID is AID.Necromancy2) - { - delay = Module.CastFinishAt(spell).AddSeconds(8); - _necroAOEs.Add(new AOEInstance(new AOEShapeCircle(4f), WorldState.Party.Player()!.Position, default, delay)); - } - } - public override void OnCastFinished(Actor caster, ActorCastInfo spell) - { - if ((AID)spell.Action.ID is AID.Necromancy2) - { - _necroAOEs.Clear(); - } + if (Module.Enemies(OID.BleedVoidzone).Any(x => x.EventState != 7)) + { } + else + base.AddAIHints(slot, actor, assignment, hints); } } -class NecroFetter(BossModule module) : Components.UniformStackSpread(module, 0, 8f, 0, 0, true) + +class BleedVoidzone(BossModule module) : Components.PersistentVoidzone(module, 8, m => m.Enemies(OID.BleedVoidzone).Where(x => x.EventState != 7)); +class TwistedTouch(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.TwistedTouch)); +class ChaosStorm(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ChaosStorm)); +class DarkDeluge(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.DarkDeluge), 5); +class NecrobombBaitAway(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(9.25f), (uint)IconID.Baitaway, ActionID.MakeSpell(AID.DeathThroes), centerAtTarget: true); // note: explosion is not always exactly the position of player, if zombie teleports to player it is player + zombie hitboxradius = 1.25 away + +class Necrobombs(BossModule module) : BossComponent(module) { - private readonly List activeChasers = []; - public override void OnTethered(Actor source, ActorTetherInfo tether) - { - if (tether.ID is (uint)TetherID.ZombieChase) - { - activeChasers.Add(source); - return; - } - else if (tether.ID is (uint)TetherID.ZombieFetterTether) - { - if (activeChasers.Count == 0) - { - foreach (var player in WorldState.Party.WithoutSlot()) - { - Spreads.Add(new(player, 8f)); - } - } - } - } + private readonly NecrobombBaitAway _ba = module.FindComponent()!; + private static readonly AOEShapeCircle circle = new(8); - public override void OnUntethered(Actor source, ActorTetherInfo tether) + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (tether.ID is (uint)TetherID.ZombieChase) - { - activeChasers.Remove(source); + if (_ba.ActiveBaits.Any()) return; - } - else if (tether.ID is (uint)TetherID.ZombieFetterTether) - { - Spreads.Clear(); - } + var forbidden = new List>(); + foreach (var e in WorldState.Actors.Where(x => !x.IsAlly && x.Tether.ID == (uint)TetherID.CrawlingNecrobombs)) + forbidden.Add(circle.Distance(e.Position, default)); + if (forbidden.Count > 0) + hints.AddForbiddenZone(p => forbidden.Min(f => f(p))); } } -class NecroBurst(BossModule module) : Components.GenericAOEs(module) + +class Burst(BossModule module) : Components.GenericAOEs(module) { private readonly List _aoes = []; - private DateTime delay; - private IEnumerable ChaserZombies + private static readonly AOEShapeCircle circle = new(8); + private static readonly HashSet casts = [AID.Burst1, AID.Burst2, AID.Burst3, AID.Burst4, AID.Burst5, AID.Burst6, AID.Burst7, AID.Burst8]; + // Note: Burst5 to Burst8 locations are unknown until players unable to move, so they are irrelevant and not drawn + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnActorModelStateChange(Actor actor, byte modelState, byte animState1, byte animState2) { - get - { - return Module.Enemies(OID.Necrobomb1) - .Concat(Module.Enemies(OID.Necrobomb2)) - .Concat(Module.Enemies(OID.Necrobomb3)) - .Concat(Module.Enemies(OID.Necrobomb4)) - .Where(e => !e.IsTargetable && !e.IsDead); - } + if (modelState == 54) + _aoes.Add(new(circle, actor.Position, default, WorldState.FutureTime(6))); // activation time can be vastly different, even twice as high so we take a conservative delay } - private readonly IEnumerable Bursts = [AID.Burst1, AID.Burst2, AID.Burst3, AID.Burst4]; - public override IEnumerable ActiveAOEs(int slot, Actor actor) + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) { - if (_aoes.Count > 0) - { - for (var i = 0; i < _aoes.Count; i++) - { - yield return _aoes[i] with { Color = ArenaColor.AOE }; - } - } + if (casts.Contains((AID)spell.Action.ID)) + _aoes.Clear(); } - public override void OnCastStarted(Actor caster, ActorCastInfo spell) +} + +class Doom(BossModule module) : BossComponent(module) +{ + private readonly List _doomed = []; + + public static bool CanActorCureDoom(Actor actor) => actor.Role == Role.Healer || actor.Class == Class.BRD; + + public override void OnStatusGain(Actor actor, ActorStatus status) { - if (Bursts.Contains((AID)spell.Action.ID)) - { - delay = Module.CastFinishAt(spell).AddSeconds(4); - foreach (var e in ChaserZombies) - { - _aoes.Add(new AOEInstance(new AOEShapeCircle(10f), e.Position, default, delay)); - } - } + if ((SID)status.ID == SID.Doom) + _doomed.Add(actor); } - public override void OnCastFinished(Actor caster, ActorCastInfo spell) + + public override void OnStatusLose(Actor actor, ActorStatus status) { - if (Bursts.Contains((AID)spell.Action.ID)) - _aoes.Clear(); + if ((SID)status.ID == SID.Doom) + _doomed.Remove(actor); } - public override void OnUntethered(Actor source, ActorTetherInfo tether) + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (tether.ID is (uint)TetherID.ZombieChase) - { - foreach (var e in ChaserZombies) + if (_doomed.Count > 0 && CanActorCureDoom(actor)) + for (var i = 0; i < _doomed.Count; ++i) { - _aoes.Add(new AOEInstance(new AOEShapeCircle(10f), e.Position, default, delay)); + var doomed = _doomed[i]; + if (actor.Role == Role.Healer) + hints.ActionsToExecute.Push(ActionID.MakeSpell(ClassShared.AID.Esuna), doomed, ActionQueue.Priority.High); + else if (actor.Class == Class.BRD) + hints.ActionsToExecute.Push(ActionID.MakeSpell(BRD.AID.WardensPaean), doomed, ActionQueue.Priority.High); } - } } } + class D112SpectralNecromancerStates : StateMachineBuilder { public D112SpectralNecromancerStates(BossModule module) : base(module) { TrivialPhase() .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter(); + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); } } -[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "VeraNala", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 737, NameID = 9511)] -public class D112SpectralNecromancer(WorldState ws, Actor primary) : BossModule(ws, primary, new(-449.6f, -531.6f), new ArenaBoundsCircle(17)); +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 737, NameID = 9508)] +public class D112SpectralNecromancer(WorldState ws, Actor primary) : BossModule(ws, primary, new(-450, -531), new ArenaBoundsCircle(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D121Mudman.cs b/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D121Mudman.cs new file mode 100644 index 0000000000..2e98a0e10f --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D121Mudman.cs @@ -0,0 +1,183 @@ +namespace BossMod.Shadowbringers.Dungeon.D12MatoyasRelict.D121Mudman; + +public enum OID : uint +{ + Boss = 0x300C, // R3.500, x1 + MudVoidzone = 0x1EB145, // R0.5 + MudmansDouble = 0x300D, // R3.5 + MudBubble1 = 0x300E, // R2.0-4.0 + MudBubble2 = 0x3009, // R4.0 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 872, // Boss->player, no cast, single-target + + HardRock = 21631, // Boss->player, 5.0s cast, single-target + Quagmire = 21633, // Helper->location, 4.0s cast, range 6 circle + + PetrifiedPeat = 21632, // Boss->self, 4.0s cast, single-target + PeatPelt = 21634, // Boss->self, 4.0s cast, single-target + + RockyRollVisual1 = 21639, // MudBubble2/MudBubble1->location, no cast, width 0 rect charge, bubble disappears + RockyRollVisual2 = 21635, // MudBubble1->player, 8.0s cast, single-target + RockyRoll1 = 21636, // MudBubble1->location, no cast, width 4 rect charge + RockyRoll2 = 21637, // MudBubble1->location, no cast, width 6 rect charge + RockyRoll3 = 21640, // MudBubble1->location, no cast, width 8 rect charge + + BrittleBrecciaVisual = 21645, // Boss->self, 4.0s cast, single-target + BrittleBreccia1 = 21646, // Helper->self, 4.3s cast, range 6+R 270-degree cone + BrittleBreccia2 = 21647, // Helper->self, 4.3s cast, range 12+R 270-degree donut segment + BrittleBreccia3 = 21648, // Helper->self, 4.3s cast, range 18+R 270-degree donut segment + + StoneAgeVisual = 21649, // Boss->self, 5.0s cast, single-target + StoneAge = 21650, // Helper->self, 5.3s cast, range 20 circle + + TasteDirt = 21641, // MudmansDouble->self, 7.5s cast, single-target + + FallingRockVisual = 21651, // Boss->self, 5.0s cast, single-target, stack + FallingRock = 21652 // Helper->player, 5.0s cast, range 6 circle +} + +public enum TetherID : uint +{ + Mudball = 7 // MudBubble1->player +} + +class StoneAge(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.StoneAge)); +class HardRock(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.HardRock)); +class MudVoidzone(BossModule module) : Components.PersistentVoidzone(module, 5, m => m.Enemies(OID.MudVoidzone)); +class Quagmire(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Quagmire), 6); +class FallingRock(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.FallingRock), 6, 4, 4); + +class BrittleBreccia(BossModule module) : Components.ConcentricAOEs(module, _shapes) +{ + private static readonly AOEShape[] _shapes = [new AOEShapeCone(6.5f, 135.Degrees()), new AOEShapeDonutSector(6.5f, 12.5f, 135.Degrees()), new AOEShapeDonutSector(12.5f, 18.5f, 135.Degrees())]; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.BrittleBreccia1) + AddSequence(caster.Position, Module.CastFinishAt(spell), caster.Rotation); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (Sequences.Count > 0) + { + var order = (AID)spell.Action.ID switch + { + AID.BrittleBreccia1 => 0, + AID.BrittleBreccia2 => 1, + AID.BrittleBreccia3 => 2, + _ => -1 + }; + AdvanceSequence(order, caster.Position, WorldState.FutureTime(1.5f), caster.Rotation); + } + } +} + +class RockyRoll(BossModule module) : Components.GenericBaitAway(module) +{ + private static readonly AOEShapeRect rect1 = new(60, 2); + private static readonly AOEShapeRect rect2 = new(60, 3); + private static readonly AOEShapeRect rect3 = new(60, 4); + private readonly List activeHoles = []; + + private static readonly Dictionary holePositions = new() + { + { 0x0A, new(-202.627f, -162.627f) }, + { 0x0B, new(-157.373f, -162.627f) }, + { 0x0C, new(-202.627f, -117.373f) }, + { 0x0D, new(-157.373f, -117.373f) } + }; + + public override void OnEventEnvControl(byte index, uint state) + { + if (holePositions.TryGetValue(index, out var value)) + { + if (state == 0x00020001) + activeHoles.Add(value); + else if (state == 0x00080004) + activeHoles.Remove(value); + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.Mudball) + CurrentBaits.Add(new(source, WorldState.Actors.Find(tether.Target)!, rect1, WorldState.FutureTime(8.2f))); + } + + public override void OnUntethered(Actor source, ActorTetherInfo tether) + { + if (tether.ID == (uint)TetherID.Mudball) + CurrentBaits.RemoveAll(x => x.Source == source); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + base.DrawArenaForeground(pcSlot, pc); + foreach (var h in activeHoles) + Arena.AddCircle(h, 5, ArenaColor.Safe, 5); + } + + public override void Update() + { + if (CurrentBaits.Count == 0) + return; + + for (var i = 0; i < CurrentBaits.Count; i++) + { + var b = CurrentBaits[i]; + var activation = WorldState.FutureTime(9.7f); + if (b.Source.HitboxRadius is > 2 and <= 3 && b.Shape == rect1) + { + b.Shape = rect2; + b.Activation = activation; + } + else if (b.Source.HitboxRadius > 3 && b.Shape == rect2) + { + b.Shape = rect3; + b.Activation = activation; + } + CurrentBaits[i] = b; + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + base.AddHints(slot, actor, hints); + if (CurrentBaits.Any(x => x.Source == actor)) + hints.Add("Bait into a hole!"); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + var forbidden = new List>(); + foreach (var b in ActiveBaitsOn(actor)) + foreach (var h in activeHoles) + forbidden.Add(ShapeDistance.InvertedRect(b.Source.Position, h, 1)); + if (forbidden.Count > 0) + hints.AddForbiddenZone(p => forbidden.Max(f => f(p))); + } +} + +class MudmanStates : StateMachineBuilder +{ + public MudmanStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 746, NameID = 9735)] +public class Mudman(WorldState ws, Actor primary) : BossModule(ws, primary, new(-180, -140), new ArenaBoundsCircle(19.5f)); diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D122Nixie.cs b/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D122Nixie.cs new file mode 100644 index 0000000000..295ba60be9 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D122Nixie.cs @@ -0,0 +1,207 @@ +namespace BossMod.Shadowbringers.Dungeon.D12MatoyasRelict.D122Nixie; + +public enum OID : uint +{ + Boss = 0x307F, // R2.4 + Icicle = 0x3081, // R1.0 + UnfinishedNixie = 0x3080, // R1.2 + Geyser = 0x1EB0C7, + CloudPlatform = 0x1EA1A1, // R0.5s-2.0 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack1 = 22932, // Boss->player, no cast, single-target + Teleport = 22933, // Boss->location, no cast, single-target + + CrashSmash = 22927, // Boss->self, 3.0s cast, single-target + CrackVisual = 23481, // Icicle->self, 5.0s cast, single-target + Crack = 22928, // Icicle->self, no cast, range 80 width 3 rect + + ShowerPower = 22929, // Boss->self, 3.0s cast, single-target + Gurgle = 22930, // Helper->self, no cast, range 60 width 10 rect + + PitterPatter = 22920, // Boss->self, 3.0s cast, single-target + Sploosh = 22926, // Helper->self, no cast, range 6 circle, geysirs, no dmg, just throwing player around + FallDamage = 22934, // Helper->player, no cast, single-target + + SinginInTheRain = 22921, // UnfinishedNixie->self, 40.0s cast, single-target + SeaShantyVisual = 22922, // Boss->self, no cast, single-target + SeaShanty = 22924, // Helper->self, no cast, ??? + SeaShantyEnrage = 22923, // Helper->self, no cast, ??? + + SplishSplash = 22925, // Boss->self, 3.0s cast, single-target + Sputter = 22931 // Helper->player, 5.0s cast, range 6 circle, spread +} + +public enum TetherID : uint +{ + Tankbuster = 8, // Icicle->player + Gurgle = 3 // Boss->Helper +} + +public enum SID : uint +{ + HeadInTheClouds = 2472, // none->player, extra=0x0 +} + +class Crack(BossModule module) : Components.BaitAwayTethers(module, new AOEShapeRect(80, 1.5f), (uint)TetherID.Tankbuster) +{ + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.Crack) + CurrentBaits.Clear(); + } +} + +class Gurgle(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List aoes = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => aoes; + + private static readonly (WPos, Angle)[] aoePositions = [ + (new(-20, -165), 90.Degrees()), + (new(-20, -155), 90.Degrees()), + (new(-20, -145), 90.Degrees()), + (new(-20, -135), 90.Degrees()), + (new(20, -165), 90.Degrees()), + (new(20, -155), 90.Degrees()), + (new(20, -145), 90.Degrees()), + (new(20, -135), 90.Degrees()), + ]; + + public override void OnEventEnvControl(byte index, uint state) + { + if (state == 0x00020001 && index is >= 0x13 and <= 0x1A) + { + var pos = aoePositions[index - 0x13]; + aoes.Add(new AOEInstance(new AOEShapeRect(60, 5), pos.Item1, pos.Item2, WorldState.FutureTime(9))); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.Gurgle) + aoes.Clear(); + } +} + +class Sputter(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.Sputter), 6); + +class Geyser(BossModule module) : BossComponent(module) +{ + private readonly List Geysers = []; + private DateTime SplooshTime = default; + private BitMask floaters; + + private Actor? BestGeyser => Geysers.Count == 0 ? null : Geysers.MinBy(g => g.Position.Z); + + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 0x12) + { + if (state == 0x00020001) + Arena.Bounds = Nixie.CombinedBounds; + else + { + Arena.Center = Nixie.GroundCenter; + Arena.Bounds = Nixie.DefaultBounds; + floaters.Reset(); + } + } + } + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.Geyser) + { + Geysers.Add(actor); + if (SplooshTime == default) + SplooshTime = WorldState.FutureTime(4.8f); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.Sploosh) + Geysers.RemoveAll(g => g.Position.AlmostEqual(caster.Position, 1)); + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.HeadInTheClouds) + { + floaters.Set(Raid.FindSlot(actor.InstanceID)); + if (floaters.NumSetBits() == Raid.WithoutSlot().Count()) + { + Arena.Center = Nixie.CloudCenter; + Arena.Bounds = Nixie.CloudBounds; + } + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (floaters[pcSlot]) + Arena.ZoneRect(Nixie.GroundCenter, default(Angle), 19.5f, 19.5f, 19.5f, ArenaColor.AOE); + else if (BestGeyser is Actor g) + Arena.ZoneCircle(g.Position, 6, ArenaColor.SafeFromAOE); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (floaters[slot]) + return; + + if (BestGeyser is Actor g) + hints.Add("Go to geyser!", !actor.Position.InCircle(g.Position, 6)); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (floaters[slot]) + hints.AddForbiddenZone(new AOEShapeRect(19.5f, 19.5f, 19.5f), Nixie.GroundCenter); + else if (BestGeyser is Actor g) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(g.Position, 6), SplooshTime); + } +} + +class NixieStates : StateMachineBuilder +{ + public NixieStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 746, NameID = 9738)] +public class Nixie(WorldState ws, Actor primary) : BossModule(ws, primary, GroundCenter, DefaultBounds) +{ + private static ArenaBoundsCustom MakeCloud() + { + var clipper = new PolygonClipper(); + var ground = new PolygonClipper.Operand(CurveApprox.Rect(new(19.5f, 0), new(0, 19.5f))); + var cloud = new PolygonClipper.Operand(CurveApprox.Rect(new(9.5f, 0), new(0, 5.5f)).Select(d => d - new WDir(0, 25))); + return new ArenaBoundsCustom(25f, clipper.Union(ground, cloud)); + } + + public static readonly ArenaBoundsCustom DefaultBounds = new(25f, new(CurveApprox.Rect(new(19.5f, 0), new(0, 19.5f)))); + public static readonly ArenaBoundsRect CloudBounds = new(9.5f, 5.5f); + public static readonly ArenaBoundsCustom CombinedBounds = MakeCloud(); + + public static readonly WPos CloudCenter = new(0, -175); + public static readonly WPos GroundCenter = new(0, -150); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + base.DrawEnemies(pcSlot, pc); + Arena.Actors(Enemies(OID.UnfinishedNixie), ArenaColor.Enemy); + } +} + diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D123MotherPorxie.cs b/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D123MotherPorxie.cs new file mode 100644 index 0000000000..f74aa3f75d --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D12MatoyasRelict/D123MotherPorxie.cs @@ -0,0 +1,186 @@ +namespace BossMod.Shadowbringers.Dungeon.D12MatoyasRelict.D123MotherPorxie; + +public enum OID : uint +{ + Boss = 0x300B, // R3.6 + AeolianCaveSprite = 0x3052, // R1.6 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 19087, // Boss->player, no cast, single-target + + TenderLoinVisual = 22803, // Boss->self, 5.0s cast, single-target + TenderLoin = 22804, // Helper->self, no cast, range 60 circle + + HuffAndPuff1 = 22809, // Boss->self, 8.0s cast, range 40 width 40 rect, knockback 15, source forward + + MediumRearVisual = 22813, // Boss->self, no cast, single-target + MediumRear1 = 22814, // Helper->self, 10.5s cast, range 5-40 donut + MediumRear2 = 22815, // Helper->self, 16.0s cast, range 5-40 donut, after limit break phase success + + MeatMallet = 22806, // Boss->location, 7.0s cast, range 45 circle, damage fall off AOE + + Barbeque = 23331, // Boss->self, 2.5s cast, single-target + BarbequeRect = 22807, // Helper->self, 3.0s cast, range 50 width 5 rect, knockback 15, source forward + BarbequeCircle = 22808, // Helper->location, 3.0s cast, range 5 circle + ToACrispVisual = 22820, // Boss->self, no cast, single-target + ToACrisp = 22821, // Helper->self, no cast, range 10 width 40 rect + + MincedMeatVisual = 22801, // Boss->player, 4.0s cast, single-target + MincedMeat = 22802, // Helper->player, no cast, single-target + + Buffet = 22822, // AeolianCaveSprite->self, 3.0s cast, range 40 width 6 rect + HuffAndPuffVisual = 22810, // Boss->self, 50.0s cast, range 40 width 40 rect, casts during limit break, only visual + Explosion = 20020, // AeolianCaveSprite->self, 2.5s cast, range 80 circle, knocks player up to see a visual knockback hint + HuffAndPuff2 = 22811, // Boss->self, no cast, range 40 width 40 rect, knockback 15, source forward + + BlowItAllDown = 22812, // Boss->self, no cast, range 40 width 40 rect, knockback 50, source forward (on limit break fail) + NeerDoneWell = 20045, // Helper->self, 8.0s cast, range 5-40 donut (on limit break fail) + + OpenFlameVisual = 22818, // Boss->self, 6.0s cast, single-target + OpenFlame = 22819, // Helper->player, no cast, range 5 circle, spread +} + +public enum IconID : uint +{ + Tankbuster = 198, // player + Spreadmarker = 169, // player +} + +class TenderLoin(BossModule module) : Components.RaidwideCastDelay(module, ActionID.MakeSpell(AID.TenderLoinVisual), ActionID.MakeSpell(AID.TenderLoin), 0.8f); +class MincedMeat(BossModule module) : Components.SingleTargetCastDelay(module, ActionID.MakeSpell(AID.MincedMeatVisual), ActionID.MakeSpell(AID.MincedMeat), 0.9f); +class OpenFlame(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.Spreadmarker, ActionID.MakeSpell(AID.OpenFlame), 5, 6.7f); +class MeatMallet(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.MeatMallet), 30); +class BarbequeCircle(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.BarbequeCircle), 5); +class BarbequeRect(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BarbequeRect), new AOEShapeRect(50, 2.5f)); +class Buffet(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Buffet), new AOEShapeRect(40, 3)); + +class HuffAndPuff(BossModule module) : Components.Knockback(module, stopAtWall: true) +{ + private Actor? MediumRear; + private Actor? PuffCast; + private (Actor? Caster, DateTime? Activation) PuffInstant; + + public override IEnumerable Sources(int slot, Actor actor) + { + if (PuffCast is Actor puffer) + yield return new Source(puffer.Position, 15, Module.CastFinishAt(puffer.CastInfo), Direction: puffer.Rotation, Kind: Kind.DirForward); + if (PuffInstant is (Actor c, DateTime t)) + yield return new Source(c.Position, 15, t, Direction: c.Rotation, Kind: Kind.DirForward); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.HuffAndPuff1: + PuffCast = caster; + break; + case AID.MediumRear1: + case AID.MediumRear2: + MediumRear = caster; + break; + case AID.HuffAndPuffVisual: + PuffInstant.Caster = caster; + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.HuffAndPuff1: + PuffCast = null; + break; + case AID.MediumRear1: + case AID.MediumRear2: + MediumRear = null; + break; + case AID.Explosion: + PuffInstant.Activation = WorldState.FutureTime(10.9f); + break; + case AID.HuffAndPuff2: + PuffInstant = (null, null); + break; + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + base.DrawArenaBackground(pcSlot, pc); + + if (MediumRear is Actor rear) + Arena.ZoneDonut(rear.Position, 5, 40, ArenaColor.AOE); + } + + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => MediumRear != null && !pos.InCircle(MediumRear.Position, 5); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + + if (MediumRear is not Actor p) + return; + + foreach (var movement in CalculateMovements(slot, actor)) + { + var offset = movement.from - movement.to; + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(p.Position + offset, 5), Module.CastFinishAt(p.CastInfo)); + break; + } + } +} + +class Barbeque(BossModule module) : BossComponent(module) +{ + private bool active = false; + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Barbeque) + active = true; + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.ToACrisp) + active = false; + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (active) + hints.GoalZones.Add(p => p.X >= 15 ? 50 : 0); + } +} + +class MotherPorxieStates : StateMachineBuilder +{ + public MotherPorxieStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 746, NameID = 9741, Contributors = "Malediktus, xan")] +public class MotherPorxie(WorldState ws, Actor primary) : BossModule(ws, primary, default, new ArenaBoundsSquare(19.5f)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) + { + base.DrawEnemies(pcSlot, pc); + Arena.Actors(Enemies(OID.AeolianCaveSprite), ArenaColor.Enemy); + } +} + diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D131Amhuluk.cs b/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D131Amhuluk.cs new file mode 100644 index 0000000000..dde6907974 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D131Amhuluk.cs @@ -0,0 +1,198 @@ +namespace BossMod.Shadowbringers.Dungeon.D13Paglthan.D131Amhuluk; + +public enum OID : uint +{ + Boss = 0x3169, // R7.008, x1 + Helper = 0x233C, // R0.500, x6, Helper type + _Gen_BallOfLevin = 0x31A2, // R1.300, x0 (spawn during fight) + _Gen_SuperchargedLevin = 0x31A3, // R2.300, x0 (spawn during fight) +} + +public enum AID : uint +{ + _AutoAttack_Attack = 870, // Boss->player, no cast, single-target + _Weaponskill_CriticalRip = 23630, // Boss->player, 5.0s cast, single-target + _Ability_ = 23633, // Boss->location, no cast, single-target + _Spell_LightningBolt = 23627, // Boss->self, 10.0s cast, single-target + _Spell_LightningBolt1 = 23628, // Helper->location, no cast, range 10 circle + _Spell_ElectricBurst = 23629, // Boss->self, 4.5s cast, range 50 width 40 rect + _Spell_Thundercall = 23632, // Boss->self, 4.0s cast, single-target + _Spell_Shock = 23635, // 31A3->self, no cast, range 10 circle + _Spell_Shock1 = 23634, // 31A2->self, no cast, range 5 circle + _Weaponskill_WideBlaster = 24773, // Boss->self, 4.0s cast, range 26 120-degree cone + _Weaponskill_SpikeFlail = 23631, // Boss->self, 1.0s cast, range 25 60-degree cone +} + +public enum SID : uint +{ + _Gen_LightningRod = 2574, // none->player/31B6, extra=0x114 + _Gen_VulnerabilityUp = 1789, // 31A3/31A2->player, extra=0x1 + +} + +public enum IconID : uint +{ + _Gen_Icon_218 = 218, // player +} + +class Levin(BossModule module) : Components.GenericAOEs(module) +{ + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var big in Module.Enemies(OID._Gen_SuperchargedLevin)) + yield return new AOEInstance(new AOEShapeCircle(10), big.Position); + foreach (var small in Module.Enemies(OID._Gen_BallOfLevin)) + yield return new AOEInstance(new AOEShapeCircle(5), small.Position); + } +} + +// claims to be a 50/40 rect, but hits behind boss, idk man +class ElectricBurst(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Spell_ElectricBurst)); +class CriticalRip(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID._Weaponskill_CriticalRip)); + +class SpikeBlaster(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List aoes = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_WideBlaster) + { + aoes.Add(new AOEInstance(new AOEShapeCone(26, 60.Degrees()), caster.Position, spell.Rotation, Module.CastFinishAt(spell), ArenaColor.Danger)); + aoes.Add(new AOEInstance(new AOEShapeCone(25, 30.Degrees()), caster.Position, spell.Rotation + 180.Degrees(), Module.CastFinishAt(spell, 2.7f), ArenaColor.AOE)); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID._Weaponskill_WideBlaster: + aoes.RemoveAt(0); + aoes.Ref(0).Color = ArenaColor.Danger; + break; + case AID._Weaponskill_SpikeFlail: + aoes.RemoveAt(0); + break; + } + } +} + +class LightningRod(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List<(int, Actor)> Rods = []; + private BitMask RodStates; + private BitMask PlayerStates; + private DateTime? LightningBoltAt; + + private IEnumerable ActiveRods => Rods.IncludedInMask(RodStates).Select(r => r.Item2); + private IEnumerable InactiveRods => Rods.ExcludedFromMask(RodStates).Select(r => r.Item2); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (LightningBoltAt == null) + yield break; + + foreach (var r in ActiveRods) + yield return new AOEInstance(new AOEShapeCircle(10), r.Position, Activation: LightningBoltAt.Value); + } + + public override void OnActorCreated(Actor actor) + { + if (actor.OID == 0x31B6) + Rods.Add((Rods.Count, actor)); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID._Spell_LightningBolt) + LightningBoltAt = Module.CastFinishAt(spell); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID._Spell_LightningBolt1) + LightningBoltAt = null; + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID._Gen_LightningRod) + { + if (actor.OID == 0x31B6) + RodStates.Set(Rods.FindIndex(r => r.Item2 == actor)); + else + PlayerStates.Set(Raid.FindSlot(actor.InstanceID)); + } + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID._Gen_LightningRod) + { + if (actor.OID == 0x31B6) + RodStates.Clear(Rods.FindIndex(r => r.Item2 == actor)); + else + PlayerStates.Clear(Raid.FindSlot(actor.InstanceID)); + } + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + base.DrawArenaBackground(pcSlot, pc); + + if (PlayerStates[pcSlot]) + foreach (var rod in InactiveRods) + Arena.AddCircle(rod.Position, 3, ArenaColor.Safe); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + base.DrawArenaForeground(pcSlot, pc); + + foreach (var (_, player) in Raid.WithSlot().IncludedInMask(PlayerStates)) + Arena.AddCircle(player.Position, 10, ArenaColor.Danger); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + base.AddHints(slot, actor, hints); + + if (PlayerStates[slot]) + hints.Add("Pass debuff!"); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + + if (!PlayerStates[slot] || LightningBoltAt == null) + return; + + var inactiveRods = InactiveRods.Select(r => ShapeDistance.InvertedCircle(r.Position, 3)).ToList(); + if (inactiveRods.Count > 0) + { + var zone = ShapeDistance.Intersection(inactiveRods); + hints.AddForbiddenZone(zone, LightningBoltAt.Value); + } + } +} + +class AmhulukStates : StateMachineBuilder +{ + public AmhulukStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 777, NameID = 10075)] +public class Amhuluk(WorldState ws, Actor primary) : BossModule(ws, primary, new(-520, 145), new ArenaBoundsCircle(20)); + diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D132MagitekCore.cs b/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D132MagitekCore.cs new file mode 100644 index 0000000000..84caca2897 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D132MagitekCore.cs @@ -0,0 +1,237 @@ +namespace BossMod.Shadowbringers.Dungeon.D13Paglthan.D132MagitekCore; + +public enum OID : uint +{ + _Gen_ = 0x2E20, // R4.000, x1 + _Gen_1 = 0x2E1E, // R5.000, x1 + _Gen_2 = 0x3346, // R0.500, x1 + _Gen_3 = 0x3345, // R0.500, x1 + _Gen_4 = 0x3344, // R0.500, x2 + _Gen_5 = 0x3353, // R2.000, x3 + _Gen_6 = 0x3355, // R1.000, x1 + _Gen_7 = 0x3343, // R0.500, x1 + Helper = 0x233C, // R0.500, x6, Helper type + _Gen_MagitekFortress = 0x32FE, // R1.000, x1 + _Gen_TemperedImperial = 0x31AD, // R0.500, x3 (spawn during fight) + _Gen_TelotekPredator = 0x31AF, // R2.100, x2 (spawn during fight) + Boss = 0x31AC, // R2.300, x1 + _Gen_MarkIITelotekColossus = 0x31AE, // R3.000, x0 (spawn during fight) + _Gen_TelotekSkyArmor = 0x31B0, // R2.000, x0 (spawn during fight) + _Gen_MagitekMissile = 0x31B2, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + _AutoAttack_Attack = 870, // _Gen_7/_Gen_1/_Gen_/_Gen_4/_Gen_TemperedImperial/_Gen_TelotekPredator/_Gen_TelotekSkyArmor/_Gen_MarkIITelotekColossus->player/3326/31D4, no cast, single-target + _Spell_Fire = 24827, // _Gen_2->318A, 1.0s cast, single-target + _AutoAttack_Attack1 = 872, // _Gen_3/_Gen_6->31D4/3326, no cast, single-target + _Weaponskill_ = 24866, // _Gen_5->3326/31D4, 3.0s cast, single-target + _Weaponskill_1 = 21174, // _Gen_1->self, no cast, range 20 ?-degree cone + _Weaponskill_2 = 24864, // _Gen_5->3326/31D4, no cast, single-target + _Weaponskill_MagitekClaw = 23706, // _Gen_TelotekPredator->player, 4.0s cast, single-target + _Weaponskill_3 = 21175, // _Gen_->self, no cast, range 9 circle + _Ability_StableCannon = 23700, // Helper->self, no cast, range 60 width 10 rect + _Weaponskill_DefensiveReaction = 23710, // Boss->self, 5.0s cast, range 60 circle + _Ability_Aethershot = 23708, // _Gen_TelotekSkyArmor->location, 4.0s cast, range 6 circle + __ = 10758, // _Gen_MagitekMissile->self, no cast, single-target + _Ability_GroundToGroundBallistic = 23703, // Helper->location, 5.0s cast, range 40 circle + _Weaponskill_Exhaust = 23705, // _Gen_MarkIITelotekColossus->self, 4.0s cast, range 40 width 7 rect + _Weaponskill_ExplosiveForce = 23704, // _Gen_MagitekMissile->player, no cast, single-target + _Ability_2TonzeMagitekMissile = 23701, // Helper->location, 5.0s cast, range 12 circle +} + +class DefensiveReaction(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_DefensiveReaction)); +class Aethershot(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Ability_Aethershot), 6); +class Exhaust(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Exhaust), new AOEShapeRect(40, 3.5f)); + +class TwoTonze(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Ability_2TonzeMagitekMissile), 12); + +class StableCannon(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly WPos[] Cannons = [new(-185, 28.3f), new(-175, 28.3f), new(-165, 28.3f)]; + + private readonly List aoes = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => aoes; + + public override void OnEventEnvControl(byte index, uint state) + { + if (index is >= 8 and <= 10) + { + switch (state) + { + case 0x00200010: + aoes.Add(new AOEInstance(new AOEShapeRect(60, 5), Cannons[index - 8], default, WorldState.FutureTime(12.6f))); + break; + case 0x00040004: + aoes.Clear(); + break; + } + } + } + + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID._Ability_StableCannon) + aoes.RemoveAt(0); + } +} + +class GroundToGroundBallistic(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID._Ability_GroundToGroundBallistic), 10, stopAtWall: true) +{ + private StableCannon? cannons; + + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) + { + cannons ??= Module.FindComponent(); + + if (cannons == null) + return false; + + return cannons.ActiveAOEs(slot, actor).Any(e => e.Check(pos)); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + base.AddAIHints(slot, actor, assignment, hints); + + if (Casters.Count == 0) + return; + + var aoes = cannons?.ActiveAOEs(slot, actor).ToList(); + if (aoes == null) + return; + + var source = Casters[0].CastInfo!.LocXZ; + hints.AddForbiddenZone(p => + { + var dist = (p - source).Normalized(); + var proj = Arena.ClampToBounds(p + dist * 10); + return aoes.Any(e => e.Check(proj)) ? -1 : 0; + }, Module.CastFinishAt(Casters[0].CastInfo)); + } +} + +class Launchpad(BossModule module) : BossComponent(module) +{ + private bool active; + + private static readonly WPos Position = new(-175, 30); + + public override void OnEventEnvControl(byte index, uint state) + { + if (index == 0x0D) + { + switch (state) + { + case 0x00020001: + active = true; + Arena.Center = MagitekCore.CombinedCenter; + Arena.Bounds = MagitekCore.CombinedBounds; + break; + case 0x00080004: + active = false; + Arena.Center = MagitekCore.DefaultCenter; + Arena.Bounds = MagitekCore.DefaultBounds; + break; + } + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (active && actor.PosRot.Y < -18) + hints.GoalZones.Add(p => 15 - (p - Position).Length()); + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (active && actor.PosRot.Y < -18) + hints.Add("Go to launchpad!", false); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + if (active && pc.PosRot.Y < -18) + Arena.ZoneCircle(Position, 2, ArenaColor.SafeFromAOE); + } +} + +class MagitekMissile(BossModule module) : BossComponent(module) +{ + private const float Radius = 1.5f; + private readonly List Missiles = []; + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID._Gen_MagitekMissile) + Missiles.Add(actor); + } + + public override void OnActorDestroyed(Actor actor) + { + Missiles.Remove(actor); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_ExplosiveForce) + Missiles.Remove(caster); + } + + public override void DrawArenaBackground(int pcSlot, Actor pc) + { + foreach (var m in Missiles) + Arena.ZoneCircle(m.Position, Radius, ArenaColor.AOE); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var m in Missiles) + hints.AddForbiddenZone(ShapeDistance.Capsule(m.Position, m.Rotation, 7, Radius), WorldState.FutureTime(1.5f)); + } +} + +class MagitekCoreStates : StateMachineBuilder +{ + public MagitekCoreStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 777, NameID = 10076)] +public class MagitekCore(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultCenter, DefaultBounds) +{ + public static readonly WPos DefaultCenter = new(-175, 43); + public static readonly ArenaBounds DefaultBounds = new ArenaBoundsSquare(14.6f); + + private static readonly (WPos, ArenaBoundsCustom) DoubleBounds = MakeCombined(); + public static readonly WPos CombinedCenter = DoubleBounds.Item1; + public static readonly ArenaBounds CombinedBounds = DoubleBounds.Item2; + + public static (WPos, ArenaBoundsCustom) MakeCombined() + { + var ground = CurveApprox.Rect(new WDir(0, 13.75f), new WDir(14.6f, 0), new WDir(0, 14.6f)); + var platform = CurveApprox.Rect(new WDir(0, -20.75f), new WDir(7.5f, 0), new WDir(0, 7.5f)); + var clipper = new PolygonClipper(); + return (new(-175, 29.25f), new(29, clipper.Union(new(ground), new(platform)))); + } + + protected override bool CheckPull() => PrimaryActor.InCombat; + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actors(WorldState.Actors.Where(x => !x.IsAlly), ArenaColor.Enemy); + } +} + diff --git a/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D133LunarBahamut.cs b/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D133LunarBahamut.cs new file mode 100644 index 0000000000..394dfb7dd6 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Dungeon/D13Paglthan/D133LunarBahamut.cs @@ -0,0 +1,128 @@ +namespace BossMod.Shadowbringers.Dungeon.D13Paglthan.D133LunarBahamut; + +public enum OID : uint +{ + Helper = 0x233C, // R0.500, x24, Helper type + Boss = 0x316A, // R8.400, x1 + _Gen_LunarNail = 0x316B, // R1.000, x0 (spawn during fight) +} + +public enum AID : uint +{ + _AutoAttack_ = 23620, // Boss->player, no cast, single-target + _Weaponskill_TwistedScream = 23367, // Boss->self, 3.0s cast, range 40 circle + _Weaponskill_Upburst = 24667, // _Gen_LunarNail->self, 3.0s cast, range 2 circle + _Weaponskill_BigBurst = 23368, // _Gen_LunarNail->self, 4.0s cast, range 9 circle + _Weaponskill_PerigeanBreath = 23385, // Boss->self, 5.0s cast, range 30 90-degree cone + _Weaponskill_AkhMorn = 23381, // Boss->players, 5.0s cast, range 4 circle + _Weaponskill_AkhMornRepeat = 23382, // Boss->players, no cast, range 4 circle + _Weaponskill_Megaflare = 23372, // Boss->self, 3.0s cast, single-target + _Weaponskill_Megaflare1 = 23373, // Helper->player, 5.0s cast, range 5 circle + _Weaponskill_Megaflare2 = 23374, // Helper->location, 3.0s cast, range 6 circle + _Weaponskill_MegaflareDive = 23378, // Boss->self, 4.0s cast, range 41 width 12 rect + _Weaponskill_KanRhai = 23375, // Boss->self, 4.0s cast, single-target + _Weaponskill_KanRhai1 = 23376, // Helper->self, no cast, range 30 width 6 rect + _Weaponskill_ = 23377, // Helper->self, no cast, single-target + _Weaponskill_LunarFlare = 23369, // Boss->self, 3.0s cast, single-target + _Weaponskill_LunarFlare1 = 23370, // Helper->location, 10.0s cast, range 11 circle + _Weaponskill_LunarFlare2 = 23371, // Helper->location, 10.0s cast, range 6 circle + _Weaponskill_Gigaflare = 23383, // Boss->self, 7.0s cast, range 40 circle + _Weaponskill_Flatten = 23384, // Boss->player, 5.0s cast, single-target +} + +class Flatten(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID._Weaponskill_Flatten)); +class KanRhaiBait(BossModule module) : Components.GenericBaitAway(module, centerAtTarget: true) +{ + public static readonly AOEShape Cross = new AOEShapeCross(15, 3); + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if (iconID == 260) + CurrentBaits.Add(new(Module.PrimaryActor, actor, Cross, IgnoreRotation: true)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_KanRhai) + CurrentBaits.Clear(); + } +} +class KanRhaiRepeat(BossModule module) : Components.GenericAOEs(module) +{ + private WPos? Source; + private int Counter; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(Source).Select(p => new AOEInstance(KanRhaiBait.Cross, p)); + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_KanRhai1) + { + Counter++; + if (Source == null) + Source = caster.Position; + else if (Counter >= 20) + { + Source = null; + Counter = 0; + } + } + } +} +class Gigaflare(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_Gigaflare)); +class LunarFlare1(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_LunarFlare1), 11); +class LunarFlare2(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_LunarFlare2), 6); +class MegaflareDive(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_MegaflareDive), new AOEShapeRect(41, 6)); +class Megaflare1(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID._Weaponskill_Megaflare1), 5); +class Megaflare2(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Megaflare2), 6); +class PerigeanBreath(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_PerigeanBreath), new AOEShapeCone(30, 45.Degrees())); +class TwistedScream(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID._Weaponskill_TwistedScream)); +class Upburst(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_Upburst), new AOEShapeCircle(2)); +class BigBurst(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID._Weaponskill_BigBurst), new AOEShapeCircle(9)); +class AkhMorn(BossModule module) : Components.UniformStackSpread(module, 4, 0, 4) +{ + private int Counter; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID._Weaponskill_AkhMorn) + AddStack(WorldState.Actors.Find(spell.TargetID)!, Module.CastFinishAt(spell)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID._Weaponskill_AkhMorn or AID._Weaponskill_AkhMornRepeat) + { + if (++Counter >= 4) + { + Stacks.Clear(); + Counter = 0; + } + } + } +} + +class LunarBahamutStates : StateMachineBuilder +{ + public LunarBahamutStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Contributed, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 777, NameID = 10077)] +public class LunarBahamut(WorldState ws, Actor primary) : BossModule(ws, primary, new(796.5f, -97.5f), new ArenaBoundsCircle(20)); +