diff --git a/BossMod/Components/Cleave.cs b/BossMod/Components/Cleave.cs index e43cce45b0..09d5ee6c38 100644 --- a/BossMod/Components/Cleave.cs +++ b/BossMod/Components/Cleave.cs @@ -9,7 +9,7 @@ public class Cleave(BossModule module, ActionID aid, AOEShape shape, uint enemyO public readonly bool ActiveWhileCasting = activeWhileCasting; public readonly bool OriginAtTarget = originAtTarget; public DateTime NextExpected; - private readonly List _enemies = module.Enemies(enemyOID != 0 ? enemyOID : module.PrimaryActor.OID); + public readonly List Enemies = module.Enemies(enemyOID != 0 ? enemyOID : module.PrimaryActor.OID); public override void AddHints(int slot, Actor actor, TextHints hints) { @@ -21,7 +21,7 @@ public override void AddHints(int slot, Actor actor, TextHints hints) public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (!OriginsAndTargets().Any()) + if (OriginsAndTargets().Count == 0) return; foreach (var (origin, target, angle) in OriginsAndTargets()) @@ -56,10 +56,13 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) } } - private IEnumerable<(Actor origin, Actor target, Angle angle)> OriginsAndTargets() + public virtual List<(Actor origin, Actor target, Angle angle)> OriginsAndTargets() { - foreach (var enemy in _enemies) + var count = Enemies.Count; + List<(Actor, Actor, Angle)> origins = new(count); + for (var i = 0; i < count; ++i) { + var enemy = Enemies[i]; if (enemy.IsDead) continue; @@ -72,8 +75,9 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) var target = WorldState.Actors.Find(enemy.TargetID); if (target != null) { - yield return (OriginAtTarget ? target : enemy, target, Angle.FromDirection(target.Position - enemy.Position)); + origins.Add(new(OriginAtTarget ? target : enemy, target, Angle.FromDirection(target.Position - enemy.Position))); } } + return origins; } } diff --git a/BossMod/Components/Gaze.cs b/BossMod/Components/Gaze.cs index ff6abfd563..8f780811d6 100644 --- a/BossMod/Components/Gaze.cs +++ b/BossMod/Components/Gaze.cs @@ -70,7 +70,7 @@ public static void DrawEye(Vector2 eyeCenter, bool danger) dl.AddCircleFilled(eyeCenter, _eyeInnerR, Colors.Border); } - private static bool HitByEye(Actor actor, Eye eye) => (actor.Rotation + eye.Forward).ToDirection().Dot((eye.Position - actor.Position).Normalized()) >= 0.707107f; // 45-degree + public static bool HitByEye(Actor actor, Eye eye) => (actor.Rotation + eye.Forward).ToDirection().Dot((eye.Position - actor.Position).Normalized()) >= 0.707107f; // 45-degree private Vector2 IndicatorScreenPos(WPos eye) { diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs index 1edb63541b..619e11f77f 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs @@ -20,11 +20,28 @@ public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, public static readonly WPos Phase1BoundsCenter = new(100, 76.28427f); public static readonly PolygonCustom[] Diamond = [new([new(115, 63), new(128.28427f, 76.28427f), new(100, 104.56854f), new(71.71573f, 76.28427f), new(85, 63)])]; private static readonly DonutV[] donut = [new(DefaultCenter, 34, 40, 80)]; + public static readonly Square[] IntersectionBlockers = [.. GenerateIntersectionBlockers()]; public static readonly Shape[] Phase2ShapesND = [new Rectangle(new(100, 115), 24, 3), new Rectangle(new(100, 85), 24, 3), new Rectangle(new(115, 100), 3, 24), new Rectangle(new(85, 100), 3, 24), new Square(new(126.5f, 100), 7.5f), new Square(new(73.5f, 100), 7.5f)]; public static readonly Shape[] Phase2ShapesWD = [.. donut, .. Phase2ShapesND]; public static readonly ArenaBoundsCircle DefaultArena = new(40); public static readonly ArenaBoundsComplex Phase1Bounds = new(Diamond, ScaleFactor: 1.414f); - public static readonly ArenaBoundsComplex Phase2BoundsWD = new(Phase2ShapesWD); - public static readonly ArenaBoundsComplex Phase2BoundsND = new(Phase2ShapesND, donut); + public static readonly ArenaBoundsComplex Phase2BoundsWD = new(Phase2ShapesWD, IntersectionBlockers); + public static readonly ArenaBoundsComplex Phase2BoundsND = new(Phase2ShapesND, [.. IntersectionBlockers, .. donut]); + + private static List GenerateIntersectionBlockers() // at intersections there are small blockers to prevent players from skipping tiles + { + var a45 = 45.Degrees(); + var a135 = 135.Degrees(); + WDir[] dirs = [a45.ToDirection(), a135.ToDirection(), (-a45).ToDirection(), (-a135).ToDirection()]; + WPos[] pos = [new(85, 85), new(115, 85), new(115, 115), new(85, 115)]; + var distance = 3 * MathF.Sqrt(2); + + List squares = new(16); + + for (var i = 0; i < 4; ++i) + for (var j = 0; j < 4; ++j) + squares.Add(new(pos[i] + distance * dirs[j], 1, a45)); + return squares; + } } diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DelugeOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DelugeOfDarkness.cs index 38e635e971..fe5eed39f6 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DelugeOfDarkness.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DelugeOfDarkness.cs @@ -5,13 +5,31 @@ // - 00200010 - phase 1 // - 00020001 - phase 2 // - 00040004 - remove telegraph (note that actual bounds are controlled by something else!) -class Phase2InnerCells(BossModule module) : BossComponent(module) +class Phase2InnerCells(BossModule module) : Components.GenericAOEs(module) { private readonly DateTime[] _breakTime = new DateTime[28]; + private static readonly AOEShapeRect square = new(3, 3, 3); + private static readonly Dictionary _cellIndexToCoordinates = GenerateCellIndexToCoordinates(); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var cell = CellIndex(actor.Position - Arena.Center) - 3; + for (var i = 0; i < 28; ++i) + { + if (i == cell) + { + if (Math.Max(0, (_breakTime[i] - WorldState.CurrentTime).TotalSeconds) < 10) + yield return new(square, CellCenter(i)); + continue; + } + else if (_breakTime[i] != default) + yield return new(square, CellCenter(i), Color: Colors.FutureVulnerable); + } + } public override void AddHints(int slot, Actor actor, TextHints hints) { - var cell = CellIndex(actor.Position - Module.Center) - 3; + var cell = CellIndex(actor.Position - Arena.Center) - 3; var breakTime = cell >= 0 && cell < _breakTime.Length ? _breakTime[cell] : default; if (breakTime != default) { @@ -38,7 +56,7 @@ public override void OnEventEnvControl(byte index, uint state) // 16 1D // 11 13 14 15 1C 1B 1A 18 // 12 19 - if (index is < 3 or > 30) + if (index is < 0x03 or > 0x1E) return; _breakTime[index - 3] = state switch { @@ -48,38 +66,66 @@ public override void OnEventEnvControl(byte index, uint state) }; } - private int CoordinateToCell(float c) => (int)Math.Floor(c / 6); - private int CellIndex(WDir offset) => CellIndex(CoordinateToCell(offset.X), CoordinateToCell(offset.Z)); - private int CellIndex(int x, int y) => (x, y) switch + private static int CoordinateToCell(float c) => (int)Math.Floor(c / 6); + private static int CellIndex(WDir offset) => CellIndex(CoordinateToCell(offset.X), CoordinateToCell(offset.Z)); + private static int CellIndex(int x, int y) => (x, y) switch { - (-4, -3) => 3, - (-3, -4) => 4, - (-3, -3) => 5, - (-2, -3) => 6, - (-1, -3) => 7, - (-3, -2) => 8, - (-3, -1) => 9, - (+3, -3) => 10, - (+2, -4) => 11, - (+2, -3) => 12, - (+1, -3) => 13, - (+0, -3) => 14, - (+2, -2) => 15, - (+2, -1) => 16, - (-4, +2) => 17, - (-3, +3) => 18, - (-3, +2) => 19, - (-2, +2) => 20, - (-1, +2) => 21, - (-3, +1) => 22, - (-3, +0) => 23, - (+3, +2) => 24, - (+2, +3) => 25, - (+2, +2) => 26, - (+1, +2) => 27, - (+0, +2) => 28, - (+2, +1) => 29, - (+2, +0) => 30, - _ => -1 + (-4, -3) => 0x03, + (-3, -4) => 0x04, + (-3, -3) => 0x05, + (-2, -3) => 0x06, + (-1, -3) => 0x07, + (-3, -2) => 0x08, + (-3, -1) => 0x09, + (+3, -3) => 0x0A, + (+2, -4) => 0x0B, + (+2, -3) => 0x0C, + (+1, -3) => 0x0D, + (+0, -3) => 0x0E, + (+2, -2) => 0x0F, + (+2, -1) => 0x10, + (-4, +2) => 0x11, + (-3, +3) => 0x12, + (-3, +2) => 0x13, + (-2, +2) => 0x14, + (-1, +2) => 0x15, + (-3, +1) => 0x16, + (-3, +0) => 0x17, + (+3, +2) => 0x18, + (+2, +3) => 0x19, + (+2, +2) => 0x1A, + (+1, +2) => 0x1B, + (+0, +2) => 0x1C, + (+2, +1) => 0x1D, + (+2, +0) => 0x1E, + _ => 0 }; + + private static Dictionary GenerateCellIndexToCoordinates() + { + var map = new Dictionary(); + for (var x = -4; x <= 3; ++x) + { + for (var y = -4; y <= 3; ++y) + { + var index = CellIndex(x, y); + if (index >= 0) + map[index] = (x, y); + } + } + return map; + } + + public static WPos CellCenter(int breakTimeIndex) + { + var cellIndex = breakTimeIndex + 3; + if (_cellIndexToCoordinates.TryGetValue(cellIndex, out var coordinates)) + { + var worldX = (coordinates.x + 0.5f) * 6; + var worldZ = (coordinates.y + 0.5f) * 6; + return Ch01CloudOfDarkness.DefaultCenter + new WDir(worldX, worldZ); + } + else + return default; + } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index 65dd13703b..6ef3fe1608 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -36,6 +36,7 @@ public enum OID : uint VisionOfRyne = 0x45B4, // R0.750, x0 (spawn during fight) VisionOfGaia = 0x45B5, // R1.500, x0 (spawn during fight) DragonPuddle = 0x1EBD41, // R0.500, x0 (spawn during fight), EventObj type, puddle appears when head is touched + GuardianOfEden = 0x45AE, // R115.380, x0 (spawn during fight), p5 failure state tree } public enum AID : uint @@ -243,6 +244,10 @@ public enum AID : uint CrystallizeTimeHallowedWings1 = 40229, // UsurperOfFrostP4->self, 4.7+1.3s cast, single-target, visual (first knockback) CrystallizeTimeHallowedWings2 = 40230, // UsurperOfFrostP4->self, 0.5+1.3s cast, single-target, visual (second knockback) CrystallizeTimeHallowedWingsAOE = 40332, // UsurperOfFrostP4->self, 0.5s cast, range 40 width 50 rect, knockback 20, heavy damage on first target, vuln on first 4 targets + + MemorysEndP4 = 40305, // OracleOfDarknessP4->self, 10.0s cast, range 100 circle, enrage + AbsoluteZeroP4 = 40245, // UsurperOfFrostP4->self, 10.0s cast, range 100 circle, enrage + ParadiseLost = 40263, // Helper->self, no cast, range 100 circle, wipe on p5 failure state } public enum SID : uint diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index 46eb451fcf..4639f2657e 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -55,6 +55,7 @@ private void Phase34(uint id) P4AkhMornMornAfah(id + 0x120000, 5.8f); P4CrystallizeTime(id + 0x130000, 4.6f); P4AkhMornMornAfah(id + 0x140000, 0.1f); + P4Enrage(id + 0x150000, 2.3f); SimpleState(id + 0xFF0000, 100, "???"); } @@ -615,4 +616,9 @@ private void P4CrystallizeTime(uint id, float delay) ActorTargetable(id + 0xD0, _module.BossP4Usurper, true, 5.3f, "Bosses reappear") .SetHint(StateMachine.StateHint.DowntimeEnd); } + + private void P4Enrage(uint id, float delay) + { + ActorCast(id, _module.BossP4Usurper, AID.AbsoluteZeroP4, delay, 10, true, "Enrage"); + } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs index 01158008de..a894ec8f05 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs @@ -345,7 +345,7 @@ class P3UltimateRelativityDarkBlizzard(BossModule module) : Components.GenericAO private readonly List _sources = []; private DateTime _activation; - private static readonly AOEShapeDonut _shape = new(2, 12); // TODO: verify inner radius + private static readonly AOEShapeDonut _shape = new(4, 12); // TODO: verify inner radius public override IEnumerable ActiveAOEs(int slot, Actor actor) => _sources.Select(s => new AOEInstance(_shape, s.Position, default, _activation)); diff --git a/BossMod/Modules/Shadowbringers/Foray/CriticalEngagement/CE12BayingOfHounds.cs b/BossMod/Modules/Shadowbringers/Foray/CriticalEngagement/CE12BayingOfHounds.cs index 618359b44e..a225491400 100644 --- a/BossMod/Modules/Shadowbringers/Foray/CriticalEngagement/CE12BayingOfHounds.cs +++ b/BossMod/Modules/Shadowbringers/Foray/CriticalEngagement/CE12BayingOfHounds.cs @@ -54,8 +54,8 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) switch ((AID)spell.Action.ID) { case AID.Hellpounce: - var offset = spell.LocXZ - Module.Center; - Activate(spell.LocXZ, Module.Center - offset, WorldState.FutureTime(3.7f)); + var offset = spell.LocXZ - Arena.Center; + Activate(spell.LocXZ, Arena.Center - offset, WorldState.FutureTime(3.7f)); break; case AID.HellpounceSecond: _charge = null; diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs deleted file mode 100644 index 1b0f59a9f0..0000000000 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01Art.cs +++ /dev/null @@ -1,131 +0,0 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA01Art; - -public enum OID : uint -{ - Boss = 0x265A, // R2.7 - Orlasrach = 0x265B, // R2.7 - Owain = 0x2662, // R2.7 - ShadowLinksHelper = 0x1EA1A1, // R2.0 (if pos -134.917, 750.44) - Helper = 0x265C -} - -public enum AID : uint -{ - AutoAttack = 14678, // Boss->player, no cast, single-target - - Thricecull = 14644, // Boss->player, 4.0s cast, single-target, tankbuster - Legendspinner = 14633, // Boss->self, 4.5s cast, range 7-22 donut - Legendcarver = 14632, // Boss->self, 4.5s cast, range 15 circle - AcallamNaSenorach = 14645, // Boss->self, 4.0s cast, range 60 circle - AcallamNaSenorachArt = 14628, // Boss->self, 7.0s cast, range 80 circle, enrage if Owain side does not get pulled, Owain teleports to Art - AcallamNaSenorachOwain = 14629, // Owain->self, 7.0s cast, range 80 circle - Mythcall = 14631, // Boss->self, 2.0s cast, single-target - Mythspinner = 14635, // Orlasrach->self, no cast, range 7-22 donut - Mythcarver = 14634, // Orlasrach->self, no cast, range 15 circle - LegendaryGeas = 14642, // Boss->location, 4.0s cast, range 8 circle - DefilersDeserts = 14643, // Helper->self, 3.5s cast, range 35+R width 8 rect - GloryUnearthedFirst = 14636, // Helper->location, 5.0s cast, range 10 circle - GloryUnearthedRest = 14637, // Helper->location, no cast, range 10 circle - Pitfall = 14639, // Boss->location, 5.0s cast, range 80 circle, damage fall off AOE - PiercingDarkVisual = 14640, // Boss->self, 2.5s cast, single-target - PiercingDark = 14641, // Helper->player, 5.0s cast, range 6 circle, spread -} - -public enum IconID : uint -{ - ChasingAOE = 92, // player->self -} - -class LegendMythSpinnerCarver(BossModule module) : Components.GenericAOEs(module) -{ - private static readonly AOEShapeCircle circle = new(15); - private static readonly AOEShapeDonut donut = new(7, 22); - private readonly List _aoes = new(5); - private bool mythcall; - - public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; - - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - void AddAOE(AOEShape shape) => _aoes.Add(new(shape, caster.Position, default, Module.CastFinishAt(spell))); - void AddAOEs(AOEShape shape) - { - var orlasrach = Module.Enemies(OID.Orlasrach); - for (var i = 0; i < orlasrach.Count; ++i) - _aoes.Add(new(shape, orlasrach[i].Position, default, Module.CastFinishAt(spell, 2.6f))); - mythcall = false; - } - switch ((AID)spell.Action.ID) - { - case AID.Legendcarver: - AddAOE(circle); - if (mythcall) - AddAOEs(circle); - break; - case AID.Legendspinner: - AddAOE(donut); - if (mythcall) - AddAOEs(donut); - break; - case AID.Mythcall: - mythcall = true; - break; - } - } - - public override void OnEventCast(Actor caster, ActorCastEvent spell) - { - if (_aoes.Count != 0 && (AID)spell.Action.ID is AID.Legendcarver or AID.Legendspinner or AID.Mythcarver or AID.Mythspinner) - _aoes.RemoveAt(0); - } -} - -class Thricecull(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Thricecull)); -class AcallamNaSenorach(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AcallamNaSenorach)); -class DefilersDeserts(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DefilersDeserts), new AOEShapeRect(35.5f, 4)); -class Pitfall(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Pitfall), 20); -class LegendaryGeasAOE(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.LegendaryGeas), 8); - -class DefilersDesertsPredict(BossModule module) : Components.GenericAOEs(module) -{ - private static readonly AOEShapeCross cross = new(35.5f, 4); - private readonly List _aoes = new(2); - - public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; - - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - void AddAOE(Angle angle) => _aoes.Add(new(cross, spell.LocXZ, angle, Module.CastFinishAt(spell, 6.9f))); - if ((AID)spell.Action.ID == AID.LegendaryGeas) - { - AddAOE(45.Degrees()); - AddAOE(default); - } - else if ((AID)spell.Action.ID == AID.DefilersDeserts) - _aoes.Clear(); - } -} - -class LegendaryGeasStay(BossModule module) : Components.StayMove(module) -{ - public override void OnActorEAnim(Actor actor, uint state) - { - if ((OID)actor.OID == OID.ShadowLinksHelper && actor.Position.AlmostEqual(new(-134.917f, 750.44f), 1)) - { - if (state == 0x00010002) - Array.Fill(PlayerStates, new(Requirement.Stay2, WorldState.CurrentTime, 1)); - else if (state == 0x00040008) - Array.Clear(PlayerStates); - } - } -} - -class GloryUnearthed(BossModule module) : Components.OpenWorldChasingAOEs(module, new AOEShapeCircle(10), ActionID.MakeSpell(AID.GloryUnearthedFirst), ActionID.MakeSpell(AID.GloryUnearthedRest), 6.5f, 1.5f, 5, true, (uint)IconID.ChasingAOE); -class PiercingDark(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.PiercingDark), 6); -class Mythcounter(BossModule module) : Components.CastCounterMulti(module, [ActionID.MakeSpell(AID.Mythspinner), ActionID.MakeSpell(AID.Mythcarver)]); - -[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7968, PlanLevel = 70)] -public class BA01Art(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) -{ - private static readonly ArenaBoundsComplex arena = new([new Polygon(new(-128.98f, 748), 29.5f, 64)], [new Rectangle(new(-129, 718), 20, 1.15f), new Rectangle(new(-129, 778), 20, 1.48f)]); -} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1Art.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1Art.cs new file mode 100644 index 0000000000..b9ab35075c --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1Art.cs @@ -0,0 +1,50 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Art; + +class Thricecull(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Thricecull)); +class AcallamNaSenorach(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AcallamNaSenorach)); +class DefilersDeserts(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.DefilersDeserts), new AOEShapeRect(35.5f, 4)); +class Pitfall(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Pitfall), 20); +class LegendaryGeasAOE(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.LegendaryGeas), 8); + +class DefilersDesertsPredict(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCross cross = new(35.5f, 4); + private readonly List _aoes = new(2); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + void AddAOE(Angle angle) => _aoes.Add(new(cross, spell.LocXZ, angle, Module.CastFinishAt(spell, 6.9f))); + if ((AID)spell.Action.ID == AID.LegendaryGeas) + { + AddAOE(45.Degrees()); + AddAOE(default); + } + else if ((AID)spell.Action.ID == AID.DefilersDeserts) + _aoes.Clear(); + } +} + +class LegendaryGeasStay(BossModule module) : Components.StayMove(module) +{ + public override void OnActorEAnim(Actor actor, uint state) + { + if ((OID)actor.OID == OID.ShadowLinksHelper && actor.Position.AlmostEqual(new(-134.917f, 750.44f), 1)) + { + if (state == 0x00010002) + Array.Fill(PlayerStates, new(Requirement.Stay2, WorldState.CurrentTime, 1)); + else if (state == 0x00040008) + Array.Clear(PlayerStates); + } + } +} + +class GloryUnearthed(BossModule module) : Components.OpenWorldChasingAOEs(module, new AOEShapeCircle(10), ActionID.MakeSpell(AID.GloryUnearthedFirst), ActionID.MakeSpell(AID.GloryUnearthedRest), 6.5f, 1.5f, 5, true, (uint)IconID.ChasingAOE); +class PiercingDark(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.PiercingDark), 6); + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7968, PlanLevel = 70)] +public class BA1Art(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(-128.98f, 748), 29.5f, 64)], [new Rectangle(new(-129, 718), 20, 1.15f), new Rectangle(new(-129, 778), 20, 1.48f)]); +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtEnums.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtEnums.cs new file mode 100644 index 0000000000..7998716d6d --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtEnums.cs @@ -0,0 +1,39 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Art; + +public enum OID : uint +{ + Boss = 0x265A, // R2.7 + Orlasrach = 0x265B, // R2.7 + Owain = 0x2662, // R2.7 + ShadowLinksHelper = 0x1EA1A1, // R2.0 (if pos -134.917, 750.44) + Helper = 0x265C +} + +public enum AID : uint +{ + AutoAttack = 14678, // Boss->player, no cast, single-target + + Thricecull = 14644, // Boss->player, 4.0s cast, single-target, tankbuster + Legendspinner = 14633, // Boss->self, 4.5s cast, range 7-22 donut + Legendcarver = 14632, // Boss->self, 4.5s cast, range 15 circle + + AcallamNaSenorach = 14645, // Boss->self, 4.0s cast, range 60 circle + AcallamNaSenorachArt = 14628, // Boss->self, 7.0s cast, range 80 circle, enrage if Owain side does not get pulled, Owain teleports to Art + AcallamNaSenorachOwain = 14629, // Owain->self, 7.0s cast, range 80 circle + + Mythcall = 14631, // Boss->self, 2.0s cast, single-target + Mythspinner = 14635, // Orlasrach->self, no cast, range 7-22 donut + Mythcarver = 14634, // Orlasrach->self, no cast, range 15 circle + LegendaryGeas = 14642, // Boss->location, 4.0s cast, range 8 circle + DefilersDeserts = 14643, // Helper->self, 3.5s cast, range 35+R width 8 rect + GloryUnearthedFirst = 14636, // Helper->location, 5.0s cast, range 10 circle + GloryUnearthedRest = 14637, // Helper->location, no cast, range 10 circle + Pitfall = 14639, // Boss->location, 5.0s cast, range 80 circle, damage fall off AOE + PiercingDarkVisual = 14640, // Boss->self, 2.5s cast, single-target + PiercingDark = 14641 // Helper->player, 5.0s cast, range 6 circle, spread +} + +public enum IconID : uint +{ + ChasingAOE = 92 // player->self +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01ArtStates.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtStates.cs similarity index 84% rename from BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01ArtStates.cs rename to BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtStates.cs index 056aedfddc..172ac61292 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA01ArtStates.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtStates.cs @@ -1,8 +1,8 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA01Art; +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Art; -class BA01ArtStates : StateMachineBuilder +class BA1ArtStates : StateMachineBuilder { - public BA01ArtStates(BossModule module) : base(module) + public BA1ArtStates(BossModule module) : base(module) { DeathPhase(0, SinglePhase) .ActivateOnEnter() @@ -62,22 +62,18 @@ private void AcallamNaSenorach(uint id, float delay) private void Mythcall(uint id, float delay) { - Cast(id, AID.Mythcall, delay, 2, "Spawn spears") - .ActivateOnEnter(); + Cast(id, AID.Mythcall, delay, 2, "Spawn spears"); CastMulti(id + 0x10, [AID.Legendcarver, AID.Legendspinner], 6, 4.5f, "In/Out AOE"); - ComponentCondition(id + 0x20, 3, comp => comp.NumCasts != 0, "Spears repeat AOE") - .DeactivateOnExit(); + ComponentCondition(id + 0x20, 3, comp => comp.AOEs.Count == 0, "Spears repeat AOE"); } private void Mythcall2(uint id, float delay) { - Cast(id, AID.Mythcall, delay, 2, "Spawn spears") - .ActivateOnEnter(); + Cast(id, AID.Mythcall, delay, 2, "Spawn spears"); Cast(id + 0x10, AID.PiercingDarkVisual, 6.1f, 2.5f, "Spreads"); CastMulti(id + 0x20, [AID.Legendcarver, AID.Legendspinner], 1.6f, 4.5f, "In/Out AOE"); ComponentCondition(id + 0x30, 0.4f, comp => comp.ActiveSpreads.Count == 0, "Spreads resolve"); - ComponentCondition(id + 0x40, 3, comp => comp.NumCasts != 0, "Spears repeat AOE") - .DeactivateOnExit(); + ComponentCondition(id + 0x40, 3, comp => comp.AOEs.Count == 0, "Spears repeat AOE"); } private void LegendaryGeas(uint id, float delay) diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/LegendMythSpinnerCarver.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/LegendMythSpinnerCarver.cs new file mode 100644 index 0000000000..406fcfdf67 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/LegendMythSpinnerCarver.cs @@ -0,0 +1,59 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Art; + +class LegendMythSpinnerCarver(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCircle circle = new(15); + private static readonly AOEShapeDonut donut = new(7, 22); + public readonly List AOEs = new(5); + private bool mythcall; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var count = AOEs.Count; + if (count == 0) + yield break; + + for (var i = 0; i < count; ++i) + { + var aoe = AOEs[i]; + if (count == 5 && i == 0) + yield return aoe with { Color = Colors.Danger }; + else + yield return aoe; + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + void AddAOE(AOEShape shape) => AOEs.Add(new(shape, caster.Position, default, Module.CastFinishAt(spell))); + void AddAOEs(AOEShape shape) + { + var orlasrach = Module.Enemies(OID.Orlasrach); + for (var i = 0; i < orlasrach.Count; ++i) + AOEs.Add(new(shape, orlasrach[i].Position, default, Module.CastFinishAt(spell, 2.6f))); + mythcall = false; + } + switch ((AID)spell.Action.ID) + { + case AID.Legendcarver: + AddAOE(circle); + if (mythcall) + AddAOEs(circle); + break; + case AID.Legendspinner: + AddAOE(donut); + if (mythcall) + AddAOEs(donut); + break; + case AID.Mythcall: + mythcall = true; + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (AOEs.Count != 0 && (AID)spell.Action.ID is AID.Legendcarver or AID.Legendspinner or AID.Mythcarver or AID.Mythspinner) + AOEs.RemoveAt(0); + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1Owain.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1Owain.cs new file mode 100644 index 0000000000..c109a8ffbe --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1Owain.cs @@ -0,0 +1,70 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; + +class ElementalMagicks(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCircle circle = new(13); + public readonly List AOEs = new(5); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + void AddAOEs(SID sid) + { + var mundberg = Module.Enemies(OID.Munderg); + var activation = Module.CastFinishAt(spell); + for (var i = 0; i < mundberg.Count; ++i) + { + var spear = mundberg[i]; + if (spear.FindStatus(sid) != null) + AOEs.Add(new(circle, spear.Position, default, activation)); + } + AOEs.Add(new(circle, spell.LocXZ, default, activation)); + } + switch ((AID)spell.Action.ID) + { + case AID.ElementalMagicksFireBoss: + AddAOEs(SID.SoulOfFire); + break; + case AID.ElementalMagicksIceBoss: + AddAOEs(SID.SoulOfIce); + break; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (AOEs.Count != 0 && (AID)spell.Action.ID is AID.ElementalMagicksFireBoss or AID.ElementalMagicksFireSpears or AID.ElementalMagicksIceBoss or AID.ElementalMagicksIceSpears) + AOEs.RemoveAt(0); + } +} + +class Thricecull(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Thricecull)); +class AcallamNaSenorach(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AcallamNaSenorach)); +class LegendaryImbas(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.LegendaryImbas)); // applies dorito stacks, seems to get skipped if less than 4 people alive? +class Pitfall(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Pitfall), 20); + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 639, NameID = 7970, PlanLevel = 70)] +public class BA1Owain(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena) +{ + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(128.98f, 748), 29.5f, 64)], [new Rectangle(new(129, 718), 20, 1.15f), new Rectangle(new(129, 778), 20, 1.48f)]); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actor(PrimaryActor); + Arena.Actors(Enemies(OID.IvoryPalm)); + } + + protected override void CalculateModuleAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + for (var i = 0; i < hints.PotentialTargets.Count; ++i) + { + var e = hints.PotentialTargets[i]; + e.Priority = (OID)e.Actor.OID switch + { + OID.IvoryPalm => 1, + _ => 0 + }; + } + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainEnums.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainEnums.cs new file mode 100644 index 0000000000..05ae642815 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainEnums.cs @@ -0,0 +1,53 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; + +public enum OID : uint +{ + Boss = 0x265D, // R2.7 + Art = 0x2661, // R2.7 + Munderg = 0x265E, // R2.7 + IvoryPalm = 0x266B, // R2.0 + Helper1 = 0x265F, + Helper2 = 0x2671, + Helper3 = 0x2660 +} + +public enum AID : uint +{ + AutoAttack = 14679, // Boss->player, no cast, single-target + ElementalShift1 = 14647, // Boss->self, 2.0s cast, single-target + ElementalShift2 = 14649, // Boss->self, no cast, single-target + + AcallamNaSenorach = 14662, // Boss->self, 5.0s cast, range 60 circle + AcallamNaSenorachArt = 14628, // Boss->self, 7.0s cast, range 80 circle, enrage if Art side does not get pulled, Art teleports to Owain + AcallamNaSenorachOwain = 14629, // Owain->self, 7.0s cast, range 80 circle + + Thricecull = 14661, // Boss->player, 5.0s cast, single-target + Mythcall = 14646, // Boss->self, 2.0s cast, single-target + + ElementalMagicksVisual = 14648, // Helper3->self, no cast, single-target + ElementalMagicksFireBoss = 14650, // Boss->self, 5.0s cast, range 13 circle + ElementalMagicksFireSpears = 14652, // Munderg->self, no cast, range 13 circle + ElementalMagicksIceBoss = 14651, // Boss->self, 5.0s cast, range 13 circle + ElementalMagicksIceSpears = 14653, // Munderg->self, no cast, range 13 circle + + Spiritcull = 14654, // Boss->self, 3.0s cast, single-target + LegendaryImbas = 14656, // Helper1/Helper2->self, 5.0s cast, ??? + PiercingLight1 = 14655, // Helper1/Helper2->player, 5.0s cast, range 6 circle + PiercingLight2 = 14660, // Helper1/Helper2->player, 5.0s cast, range 6 circle + Pitfall = 14669, // Boss->location, 5.0s cast, range 80 circle + EurekanAero = 14657, // IvoryPalm->self, no cast, range 6 120-degree cone + Explosion = 14658 // IvoryPalm->self, 6.0s cast, range 60 circle, mini enrage +} + +public enum SID : uint +{ + SoulOfFire = 1783, // none->Munderg, extra=0x121 + SoulOfIce = 1784, // none->Munderg, extra=0x122 + BloodSacrifice = 1753, // none->player, extra=0x0 + Petrification = 610 // none->IvoryPalm, extra=0x0 +} + +public enum IconID : uint +{ + DoritoStack = 55 // player->self +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainStates.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainStates.cs new file mode 100644 index 0000000000..409ba95e06 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainStates.cs @@ -0,0 +1,121 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; + +class BA1OwainStates : StateMachineBuilder +{ + public BA1OwainStates(BossModule module) : base(module) + { + DeathPhase(0, SinglePhase) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } + // all timings seem to have upto 1s variation + private void SinglePhase(uint id) + { + Thricecull(id, 100); + AcallamNaSenorach(id + 0x10000, 7.1f); + Mythcall(id + 0x20000, 7.9f); + Thricecull(id + 0x30000, 10); + AcallamNaSenorach(id + 0x40000, 7); + ElementalShift(id + 0x50000, 7.5f); + Thricecull(id + 0x60000, 11); + Spiritcull(id + 0x70000, 8.3f); + Thricecull(id + 0x60000, 5.1f); + AcallamNaSenorach(id + 0x70000, 6); + PiercingLight2(id + 0x80000, 6.1f); + AcallamNaSenorach(id + 0x90000, 9.7f); + AcallamNaSenorach(id + 0xA0000, 5.1f); + ElementalShiftSpiritcull(id + 0xB0000, 8.3f); + Thricecull(id + 0xC0000, 9.3f); + AcallamNaSenorach(id + 0xD0000, 17.2f); + IvoryPalmElementalMagicks(id + 0xE0000, 1.2f); + // from now on repeats until wipe or victory, this extends timeline until up around 20min since its theoretically possible to solo it as long as Owain is pulled + for (var i = 0; i < 10; ++i) + { + var pid = (uint)(i * 0x10000); + Spiritcull(id += 0xF0000 + pid, 5.1f); + Thricecull(id += 0x100000 + pid, 5.1f); + AcallamNaSenorach(id += 0x11000 + pid, 6); + PiercingLight2(id += 0x120000 + pid, 6.1f); + AcallamNaSenorach(id += 0x130000 + pid, 9.7f); + AcallamNaSenorach(id += 0x140000 + pid, 5.1f); + ElementalShiftSpiritcull(id + 0x150000 + pid, 8.3f); + Thricecull(id += 0x160000 + pid, 9.3f); + AcallamNaSenorach(id += 0x170000 + pid, 17.2f); + IvoryPalmElementalMagicks(id += 0x180000 + pid, 1.2f); + } + } + + private void Thricecull(uint id, float delay) + { + Cast(id, AID.Thricecull, delay, 5, "Tankbuster") + .SetHint(StateMachine.StateHint.Tankbuster); + } + + private void AcallamNaSenorach(uint id, float delay) + { + Cast(id, AID.AcallamNaSenorach, delay, 5, "Raidwide") + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void Mythcall(uint id, float delay) + { + Cast(id, AID.Mythcall, delay, 2, "Spawn spears"); + Cast(id + 0x10, AID.ElementalShift1, 2.2f, 2, "Switch elements"); + CastMulti(id + 0x20, [AID.ElementalMagicksIceBoss, AID.ElementalMagicksFireBoss], 6.3f, 5, "Circle AOEs"); + } + + private void ElementalShift(uint id, float delay) + { + Cast(id, AID.ElementalShift1, delay, 2, "Switch elements"); + CastMulti(id + 0x10, [AID.ElementalMagicksIceBoss, AID.ElementalMagicksFireBoss], 6.4f, 5, "Circle AOEs"); + } + + private void Spiritcull(uint id, float delay) + { + Cast(id, AID.Spiritcull, delay, 3, "Dorito stacks appear") + .ActivateOnEnter(); + ComponentCondition(id + 0x10, 0.1f, comp => comp.Casters.Count != 0, "Spreads appear") + .SetHint(StateMachine.StateHint.Raidwide) + .DeactivateOnExit(); + ComponentCondition(id + 0x20, 5, comp => comp.Spreads.Count == 0, "Spreads and dorito stacks resolve"); + } + + private void PiercingLight2(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Spreads.Count != 0, "Spreads appear"); + CastStart(id + 0x10, AID.Pitfall, 1, "Proximity AOE"); + ComponentCondition(id + 0x20, 3.8f, comp => comp.Spreads.Count == 0, "Spreads resolve"); + CastEnd(id + 0x30, 1, "Proximity AOE resolves"); + } + + private void ElementalShiftSpiritcull(uint id, float delay) + { + Cast(id, AID.ElementalShift1, delay, 2, "Switch elements"); + Cast(id + 0x10, AID.Spiritcull, 6.3f, 3, "Dorito stacks appear") + .ActivateOnEnter(); + ComponentCondition(id + 0x20, 1.1f, comp => comp.Casters.Count != 0, "Spreads appear") + .SetHint(StateMachine.StateHint.Raidwide) + .DeactivateOnExit(); + CastStartMulti(id + 030, [AID.ElementalMagicksIceBoss, AID.ElementalMagicksFireBoss], 2, "Circle AOEs start"); + ComponentCondition(id + 0x40, 3, comp => comp.Spreads.Count == 0, "Spreads resolve"); + ComponentCondition(id + 0x50, 2, comp => comp.AOEs.Count == 0, "Circles resolve"); + } + + private void IvoryPalmElementalMagicks(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Tethers.Count != 0, "Hands spawn"); + Cast(id + 0x10, AID.ElementalShift1, 6.4f, 2, "Switch elements"); + CastMulti(id + 0x20, [AID.ElementalMagicksIceBoss, AID.ElementalMagicksFireBoss], 6.3f, 5, "Circle AOEs"); + Cast(id + 0x30, AID.Thricecull, 6, 5, "Tankbuster") + .SetHint(StateMachine.StateHint.Tankbuster); + ComponentCondition(id + 0x40, 5, comp => comp.Casters.Count == 0, "Hands soft enrage"); // quite some timing variation here since hands could be killed at any time + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs new file mode 100644 index 0000000000..f55a737b69 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs @@ -0,0 +1,67 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; + +class IvoryPalm(BossModule module) : Components.GenericGaze(module, inverted: true) +{ + public readonly List<(Actor target, Actor source)> Tethers = new(2); + + public override IEnumerable ActiveEyes(int slot, Actor actor) + { + var count = Tethers.Count; + if (count == 0) + yield break; + for (var i = 0; i < count; ++i) + { + var tether = Tethers[i]; + if (tether.target == actor && !tether.source.IsDead) // apparently tethers don't get removed immediately upon death + { + yield return new(tether.source.Position); + yield break; + } + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + foreach (var eye in ActiveEyes(slot, actor)) + { + if (HitByEye(actor, eye) != Inverted) + { + hints.Add("Face the hand to petrify it!"); + break; + } + } + } + + public override void OnTethered(Actor source, ActorTetherInfo tether) + { + // only tethers in this fight are from this mechanic, so no need to check tether IDs + Tethers.Add((WorldState.Actors.Find(tether.Target)!, source)); + } + + public override void OnUntethered(Actor source, ActorTetherInfo tether) + { + Tethers.Remove((WorldState.Actors.Find(tether.Target)!, source)); + } +} + +class IvoryPalmExplosion(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.Explosion), "Ivory Palm is enraging!", true); + +class EurekanAero(BossModule module) : Components.Cleave(module, ActionID.MakeSpell(AID.EurekanAero), new AOEShapeCone(6, 60.Degrees()), (uint)OID.IvoryPalm) +{ + public override List<(Actor origin, Actor target, Angle angle)> OriginsAndTargets() + { + var count = Enemies.Count; + List<(Actor, Actor, Angle)> origins = new(count); + for (var i = 0; i < count; ++i) + { + var enemy = Enemies[i]; + if (enemy.IsDead || enemy.FindStatus(SID.Petrification) != null) + continue; + + var target = WorldState.Actors.Find(enemy.TargetID); + if (target != null) + origins.Add(new(OriginAtTarget ? target : enemy, target, Angle.FromDirection(target.Position - enemy.Position))); + } + return origins; + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/Spiritcull.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/Spiritcull.cs new file mode 100644 index 0000000000..33003c546b --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/Spiritcull.cs @@ -0,0 +1,66 @@ +namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; + +class PiercingLight1(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.PiercingLight1), 6); +class PiercingLight2(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.PiercingLight2), 6); + +class Spiritcull(BossModule module) : Components.GenericStackSpread(module) +{ + private readonly List targets = new(6); + + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) + { + if ((IconID)iconID == IconID.DoritoStack) + { + targets.Add(actor); + if (Stacks.Count == 0) + Stacks.Add(new(actor, 1.5f, 24, 24, activation: WorldState.FutureTime(5.1f))); + } + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.BloodSacrifice) + { + targets.Clear(); + Stacks.Clear(); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (targets.Contains(actor)) + base.AddHints(slot, actor, hints); + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (targets.Contains(actor)) + base.AddAIHints(slot, actor, assignment, hints); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + if (Stacks.Count != 0 && targets.Contains(pc)) + { + + Actor? actor = null; + var minDistanceSq = float.MaxValue; + + for (var i = 0; i < targets.Count; ++i) + { + var target = targets[i]; + if (target == pc) + continue; + var distanceSq = (pc.Position - target.Position).LengthSq(); + if (distanceSq < minDistanceSq) + { + minDistanceSq = distanceSq; + actor = target; + } + } + Stacks[0] = Stacks[0] with { Target = actor ?? pc }; + + base.DrawArenaForeground(pcSlot, pc); + } + } +} diff --git a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs index fd2769f8dc..bd87de5cb3 100644 --- a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs +++ b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs @@ -326,11 +326,11 @@ private bool DrawPartyTable() ImGui.TableSetupColumn("Z", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, 90); ImGui.TableSetupColumn("Rot", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, 90); ImGui.TableSetupColumn("HP", ImGuiTableColumnFlags.WidthFixed, 200); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.None, 100); - ImGui.TableSetupColumn("Target", ImGuiTableColumnFlags.None, 100); - ImGui.TableSetupColumn("Cast", ImGuiTableColumnFlags.None, 100); - ImGui.TableSetupColumn("Statuses", ImGuiTableColumnFlags.None, 100); - ImGui.TableSetupColumn("Hints", ImGuiTableColumnFlags.None, 250); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 100); + ImGui.TableSetupColumn("Target", ImGuiTableColumnFlags.WidthStretch, 100); + ImGui.TableSetupColumn("Cast", ImGuiTableColumnFlags.WidthStretch, 100); + ImGui.TableSetupColumn("Statuses", ImGuiTableColumnFlags.WidthStretch, 100); + ImGui.TableSetupColumn("Hints", ImGuiTableColumnFlags.WidthStretch, 250); ImGui.TableHeadersRow(); foreach ((var slot, var player) in _player.WorldState.Party.WithSlot(true)) { @@ -396,10 +396,10 @@ private void DrawEnemyTable(uint oid, List actors) ImGui.TableSetupColumn("Z", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, 90); ImGui.TableSetupColumn("Rot", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, 90); ImGui.TableSetupColumn("HP", ImGuiTableColumnFlags.WidthFixed, 200); - ImGui.TableSetupColumn("Name"); - ImGui.TableSetupColumn("Target"); - ImGui.TableSetupColumn("Cast"); - ImGui.TableSetupColumn("Statuses"); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 200); + ImGui.TableSetupColumn("Target", ImGuiTableColumnFlags.WidthStretch, 200); + ImGui.TableSetupColumn("Cast", ImGuiTableColumnFlags.WidthStretch, 200); + ImGui.TableSetupColumn("Statuses", ImGuiTableColumnFlags.WidthStretch, 200); ImGui.TableHeadersRow(); foreach (var enemy in actors) { @@ -421,10 +421,10 @@ private void DrawAllActorsTable() ImGui.TableSetupColumn("Z", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, 90); ImGui.TableSetupColumn("Rot", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, 90); ImGui.TableSetupColumn("HP", ImGuiTableColumnFlags.WidthFixed, 200); - ImGui.TableSetupColumn("Name"); - ImGui.TableSetupColumn("Target"); - ImGui.TableSetupColumn("Cast"); - ImGui.TableSetupColumn("Statuses"); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 200); + ImGui.TableSetupColumn("Target", ImGuiTableColumnFlags.WidthStretch, 200); + ImGui.TableSetupColumn("Cast", ImGuiTableColumnFlags.WidthStretch, 200); + ImGui.TableSetupColumn("Statuses", ImGuiTableColumnFlags.WidthStretch, 200); ImGui.TableHeadersRow(); foreach (var actor in _player.WorldState.Actors) { diff --git a/TODO b/TODO index 3ae396629b..e7eae64fdc 100644 --- a/TODO +++ b/TODO @@ -75,6 +75,7 @@ autorotation: - dot/regen server tick tracking - brd -- take traits into account (ij proccing rs, ea proccing repertoire) +-- better handle bad misalignment (see JeunoTheFirstWalk_BRD100_VeynHumanmale_2025_01_05_20_42_19.log) - drg -- priorities... -- dragon sight is a true north