diff --git a/BossMod/Components/BaitAway.cs b/BossMod/Components/BaitAway.cs index ce52c99b1e..eba66edacb 100644 --- a/BossMod/Components/BaitAway.cs +++ b/BossMod/Components/BaitAway.cs @@ -75,38 +75,80 @@ public List ActiveBaitsNotOn(Actor target) public WPos BaitOrigin(Bait bait) => (CenterAtTarget ? bait.Target : bait.Source).Position; public bool IsClippedBy(Actor actor, Bait bait) => bait.Shape.Check(actor.Position, BaitOrigin(bait), bait.Rotation); - public IEnumerable PlayersClippedBy(Bait bait) => Raid.WithoutSlot().Exclude(bait.Target).InShape(bait.Shape, BaitOrigin(bait), bait.Rotation); + public List PlayersClippedBy(Bait bait) + { + var actors = Raid.WithoutSlot(); + var len = actors.Length; + List result = new(len); + for (var i = 0; i < len; ++i) + { + var actor = actors[i]; + if (actor != bait.Target && bait.Shape.Check(actor.Position, BaitOrigin(bait), bait.Rotation)) + result.Add(actor); + } + + return result; + } public override void AddHints(int slot, Actor actor, TextHints hints) { if (!EnableHints) return; - + var count = ActiveBaits.Count; + if (count == 0) + return; if (ForbiddenPlayers[slot]) { - if (ActiveBaitsOn(actor).Count != 0) + var activeBaits = ActiveBaitsOn(actor); + if (activeBaits.Count != 0) hints.Add("Avoid baiting!"); } else { - if (ActiveBaitsOn(actor).Any(b => PlayersClippedBy(b).Any())) - hints.Add(BaitAwayHint); + var activeBaits = ActiveBaitsOn(actor); + for (var i = 0; i < activeBaits.Count; ++i) + { + var clippedPlayers = PlayersClippedBy(activeBaits[i]); + if (clippedPlayers.Count != 0) + { + hints.Add(BaitAwayHint); + break; + } + } } - if (!IgnoreOtherBaits && ActiveBaitsNotOn(actor).Any(b => IsClippedBy(actor, b))) - hints.Add("GTFO from baited aoe!"); + if (!IgnoreOtherBaits) + { + var otherActiveBaits = ActiveBaitsNotOn(actor); + for (var i = 0; i < otherActiveBaits.Count; ++i) + { + if (IsClippedBy(actor, otherActiveBaits[i])) + { + hints.Add("GTFO from baited aoe!"); + break; + } + } + } } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { if (ActiveBaits.Count == 0) return; + var activeBaitsNotOnActor = ActiveBaitsNotOn(actor); + var activeBaitsOnActor = ActiveBaitsOn(actor); + var countActiveBaitsNotOnActor = activeBaitsNotOnActor.Count; + var countActiveBaitsOnActor = activeBaitsOnActor.Count; - foreach (var bait in ActiveBaitsNotOn(actor)) + for (var i = 0; i < countActiveBaitsNotOnActor; ++i) + { + var bait = activeBaitsNotOnActor[i]; hints.AddForbiddenZone(bait.Shape, BaitOrigin(bait), bait.Rotation, bait.Activation); - - foreach (var bait in ActiveBaitsOn(actor)) - AddTargetSpecificHints(actor, bait, hints); + } + for (var i = 0; i < countActiveBaitsOnActor; ++i) + { + AddTargetSpecificHints(actor, activeBaitsOnActor[i], hints); + } } private void AddTargetSpecificHints(Actor actor, Bait bait, AIHints hints) diff --git a/BossMod/Components/Towers.cs b/BossMod/Components/Towers.cs index 6eb2328656..8cc116be4b 100644 --- a/BossMod/Components/Towers.cs +++ b/BossMod/Components/Towers.cs @@ -274,7 +274,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } // for tower mechanics in open world since likely not everyone is in your party -public class GenericTowersOpenWorld(BossModule module, ActionID aid = default, bool prioritizeInsufficient = false) : CastCounter(module, aid) +public class GenericTowersOpenWorld(BossModule module, ActionID aid = default, bool prioritizeInsufficient = false, bool prioritizeEmpty = false) : CastCounter(module, aid) { public struct Tower(WPos position, float radius, int minSoakers = 1, int maxSoakers = 1, HashSet? allowedSoakers = null, DateTime activation = default) { @@ -315,6 +315,7 @@ public int NumInside(BossModule module) public readonly List Towers = []; public readonly bool PrioritizeInsufficient = prioritizeInsufficient; // give priority to towers with more than 0 but less than min soakers + public readonly bool PrioritizeEmpty = prioritizeEmpty; // give priority to towers with 0 soakers // default tower styling public static void DrawTower(MiniArena arena, WPos pos, float radius, bool safe) @@ -420,7 +421,16 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } if (!hasForbiddenSoakers) { - if (PrioritizeInsufficient) + if (PrioritizeEmpty) + { + for (var i = 0; i < count; ++i) + { + var t = Towers[i]; + if (t.NumInside(Module) == 0) + forbiddenInverted.Add(ShapeDistance.InvertedCircle(t.Position, t.Radius)); + } + } + else if (PrioritizeInsufficient) // less soakers than max { List insufficientTowers = new(count); for (var i = 0; i < count; ++i) @@ -510,7 +520,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } } -public class CastTowersOpenWorld(BossModule module, ActionID aid, float radius, int minSoakers = 1, int maxSoakers = 1) : GenericTowersOpenWorld(module, aid) +public class CastTowersOpenWorld(BossModule module, ActionID aid, float radius, int minSoakers = 1, int maxSoakers = 1, bool prioritizeInsufficient = false, bool prioritizeEmpty = false) : GenericTowersOpenWorld(module, aid, prioritizeInsufficient, prioritizeEmpty) { public readonly float Radius = radius; public readonly int MinSoakers = minSoakers; diff --git a/BossMod/Data/ActorEnumeration.cs b/BossMod/Data/ActorEnumeration.cs index 79dfe54599..22e24f5a6b 100644 --- a/BossMod/Data/ActorEnumeration.cs +++ b/BossMod/Data/ActorEnumeration.cs @@ -100,24 +100,64 @@ public static IEnumerable InRadiusExcluding(this IEnumerable range } // select actors in specified shape - public static IEnumerable InShape(this IEnumerable range, AOEShape shape, Actor origin) + public static List InShape(this IEnumerable range, AOEShape shape, Actor origin) { - return range.Where(actor => shape.Check(actor.Position, origin)); + List result = []; + + foreach (var actor in range) + { + if (shape.Check(actor.Position, origin)) + { + result.Add(actor); + } + } + + return result; } - public static IEnumerable<(int, Actor)> InShape(this IEnumerable<(int, Actor)> range, AOEShape shape, Actor origin) + public static List<(int, Actor)> InShape(this IEnumerable<(int, Actor)> range, AOEShape shape, Actor origin) { - return range.WhereActor(actor => shape.Check(actor.Position, origin)); + List<(int, Actor)> result = []; + + foreach (var tuple in range) + { + if (shape.Check(tuple.Item2.Position, origin)) + { + result.Add(tuple); + } + } + + return result; } - public static IEnumerable InShape(this IEnumerable range, AOEShape shape, WPos origin, Angle rotation) + public static List InShape(this IEnumerable range, AOEShape shape, WPos origin, Angle rotation) { - return range.Where(actor => shape.Check(actor.Position, origin, rotation)); + List result = []; + + foreach (var actor in range) + { + if (shape.Check(actor.Position, origin, rotation)) + { + result.Add(actor); + } + } + + return result; } - public static IEnumerable<(int, Actor)> InShape(this IEnumerable<(int, Actor)> range, AOEShape shape, WPos origin, Angle rotation) + public static List<(int, Actor)> InShape(this IEnumerable<(int, Actor)> range, AOEShape shape, WPos origin, Angle rotation) { - return range.WhereActor(actor => shape.Check(actor.Position, origin, rotation)); + List<(int, Actor)> result = []; + + foreach (var tuple in range) + { + if (shape.Check(tuple.Item2.Position, origin, rotation)) + { + result.Add(tuple); + } + } + + return result; } // select actors that have tether with specific ID diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs index c42fcd491a..ad05807b0a 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/CurseOfDarkness.cs @@ -13,8 +13,11 @@ public override void Update() CurrentBaits.Clear(); var deadline = WorldState.FutureTime(7d); foreach (var (i, p) in Raid.WithSlot(false, false, true)) - if (_activation[i] != default && _activation[i] < deadline) - CurrentBaits.Add(new(p, p, _shape, _activation[i])); + { + ref var activation = ref _activation[i]; + if (activation != default && activation < deadline) + CurrentBaits.Add(new(p, p, _shape, activation)); + } } public override void OnStatusGain(Actor actor, ActorStatus status) diff --git a/BossMod/Modules/Dawntrail/Dungeon/D04Vanguard/D042Protector.cs b/BossMod/Modules/Dawntrail/Dungeon/D04Vanguard/D042Protector.cs index a8e1873cc2..9ffccd4124 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D04Vanguard/D042Protector.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D04Vanguard/D042Protector.cs @@ -156,10 +156,10 @@ public override void Update() for (var i = 0; i < 4; ++i) { - var aoe = aoeChecks[i]; - if (ActiveAOEs(0, Raid.Player()!).Any(c => c.Shape == aoe.AOE && c.Activation <= WorldState.CurrentTime)) + var aoeCheck = aoeChecks[i]; + if (_aoe is AOEInstance aoe && aoe.Shape == aoeCheck.AOE && aoe.Activation <= WorldState.CurrentTime) { - Arena.Bounds = aoe.Bounds; + Arena.Bounds = aoeCheck.Bounds; _aoe = null; break; } diff --git a/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs b/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs index 6fd0551bff..32f8bf6154 100644 --- a/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs +++ b/BossMod/Modules/Dawntrail/Quest/MSQ/TheWarmthOfTheFamily/Tturuhhetso.cs @@ -57,7 +57,7 @@ public enum IconID : uint } class CandescentRayLineStack(BossModule module) : Components.LineStack(module, null, ActionID.MakeSpell(AID.CandescentRayLineStack), minStackSize: 3, maxStackSize: 3); -class CandescentRayTB(BossModule module) : Components.CastSharedTankbuster(module, ActionID.MakeSpell(AID.CandescentRayTB), new AOEShapeRect(50, 4)) +class CandescentRayTB(BossModule module) : Components.CastSharedTankbuster(module, ActionID.MakeSpell(AID.CandescentRayTB), new AOEShapeRect(50f, 4f)) { public override void AddHints(int slot, Actor actor, TextHints hints) { @@ -70,19 +70,21 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { if (Target == null) return; - var koana = Module.Enemies(OID.Koana).FirstOrDefault(); - var wuk = Module.Enemies(OID.WukLamat).FirstOrDefault(); + var koanas = Module.Enemies(OID.Koana); + var wuks = Module.Enemies(OID.WukLamat); + var koana = koanas.Count != 0 ? koanas[0] : null; + var wuk = wuks.Count != 0 ? wuks[0] : null; var primary = Module.PrimaryActor; if (koana != null) - hints.AddForbiddenZone(ShapeDistance.Cone(primary.Position, 100, primary.AngleTo(koana), Angle.Asin(4 / (koana.Position - primary.Position).Length())), Activation); + hints.AddForbiddenZone(ShapeDistance.Cone(primary.Position, 100f, primary.AngleTo(koana), Angle.Asin(4f / (koana.Position - primary.Position).Length())), Activation); if (wuk != null) - hints.AddForbiddenZone(ShapeDistance.InvertedRect(primary.Position, primary.AngleTo(wuk), 50, 0, 4), Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedRect(primary.Position, primary.AngleTo(wuk), 50f, default, 4f), Activation); } } -class SearingSwell(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SearingSwell), new AOEShapeCone(40, 22.5f.Degrees())); -class Ensnare(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Ensnare), 6); -class TriceraSnare(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.Spreadmarker, ActionID.MakeSpell(AID.TriceraSnare), 6, 4.7f) +class SearingSwell(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.SearingSwell), new AOEShapeCone(40f, 22.5f.Degrees())); +class Ensnare(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.Ensnare), 6f); +class TriceraSnare(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.Spreadmarker, ActionID.MakeSpell(AID.TriceraSnare), 6f, 4.7f) { public override void OnEventDirectorUpdate(uint updateID, uint param1, uint param2, uint param3, uint param4) { @@ -96,7 +98,7 @@ class PrimordialRoar2(BossModule module) : Components.RaidwideCast(module, Actio class OrbCollecting(BossModule module) : BossComponent(module) { - private readonly List _orbs = module.Enemies(OID.Orbs); + private readonly List _orbs = module.Enemies((uint)OID.Orbs); private IEnumerable ActiveOrbs => _orbs.Where(x => x.Tether.ID != 0); @@ -132,9 +134,9 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) class FlameBlast(BossModule module) : Components.GenericAOEs(module) { - private static readonly AOEShapeRect rect = new(20, 2, 20); + private static readonly AOEShapeRect rect = new(20f, 2f, 20f); private readonly List _aoes = []; - private static readonly Angle a90 = 90.Degrees(); + private static readonly Angle a90 = 90f.Degrees(); public override IEnumerable ActiveAOEs(int slot, Actor actor) { @@ -155,13 +157,13 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) public override void OnActorCreated(Actor actor) { - if ((OID)actor.OID == OID.BallOfFire) - _aoes.Add(new(rect, actor.Position, actor.Rotation + a90, WorldState.FutureTime(6.7f))); + if (actor.OID == (uint)OID.BallOfFire) + _aoes.Add(new(rect, WPos.ClampToGrid(actor.Position), actor.Rotation + a90, WorldState.FutureTime(6.7d))); } public override void OnCastFinished(Actor caster, ActorCastInfo spell) { - if (_aoes.Count != 0 && (AID)spell.Action.ID == AID.FlameBlast) + if (_aoes.Count != 0 && spell.Action.ID == (uint)AID.FlameBlast) _aoes.RemoveAt(0); } } @@ -185,13 +187,13 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID == AID.Firestorm) + if (spell.Action.ID == (uint)AID.Firestorm) _aoes.Add(new(circle, caster.Position, spell.Rotation, Module.CastFinishAt(spell))); } public override void OnCastFinished(Actor caster, ActorCastInfo spell) { - if (_aoes.Count != 0 && (AID)spell.Action.ID == AID.Firestorm) + if (_aoes.Count != 0 && spell.Action.ID == (uint)AID.Firestorm) _aoes.RemoveAt(0); } } diff --git a/BossMod/Modules/Dawntrail/Raid/M02NHoneyBLovely/Sweethearts.cs b/BossMod/Modules/Dawntrail/Raid/M02NHoneyBLovely/Sweethearts.cs index 5b1cc5f79a..f0a1340292 100644 --- a/BossMod/Modules/Dawntrail/Raid/M02NHoneyBLovely/Sweethearts.cs +++ b/BossMod/Modules/Dawntrail/Raid/M02NHoneyBLovely/Sweethearts.cs @@ -12,7 +12,7 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) if (count == 0) return []; var aoes = new AOEInstance[count]; - for (var i = 0; i < _hearts.Count; ++i) + for (var i = 0; i < count; ++i) { var h = _hearts[i]; aoes[i] = new(capsule, h.Position, h.Rotation); diff --git a/BossMod/Modules/Stormblood/Dungeon/D04DomaCastle/D043HypertunedGrynewaht.cs b/BossMod/Modules/Stormblood/Dungeon/D04DomaCastle/D043HypertunedGrynewaht.cs index 09520a8f4f..6645d6bbfe 100644 --- a/BossMod/Modules/Stormblood/Dungeon/D04DomaCastle/D043HypertunedGrynewaht.cs +++ b/BossMod/Modules/Stormblood/Dungeon/D04DomaCastle/D043HypertunedGrynewaht.cs @@ -57,22 +57,24 @@ class Chainsaw(BossModule module) : Components.GenericAOEs(module) public override IEnumerable ActiveAOEs(int slot, Actor actor) { - if (_aoe != null) - yield return _aoe.Value with { Origin = Module.PrimaryActor.Position, Rotation = Module.PrimaryActor.Rotation }; + if (_aoe is AOEInstance aoe) + return [aoe with { Origin = Module.PrimaryActor.Position, Rotation = Module.PrimaryActor.Rotation }]; + else + return []; } public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID == AID.ChainsawFirst) + if (spell.Action.ID == (uint)AID.ChainsawFirst) _aoe = new(rect, Module.PrimaryActor.Position, spell.Rotation, Module.CastFinishAt(spell)); } public override void OnEventCast(Actor caster, ActorCastEvent spell) { - switch ((AID)spell.Action.ID) + switch (spell.Action.ID) { - case AID.ChainsawFirst: - case AID.ChainsawRest: + case (uint)AID.ChainsawFirst: + case (uint)AID.ChainsawRest: if (++NumCasts == 5) { _aoe = null; @@ -103,7 +105,7 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (_aoe.ActiveAOEs(slot, actor).Count() <= 1) + if (_aoe.Casters.Count <= 1) base.AddAIHints(slot, actor, assignment, hints); } } @@ -114,42 +116,44 @@ class ThermobaricChargeBait(BossModule module) : Components.GenericBaitAway(modu public override void OnStatusGain(Actor actor, ActorStatus status) { - if ((SID)status.ID == SID.Prey) + if (status.ID == (uint)SID.Prey) CurrentBaits.Add(new(Module.PrimaryActor, actor, circle, status.ExpireAt)); } public override void OnStatusLose(Actor actor, ActorStatus status) { - if ((SID)status.ID == SID.Prey) + if (status.ID == (uint)SID.Prey) CurrentBaits.Clear(); } public override void AddHints(int slot, Actor actor, TextHints hints) { - if (ActiveBaits.Any(x => x.Target == actor)) + if (CurrentBaits.Count != 0 && CurrentBaits[0].Target == actor) hints.Add("Bait away!"); + else + base.AddHints(slot, actor, hints); } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (ActiveBaits.Any(x => x.Target == actor)) - hints.AddForbiddenZone(ShapeDistance.Circle(Arena.Center, 23)); + if (CurrentBaits.Count != 0 && CurrentBaits[0].Target == actor) + hints.AddForbiddenZone(ShapeDistance.Circle(Arena.Center, 23f)); } } class Gunsaw(BossModule module) : Components.GenericBaitAway(module) { - private static readonly AOEShapeRect rect = new(60.9f, 1); + private static readonly AOEShapeRect rect = new(60.9f, 1f); public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if ((AID)spell.Action.ID == AID.GunsawFirst) + if (spell.Action.ID == (uint)AID.GunsawFirst) { var target = Raid.WithoutSlot(false, true, true).FirstOrDefault(x => x.Position.InRect(Module.PrimaryActor.Position, Module.PrimaryActor.Rotation, rect.LengthFront, 0, 0.02f)); if (target != default) CurrentBaits.Add(new(caster, target, rect)); } - else if ((AID)spell.Action.ID == AID.GunsawRest) + else if (spell.Action.ID == (uint)AID.GunsawRest) if (++NumCasts == 4) { CurrentBaits.Clear(); diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/AutoAttacks.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/AutoAttacks.cs new file mode 100644 index 0000000000..b2371c35b4 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/AutoAttacks.cs @@ -0,0 +1,222 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA4ProtoOzma; + +class AutoAttacksCube(BossModule module) : Components.GenericBaitAway(module) +{ + private readonly List targets = new(3); + private static readonly AOEShapeRect rect = new(40.5f, 2f); + + // todo: this is a hack, ideally we need to determine who has the current highest enmity on each platform + // the hack just assumes that people with highest enmity for cube auto attack never changes + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch (spell.Action.ID) + { + case (uint)AID.AutoAttackSphere: + if (targets.Count == 3) + targets.Clear(); + targets.Add(WorldState.Actors.Find(spell.MainTargetID)!); + break; + case (uint)AID.FlareStarVisual: + if (caster == Module.PrimaryActor) + { + var activation = WorldState.FutureTime(4.3d); + AddBaits(ref activation); + } + break; + case (uint)AID.AutoAttackCube: + ++NumCasts; + if (NumCasts % 3 == 0) + { + CurrentBaits.Clear(); + if (NumCasts <= 42) + { + var activation = WorldState.FutureTime(2.6d); // time varies wildly depending on current mechanic, taking lowest + AddBaits(ref activation); + } + } + break; + case (uint)AID.TransfigurationSphere1: + case (uint)AID.TransfigurationSphere2: + case (uint)AID.TransfigurationSphere3: + NumCasts = 0; + break; + } + void AddBaits(ref DateTime activation) + { + var count = targets.Count; + var s = Module.PrimaryActor; + for (var i = 0; i < count; ++i) + CurrentBaits.Add(new(s, targets[i], rect, activation)); + } + } +} + +class AutoAttacksPyramid(BossModule module) : Components.GenericBaitAway(module, centerAtTarget: true) +{ + private static readonly AOEShapeCircle circle = new(4f); + private static readonly AOEShapeRect rect = new(100f, 5f); + private static readonly Angle a120 = 120f.Degrees(), am120 = -120f.Degrees(); + private readonly List players = []; + private bool active; + + // this is just an estimation, targets quickly look random if not in predetermined spots behind platform black hole buffers... + public override void Update() + { + if (active) + { + List actorsP1 = []; + List actorsP2 = []; + List actorsP3 = []; + CurrentBaits.Clear(); + + var primaryPos = Module.PrimaryActor.Position; + var countP = players.Count; + if (countP == 0) + { + foreach (var a in Module.WorldState.Actors.Actors.Values) + if (a.OID == 0) + players.Add(a); + } + + for (var i = 0; i < countP; ++i) + { + var a = players[i]; + var pos = a.Position; + if (rect.Check(pos, primaryPos, default)) + actorsP1.Add(a); + else if (rect.Check(pos, primaryPos, a120)) + actorsP2.Add(a); + else if (rect.Check(pos, primaryPos, am120)) + actorsP3.Add(a); + } + + var countp1 = actorsP1.Count; + var countp2 = actorsP2.Count; + var countp3 = actorsP3.Count; + + SortActors(ref actorsP1); + SortActors(ref actorsP2); + SortActors(ref actorsP3); + + var t1 = countp1 != 0 ? actorsP1[countp1 - 1] : null; + var t2 = countp2 != 0 ? actorsP2[countp2 - 1] : null; + var t3 = countp3 != 0 ? actorsP3[countp3 - 1] : null; + AddBait(ref t1); + AddBait(ref t2); + AddBait(ref t3); + void AddBait(ref Actor? t) + { + if (t != null) + CurrentBaits.Add(new(Module.PrimaryActor, t, circle)); + } + void SortActors(ref List actors) + => actors.Sort((a, b) => + { + var distA = (a.Position - primaryPos).LengthSq(); + var distB = (b.Position - primaryPos).LengthSq(); + return distA.CompareTo(distB); + }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch (spell.Action.ID) + { + case (uint)AID.ExecrationVisual: + if (caster == Module.PrimaryActor) + active = true; + break; + case (uint)AID.TransfigurationSphere1: + case (uint)AID.TransfigurationSphere2: + case (uint)AID.TransfigurationSphere3: + active = false; + CurrentBaits.Clear(); + break; + } + } +} + +class AutoAttacksStar(BossModule module) : Components.GenericStackSpread(module) +{ + private bool active; + private static readonly Angle a120 = 120f.Degrees(), am120 = -120f.Degrees(); + private readonly List players = []; + private static readonly AOEShapeRect rect = new(100f, 5f); + + public override void Update() + { + if (active) + { + List actorsP1 = []; + List actorsP2 = []; + List actorsP3 = []; + Stacks.Clear(); + + var primaryPos = Module.PrimaryActor.Position; + var countP = players.Count; + if (countP == 0) + { + foreach (var a in Module.WorldState.Actors.Actors.Values) + if (a.OID == 0) + players.Add(a); + } + + for (var i = 0; i < countP; ++i) + { + var a = players[i]; + var pos = a.Position; + if (rect.Check(pos, primaryPos, default)) + actorsP1.Add(a); + else if (rect.Check(pos, primaryPos, a120)) + actorsP2.Add(a); + else if (rect.Check(pos, primaryPos, am120)) + actorsP3.Add(a); + } + + var countp1 = actorsP1.Count; + var countp2 = actorsP2.Count; + var countp3 = actorsP3.Count; + + SortActors(ref actorsP1); + SortActors(ref actorsP2); + SortActors(ref actorsP3); + + var t1 = countp1 != 0 ? actorsP1[0] : null; + var t2 = countp2 != 0 ? actorsP2[0] : null; + var t3 = countp3 != 0 ? actorsP3[0] : null; + AddBait(ref t1); + AddBait(ref t2); + AddBait(ref t3); + void AddBait(ref Actor? t) + { + if (t != null) + Stacks.Add(new(t, 6f, 2, 24)); + } + void SortActors(ref List actors) + => actors.Sort((a, b) => + { + var distA = (a.Position - primaryPos).LengthSq(); + var distB = (b.Position - primaryPos).LengthSq(); + return distA.CompareTo(distB); + }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch (spell.Action.ID) + { + case (uint)AID.MourningStarVisual: + if (caster == Module.PrimaryActor) + active = true; + break; + case (uint)AID.TransfigurationSphere1: + case (uint)AID.TransfigurationSphere2: + case (uint)AID.TransfigurationSphere3: + active = false; + Stacks.Clear(); + break; + } + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzma.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzma.cs new file mode 100644 index 0000000000..3e3bba32da --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzma.cs @@ -0,0 +1,71 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA4ProtoOzma; + +class Tornado(BossModule module) : Components.BaitAwayCast(module, ActionID.MakeSpell(AID.Tornado), new AOEShapeCircle(6f), true); +class MeteorStack(BossModule module) : Components.StackWithIcon(module, (uint)IconID.MeteorStack, ActionID.MakeSpell(AID.Meteor), 10f, 5.1f, 4, 24) +{ + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID == (uint)AID.Meteor) // for some reason the stack is location targeted and not player targeted + Stacks.Clear(); + } +} + +class MeteorBait(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.MeteorBaitaway, ActionID.MakeSpell(AID.MeteorImpact), 18f, 8.9f) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (spell.Action.ID == (uint)AID.MeteorImpact) + Spreads.Clear(); + } +} +class MeteorImpact(BossModule module) : Components.SimpleAOEs(module, ActionID.MakeSpell(AID.MeteorImpact), 18f); + +class AccelerationBomb(BossModule module) : Components.StayMove(module, 3f) +{ + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if (status.ID == (uint)SID.AccelerationBomb && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + PlayerStates[slot] = new(Requirement.Stay, status.ExpireAt); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if (status.ID == (uint)SID.AccelerationBomb && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) + PlayerStates[slot] = default; + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.BaldesionArsenal, GroupID = 639, NameID = 7981, SortOrder = 5)] +public class BA4ProtoOzma(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) +{ + private static readonly WPos arenaCenter = new(-17.043f, 29.01f); + private static readonly WPos[] vertices = [new(-41.461f, 23.856f), new(-35.261f, 25.114f), new(-35.017f, 24.387f), new(-30.37f, 27.089f), new(-25.37f, 18.429f), + new(-30.154f, 15.639f), new(-26.262f, 12.808f), new(-22.582f, 11.188f), new(-18.653f, 10.418f), new(-15.122f, 10.418f), + new(-11.165f, 11.273f), new(-7.626f, 12.893f), new(-4.56f, 15.181f), new(-0.37f, 10.44f), new(-4.53f, 7.37f), + new(-9.464f, 5.228f), new(-14.389f, 4.157f), new(-19.697f, 4.157f), new(-24.572f, 5.177f), new(-29.522f, 7.32f), + new(-33.85f, 10.507f), new(-35.698f, 12.464f), new(-46.154f, 6.429f), new(-51.154f, 15.089f), new(-40.595f, 21.181f)]; + // ozma's arena consists of 3 identical segments, so we rotate the vertices, the segments are slighly off from polygonal donut segments (check arenaslices.jpg for visualisation), + // so we can't generate them directly if we want pixel perfectness. max error of this should be about 1/1000th of a yalm + private static readonly ArenaBoundsComplex arena = new([new PolygonCustom(vertices), new PolygonCustom(WPos.GenerateRotatedVertices(arenaCenter, vertices, 120f)), + new PolygonCustom(WPos.GenerateRotatedVertices(arenaCenter, vertices, 240f))]); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actor(PrimaryActor); + Arena.Actors(Enemies((uint)OID.ArsenalUrolith)); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var count = hints.PotentialTargets.Count; + for (var i = 0; i < count; ++i) + { + var e = hints.PotentialTargets[i]; + e.Priority = e.Actor.OID switch + { + (uint)OID.ArsenalUrolith => 1, + _ => 0 + }; + } + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzmaEnums.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzmaEnums.cs new file mode 100644 index 0000000000..63f62b41b7 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzmaEnums.cs @@ -0,0 +1,65 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA4ProtoOzma; + +public enum OID : uint +{ + Boss = 0x25E8, // R13.500, x1 + BlackHoleBuffer = 0x1EA1A1, // R2.0 + Shadow = 0x25E9, // R13.5 + Ozmasphere = 0x25EA, // R1.0 + ArsenalUrolith = 0x25EB, // R3.0 + Helper = 0x2629 +} + +public enum AID : uint +{ + AutoAttackSphere = 14251, // Helper->player, no cast, single-target, targets highest enmity on platform + AutoAttackCube = 14252, // Helper->self, no cast, range 40+R width 4 rect, targets highest enmity on platform + AutoAttackStar = 14262, // Helper->players, no cast, range 6 circle, stack AOE on a random player on each platform + AutoAttackPyramid = 14253, // Helper->players, no cast, range 4 circle, targets furthest player on each platform + AutoAttackAdd = 872, // ArsenalUrolith->player, no cast, single-target + + TransfigurationStar = 14258, // Boss/Shadow->self, no cast, single-target + TransfigurationSphere1 = 14259, // Boss->self, no cast, single-target, star to sphere + TransfigurationSphere2 = 14245, // Boss->self, no cast, single-target, pyramid to sphere + TransfigurationSphere3 = 14239, // Boss->self, no cast, single-target, cube to sphere + TransfigurationCube = 14238, // Shadow/Boss->self, no cast, single-target + TransfigurationPyramid = 14244, // Boss/Shadow->self, no cast, single-target + + MourningStarVisual = 14260, // Boss/Shadow->self, no cast, single-target, transition attack star + MourningStar = 14261, // Helper->self, no cast, range 27 circle + ExecrationVisual = 14246, // Boss/Shadow->self, no cast, single-target, transition attack pyramid + Execration = 14247, // Helper->self, no cast, range 40+R width 11 rect + FlareStarVisual = 14240, // Shadow/Boss->self, no cast, single-target, transition attack cube + FlareStar = 14241, // Helper->self, no cast, range 17+R-38+R donut + + ShootingStarVisual = 14263, // Boss->self, 5.0s cast, single-target + ShootingStar = 14264, // Helper->self, 5.0s cast, range 26 circle + + BlackHole = 14237, // Boss->self, no cast, range 40 circle + + Explosion = 14242, // Ozmasphere->self, no cast, range 6 circle + Meteor = 14248, // Helper->location, no cast, range 10 circle, stack + Holy = 14249, // Boss->self, 4.0s cast, range 50 circle, knockback 3, away from source + + Tornado = 14255, // ArsenalUrolith->players, 5.0s cast, range 6 circle, enrage cast on random player if not killed within 30s, instant kills everyone in circle + MeteorImpact = 14256, // ArsenalUrolith->self, 4.0s cast, range 20 circle + DebrisBurst = 14257, // ArsenalUrolith->self, no cast, range 40 circle + + AccelerationBomb = 14250, // Boss->self, no cast, ??? + + ShootingStarEnrageVisualFirst = 14701, // Boss->self, 10.0s cast, single-target + ShootingStarEnrageVisualRepeat = 14715, // Boss->self, no cast, single-target + ShootingStarEnrageFirst = 14702, // Helper->self, 10.0s cast, range 26 circle + ShootingStarEnrageRepeat = 14716 // Helper->self, no cast, range 26 circle +} + +public enum SID : uint +{ + AccelerationBomb = 1072 // none->player, extra=0x0 +} + +public enum IconID : uint +{ + MeteorStack = 62, // player->self + MeteorBaitaway = 57 // player->self +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzmaStates.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzmaStates.cs new file mode 100644 index 0000000000..06042f45a9 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/BA4ProtoOzmaStates.cs @@ -0,0 +1,29 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA4ProtoOzma; + +class BA4ProtoOzmaStates : StateMachineBuilder +{ + public BA4ProtoOzmaStates(BossModule module) : base(module) + { + DeathPhase(0, SinglePhase) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } + + private void SinglePhase(uint id) + { + SimpleState(id + 0xFF0000, 10000, "???"); + } + //TODO: implement + //private void XXX(uint id, float delay) +} \ No newline at end of file diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Blackhole.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Blackhole.cs new file mode 100644 index 0000000000..d4eb1dfa8e --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Blackhole.cs @@ -0,0 +1,68 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA4ProtoOzma; + +class BlackHole(BossModule module) : Components.GenericTowersOpenWorld(module, prioritizeEmpty: true) +{ + private const string Hint1 = "Stand inside a black hole buffer!"; + private const string Hint2 = "There are uncovered black hole buffers!"; + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch (spell.Action.ID) + { + case (uint)AID.TransfigurationSphere1: + case (uint)AID.TransfigurationSphere2: + case (uint)AID.TransfigurationSphere3: + var buffers = Module.Enemies((uint)OID.BlackHoleBuffer); // for some reason the buffers have slightly different coordinates than their collision data, not sure which is correct + var count = buffers.Count; + var soakers = Tower.Soakers(Module); + for (var i = 0; i < count; ++i) + { + var buffer = buffers[i].Position; + if (Module.InBounds(buffer) && (int)buffer.Z != 44) // filter out irrelevant actors, unfortunately OID is also used for other stuff on this map + { + Towers.Add(new(buffer, 2f, 1, 99, soakers, WorldState.FutureTime(9.1d))); + } + } + break; + + case (uint)AID.BlackHole: + Towers.Clear(); + break; + } + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + var count = Towers.Count; + if (count == 0) + return; + base.DrawArenaForeground(pcSlot, pc); + for (var i = 0; i < count; ++i) + { + var t = Towers[i]; + if (t.NumInside(Module) == 0) + Arena.AddCircle(t.Position, t.Radius, Colors.Vulnerable, 3f); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + var count = Towers.Count; + if (count == 0) + return; + var uncovered = false; + var isInside = false; + for (var i = 0; i < count; ++i) + { + var t = Towers[i]; + if (t.NumInside(Module) == 0) + uncovered = true; + else if (t.IsInside(actor)) + isInside = true; + } + + if (uncovered) + hints.Add(Hint2); + hints.Add(Hint1, !isInside); + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Knockbacks.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Knockbacks.cs new file mode 100644 index 0000000000..278f094f47 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Knockbacks.cs @@ -0,0 +1,54 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA4ProtoOzma; + +class Holy(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.Holy), 3f) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (Casters.Count != 0) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center, 9f), Module.CastFinishAt(Casters[0].CastInfo)); + } +} + +class ShootingStar(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.ShootingStar), 8f, shape: new AOEShapeCircle(26f)) +{ + private readonly TransitionAttacks _aoe = module.FindComponent()!; + private static readonly Angle a60 = 60f.Degrees(), am60 = -60.Degrees(), a180 = 180.Degrees(), a120 = 120f.Degrees(), am120 = -120f.Degrees(), a30 = 30f.Degrees(); + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var count = Casters.Count; + if (count == 0) + return; + var transitionAOE = _aoe.AOEs.Count != 0 ? _aoe.AOEs[0].Shape : null; + var forbidden = new Func[transitionAOE != null ? count : 2 * count]; + var index = 0; + for (var i = 0; i < count; ++i) + { + var caster = Casters[i]; + var pos = caster.Position; + void AddForbiddenCone(Angle direction) => forbidden[index++] = ShapeDistance.InvertedCone(pos, 3.5f, direction, a30); + + switch ((int)pos.X) + { + case -38: + if (transitionAOE != TransitionAttacks.Donut) + AddForbiddenCone(a60); + if (transitionAOE != TransitionAttacks.Circle) + AddForbiddenCone(am120); + break; + case -17: + if (transitionAOE != TransitionAttacks.Donut) + AddForbiddenCone(a180); + if (transitionAOE != TransitionAttacks.Circle) + AddForbiddenCone(default); + break; + case 4: + if (transitionAOE != TransitionAttacks.Donut) + AddForbiddenCone(am60); + if (transitionAOE != TransitionAttacks.Circle) + AddForbiddenCone(a120); + break; + } + } + hints.AddForbiddenZone(ShapeDistance.Intersection(forbidden), Module.CastFinishAt(Casters[0].CastInfo)); + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Ozmaspheres.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Ozmaspheres.cs new file mode 100644 index 0000000000..f917739e4e --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/Ozmaspheres.cs @@ -0,0 +1,80 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA4ProtoOzma; + +class Ozmaspheres(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCapsule capsule = new(6, 3); + + private static List Orbs(BossModule module) + { + var orbs = module.Enemies((uint)OID.Ozmasphere); + var count = orbs.Count; + if (count == 0) + return []; + List orbz = new(count); + for (var i = 0; i < count; ++i) + { + var o = orbs[i]; + if (!o.IsDead) + orbz.Add(o); + } + return orbz; + } + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var orbs = Orbs(Module); + var count = orbs.Count; + if (count == 0 || actor.Role == Role.Tank) + return []; + var aoes = new AOEInstance[count]; + for (var i = 0; i < count; ++i) + { + var o = orbs[i]; + aoes[i] = new(capsule, o.Position, o.Rotation); + } + return aoes; + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + var orbs = Orbs(Module); + if (orbs.Count != 0) + { + if (actor.Role == Role.Tank) + hints.Add("Soak the orbs (with mitigations)!"); + else + hints.Add("Avoid the orbs!"); + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var orbs = Orbs(Module); + var count = orbs.Count; + if (count != 0) + { + var forbidden = new Func[count]; + if (actor.Role == Role.Tank) + { + for (var i = 0; i < count; ++i) + { + var o = orbs[i]; + forbidden[i] = ShapeDistance.InvertedRect(o.Position + 0.5f * o.Rotation.ToDirection(), new WDir(0f, 1f), 0.5f, 0.5f, 0.5f); + } + hints.AddForbiddenZone(ShapeDistance.Intersection(forbidden), DateTime.MaxValue); + } + else + base.AddAIHints(slot, actor, assignment, hints); + } + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + var orbs = Orbs(Module); + var count = orbs.Count; + if (count == 0) + return; + for (var i = 0; i < count; ++i) + Arena.AddCircle(orbs[i].Position, 1f, pc.Role == Role.Tank ? Colors.Safe : 0); + } +} \ No newline at end of file diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/TransitionAttacks.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/TransitionAttacks.cs new file mode 100644 index 0000000000..bf37e7ab1e --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/TransitionAttacks.cs @@ -0,0 +1,81 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA4ProtoOzma; + +class TransitionAttacks(BossModule module) : Components.GenericAOEs(module) +{ + public static readonly AOEShapeCircle Circle = new(27f); + public static readonly AOEShapeDonut Donut = new(17.5f, 38.5f); + private static readonly AOEShapeRect rect = new(40.5f, 5.5f); + public readonly List AOEs = new(3); + private static readonly Angle[] angles = [-120.003f.Degrees(), -0.003f.Degrees(), 119.997f.Degrees()]; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var count = AOEs.Count; + if (count == 0) + return []; + var firstactivation = AOEs[0].Activation; + var aoes = new AOEInstance[count]; + var index = 0; + for (var i = 0; i < count; ++i) + { + var aoe = AOEs[i]; + if ((aoe.Activation - firstactivation).TotalSeconds < 1d) + aoes[index++] = aoe; + } + return aoes[..index]; + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + void AddAOEs() + { + for (var i = 0; i < 3; ++i) + AddAOE(rect, angles[i], new(-17f, 29.012f)); + } + void AddAOE(AOEShape shape, Angle rotation = default, WPos position = default) + => AOEs.Add(new(shape, WPos.ClampToGrid(position == default ? caster.Position : position), rotation, WorldState.FutureTime(7.8))); + void TransfigurationCounter() + { + if (caster == Module.PrimaryActor) + ++NumCasts; + } + switch (spell.Action.ID) + { + case (uint)AID.TransfigurationStar: + TransfigurationCounter(); + if (NumCasts < 7) // no transition AOE at last transition, but enrage start + AddAOE(Circle); + break; + case (uint)AID.TransfigurationCube: + AddAOE(Donut); + TransfigurationCounter(); + break; + case (uint)AID.TransfigurationPyramid: + if (caster == Module.PrimaryActor) + { + AddAOEs(); + TransfigurationCounter(); + } + else + switch ((int)caster.Position.X) + { + case -58: + AddAOE(rect, 59.995f.Degrees()); + break; + case -17: + AddAOE(rect, 180.Degrees()); + break; + case 24: + AddAOE(rect, -60.Degrees()); + break; + } + break; + case (uint)AID.Execration: + case (uint)AID.FlareStar: + case (uint)AID.MourningStar: + if (AOEs.Count != 0) + AOEs.RemoveAt(0); + break; + } + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/arenaslices.jpg b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/arenaslices.jpg new file mode 100644 index 0000000000..0949da528c Binary files /dev/null and b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA4ProtoOzma/arenaslices.jpg differ