diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index c79136fcb8..3488eb18ea 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -142,7 +142,7 @@ public override WDir ClampToBounds(WDir offset) } // if rotation is 0, half-width is along X and half-height is along Z -public record class ArenaBoundsRect(float HalfWidth, float HalfHeight, Angle Rotation = default, float MapResolution = 0.5f) : ArenaBounds(MathF.Max(HalfWidth, HalfHeight), MapResolution) +public record class ArenaBoundsRect(float HalfWidth, float HalfHeight, Angle Rotation = default, float Radius = 0, float MapResolution = 0.5f) : ArenaBounds(Radius > 0 ? Radius : MathF.Max(HalfWidth, HalfHeight), MapResolution) { public readonly WDir Orientation = Rotation.ToDirection(); diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/AeroIII.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/AeroIII.cs new file mode 100644 index 0000000000..39951ca0b9 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/AeroIII.cs @@ -0,0 +1,21 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +class AeroIII(BossModule module) : Components.Knockback(module, ignoreImmunes: true) +{ + public readonly IReadOnlyList Voidzones = module.Enemies(OID.BitingWind); + + private static readonly AOEShapeCircle _shape = new(4); + + public override IEnumerable Sources(int slot, Actor actor) + { + foreach (var v in Voidzones) + yield return new(v.Position, 25, Shape: _shape); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var v in Voidzones) + _shape.Outline(Arena, v.Position); + base.DrawArenaForeground(pcSlot, pc); + } +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/BitterWhirlwind.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/BitterWhirlwind.cs new file mode 100644 index 0000000000..cf27c815d0 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/BitterWhirlwind.cs @@ -0,0 +1,49 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +// TODO: generalize to 'aoe tankswap tankbuster' +class BitterWhirlwind(BossModule module) : Components.GenericBaitAway(module, centerAtTarget: true) +{ + private Actor? _source; + private ulong _prevTarget; + private DateTime _activation; + + private static readonly AOEShapeCircle _shape = new(5); + + public override void Update() + { + CurrentBaits.Clear(); + if (_source != null) + { + var target = WorldState.Actors.Find(_source.CastInfo?.TargetID ?? Module.PrimaryActor.TargetID); + if (target != null) + { + CurrentBaits.Add(new(Module.PrimaryActor, target, _shape, _activation)); + } + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (CurrentBaits.Any(b => b.Target.InstanceID == _prevTarget) && actor.Role == Role.Tank) + hints.Add(_prevTarget != actor.InstanceID ? "Taunt!" : "Pass aggro!"); + base.AddHints(slot, actor, hints); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.BitterWhirlwindAOEFirst) + { + _source = caster; + _activation = spell.NPCFinishAt; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.BitterWhirlwindAOEFirst or AID.BitterWhirlwindAOERest) + { + ++NumCasts; + _prevTarget = spell.MainTargetID; + } + } +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ChasmOfVollok.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ChasmOfVollok.cs new file mode 100644 index 0000000000..f9b31a4e22 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ChasmOfVollok.cs @@ -0,0 +1,108 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +class ChasmOfVollokFangSmall(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.ChasmOfVollokFangSmallAOE)) +{ + public readonly List AOEs = []; + + private static readonly AOEShapeRect _shape = new(2.5f, 2.5f, 2.5f); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.ChasmOfVollokFangSmall) + { + // the visual cast happens on one of the side platforms at intercardinals, offset by 30 + var platformOffset = 30 / 1.41421356f; + var offset = new WDir(caster.Position.X > Module.Center.X ? -platformOffset : +platformOffset, caster.Position.Z > Module.Center.Z ? -platformOffset : +platformOffset); + AOEs.Add(new(_shape, caster.Position + offset, spell.Rotation, spell.NPCFinishAt)); + } + } +} + +// note: we can start showing aoes earlier, right when fang actors spawn +class ChasmOfVollokFangLarge(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.ChasmOfVollokFangLargeAOE)) +{ + public readonly List AOEs = []; + + private static readonly AOEShapeRect _shape = new(5, 5, 5); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.VollokLargeAOE) + { + AOEs.Add(new(_shape, caster.Position, spell.Rotation, spell.NPCFinishAt)); + + var mainOffset = Ex2ZoraalJa.NormalCenter - Module.Center; + var fangOffset = caster.Position - Module.Center; + var mirrorOffset = fangOffset.Dot(mainOffset) > 0 ? -2 * mainOffset : 2 * mainOffset; + AOEs.Add(new(_shape, caster.Position + mirrorOffset, spell.Rotation, spell.NPCFinishAt)); + } + } +} + +class ChasmOfVollokPlayer(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.ChasmOfVollokPlayer), "GTFO from occupied cell!") +{ + public bool Active; + private readonly List _targets = []; + private DateTime _activation; + + private static readonly AOEShapeRect _shape = new(2.5f, 2.5f, 2.5f); + private static readonly WDir _localX = (-135).Degrees().ToDirection(); + private static readonly WDir _localZ = 135.Degrees().ToDirection(); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (!Active) + yield break; + var platformOffset = 2 * (Module.Center - Ex2ZoraalJa.NormalCenter); + foreach (var t in _targets.Exclude(actor)) + { + var playerOffset = t.Position - Ex2ZoraalJa.NormalCenter; + var playerX = _localX.Dot(playerOffset); + var playerZ = _localZ.Dot(playerOffset); + if (Math.Abs(playerX) >= 15 || Math.Abs(playerZ) >= 15) + { + playerOffset -= platformOffset; + playerX = _localX.Dot(playerOffset); + playerZ = _localZ.Dot(playerOffset); + } + var cellX = CoordinateToCell(playerX); + var cellZ = CoordinateToCell(playerZ); + var cellCenter = Ex2ZoraalJa.NormalCenter + _localX * CellCenterCoordinate(cellX) + _localZ * CellCenterCoordinate(cellZ); + + yield return new(_shape, cellCenter, 45.Degrees(), _activation); + if (platformOffset != default) + yield return new(_shape, cellCenter + platformOffset, 45.Degrees(), _activation); + } + } + + public override void Update() + { + // assume that if player dies, he won't participate in the mechanic + _targets.RemoveAll(t => t.IsDead); + } + + public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot, Actor player, ref uint customColor) => PlayerPriority.Normal; + + public override void OnEventIcon(Actor actor, uint iconID) + { + if (iconID == (uint)IconID.ChasmOfVollok) + { + _targets.Add(actor); + _activation = WorldState.FutureTime(6.1f); + } + } + + private int CoordinateToCell(float x) => x switch + { + < -5 => 0, + < 0 => 1, + < 5 => 2, + _ => 3 + }; + + private float CellCenterCoordinate(int c) => -7.5f + c * 5; +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/DrumOfVollok.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/DrumOfVollok.cs new file mode 100644 index 0000000000..849a9080b0 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/DrumOfVollok.cs @@ -0,0 +1,42 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +class DrumOfVollokPlatforms(BossModule module) : BossComponent(module) +{ + public bool Active; + + public override void OnEventEnvControl(byte index, uint state) + { + if (index != 11) + return; + + switch (state) + { + case 0x00800040: + Module.Arena.Bounds = Ex2ZoraalJa.NWPlatformBounds; + Module.Arena.Center += 15 * 135.Degrees().ToDirection(); + Active = true; + break; + case 0x02000100: + Module.Arena.Bounds = Ex2ZoraalJa.NEPlatformBounds; + Module.Arena.Center += 15 * (-135).Degrees().ToDirection(); + Active = true; + break; + } + } +} + +class DrumOfVollok(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.DrumOfVollokAOE), 4, 2, 2); + +class DrumOfVollokKnockback(BossModule module) : Components.Knockback(module, ignoreImmunes: true) +{ + private readonly DrumOfVollok? _main = module.FindComponent(); + + public override IEnumerable Sources(int slot, Actor actor) + { + if (_main == null || _main.Stacks.Any(s => s.Target == actor)) + yield break; + foreach (var s in _main.Stacks) + if (actor.Position.InCircle(s.Target.Position, s.Radius)) + yield return new(s.Target.Position, 25, s.Activation); + } +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/DutysEdge.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/DutysEdge.cs new file mode 100644 index 0000000000..46e0891547 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/DutysEdge.cs @@ -0,0 +1,19 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +// TODO: create and use generic 'line stack' component +class DutysEdge(BossModule module) : Components.GenericWildCharge(module, 4, ActionID.MakeSpell(AID.DutysEdgeAOE), 100) +{ + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.DutysEdgeTarget: + foreach (var (i, p) in Raid.WithSlot(true)) + PlayerRoles[i] = p.InstanceID == spell.MainTargetID ? PlayerRole.Target : PlayerRole.Share; + break; + case AID.DutysEdgeAOE: + ++NumCasts; + break; + } + } +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJa.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJa.cs new file mode 100644 index 0000000000..eedd95ce59 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJa.cs @@ -0,0 +1,28 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +class MultidirectionalDivide(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MultidirectionalDivide), new AOEShapeCross(30, 2)); +class MultidirectionalDivideMain(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MultidirectionalDivideMain), new AOEShapeCross(30, 4)); +class MultidirectionalDivideExtra(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MultidirectionalDivideExtra), new AOEShapeCross(40, 2)); +class RegicidalRage(BossModule module) : Components.TankbusterTether(module, ActionID.MakeSpell(AID.RegicidalRageAOE), (uint)TetherID.RegicidalRage, 8); +class BurningChains(BossModule module) : Components.Chains(module, (uint)TetherID.BurningChains, ActionID.MakeSpell(AID.BurningChainsAOE)); +class HalfCircuitRect(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HalfCircuitAOERect), new AOEShapeRect(60, 60)); +class HalfCircuitDonut(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HalfCircuitAOEDonut), new AOEShapeDonut(10, 30)); +class HalfCircuitCircle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HalfCircuitAOECircle), new AOEShapeCircle(10)); + +[ModuleInfo(BossModuleInfo.Maturity.Verified, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 996, PlanLevel = 100)] +public class Ex2ZoraalJa(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), NormalBounds) +{ + public static readonly WPos NormalCenter = new(100, 100); + public static readonly ArenaBoundsRect NormalBounds = new(20, 20, 45.Degrees()); + public static readonly ArenaBoundsRect SmallBounds = new(10, 10, 45.Degrees(), 20); + public static readonly ArenaBoundsCustom NWPlatformBounds = BuildTwoPlatformsBounds(135.Degrees()); + public static readonly ArenaBoundsCustom NEPlatformBounds = BuildTwoPlatformsBounds(-135.Degrees()); + + private static ArenaBoundsCustom BuildTwoPlatformsBounds(Angle orientation) + { + var dir = orientation.ToDirection(); + var main = new PolygonClipper.Operand(CurveApprox.Rect(dir, 10, 10).Select(p => p - 15 * dir)); + var side = new PolygonClipper.Operand(CurveApprox.Rect(dir, 10, 10).Select(p => p + 15 * dir)); + return new(20, NormalBounds.Clipper.Union(main, side)); + } +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJaEnums.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJaEnums.cs new file mode 100644 index 0000000000..ea7be0b9fd --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJaEnums.cs @@ -0,0 +1,125 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +public enum OID : uint +{ + Boss = 0x42B5, // R10.050, x1 + Helper = 0x233C, // R0.500, x18 (spawn during fight), mixed types + //_Gen_Actor1e8536 = 0x1E8536, // R2.000, x1, EventObj type + //_Gen_Exit = 0x1E850B, // R0.500, x1, EventObj type + FangVollokSmall = 0x42B7, // R1.000, x0 (spawn during fight), 1-cell aoes + FangVollokLarge = 0x42B8, // R2.000, x0 (spawn during fight) + FangBlade = 0x42AB, // R1.000, x0 (spawn during fight), sword aoes + //_Gen_ = 0x19A, // R0.500, x0 (spawn during fight) + ProjectionOfTriumphCircle = 0x4156, // R1.000, x0 (spawn during fight), projection of triumph line + ProjectionOfTriumphDonut = 0x4157, // R1.000, x0 (spawn during fight), projection of triumph line + ProjectionOfTurmoil = 0x42BD, // R1.000, x0 (spawn during fight), projection of turmoil line + BitingWind = 0x42BE, // R0.800, x0 (spawn during fight), knockback zone + //_Gen_ = 0x42B9, // R10.050, x0 (spawn during fight) +} + +public enum AID : uint +{ + AutoAttack = 37799, // Boss->player, no cast, single-target + Teleport = 37717, // Boss->location, no cast, single-target + Actualize = 37784, // Boss->self, 5.0s cast, range 100 circle + + MultidirectionalDivide = 37794, // Boss->self, 5.0s cast, range 30 width 4 cross + MultidirectionalDivideMain = 37795, // Helper->self, 16.0s cast, range 30 width 8 cross + MultidirectionalDivideExtra = 37796, // Helper->self, 16.0s cast, range 40 width 4 rect + ForwardHalfR = 37755, // Boss->self, 8.0+1.0s cast, single-target, visual (jump forward, turn around, cleave right) + ForwardHalfL = 37756, // Boss->self, 8.0+1.0s cast, single-target, visual (jump forward, turn around, cleave left) + BackwardHalfR = 37757, // Boss->self, 8.0+1.0s cast, single-target, visual (jump back, cleave right) + BackwardHalfL = 37758, // Boss->self, 8.0+1.0s cast, single-target, visual (jump back, cleave left) + ForwardEdge = 37759, // Helper->self, 1.0s cast, range 60 width 60 rect + BackwardEdge = 39282, // Helper->self, 1.0s cast, range 60 width 60 rect + HalfFullShortAOE = 37760, // Helper->self, 1.0s cast, range 60 width 120 rect + RegicidalRage = 39227, // Boss->self, 8.0s cast, single-target + RegicidalRageAOE = 39228, // Helper->players, no cast, range 8 circle tankbuster tether + + DawnOfAnAge = 37783, // Boss->self, 7.0s cast, range 100 circle, raidwide + arena transition + VollokSmall = 37719, // Boss->self, 4.0s cast, single-target, visual (spawn 16 small fangs) + Sync = 37721, // Boss->self, 5.0s cast, single-target, visual (mirror fangs to main arena) + ChasmOfVollokFangSmall = 37785, // FangVollokSmall->self, 8.0s cast, range 5 width 5 rect, outside main area + ChasmOfVollokFangSmallAOE = 37786, // Helper->self, 1.0s cast, range 5 width 5 rect, mirrored to main area + HalfFullR = 37736, // Boss->self, 6.0s cast, single-target, visual (cleave right) + HalfFullL = 37737, // Boss->self, 6.0s cast, single-target, visual (cleave left) + HalfFullLongAOE = 37790, // Helper->self, 6.3s cast, range 60 width 120 rect + GreaterGateway = 37761, // Boss->self, 4.0s cast, single-target, visual (create portals) + BladeWarp = 37726, // Boss->self, 4.0s cast, single-target, visual (create swords) + ForgedTrack = 37727, // Boss->self, 4.0s cast, single-target, visual (tether swords) + ForgedTrackVisual = 37728, // Helper->19A, no cast, single-target, ??? + ForgedTrackPreview = 37788, // FangBlade->self, 10.9s cast, range 20 width 5 rect + ForgedTrackAOE = 37789, // FangBlade->self, no cast, range 20 width 5 rect (narrow line) + FieryEdge = 37762, // FangBlade->self, no cast, single-target, visual (wide/triple line) + FieryEdgeAOECenter = 37763, // Helper->self, no cast, range 20 width 5 rect, central aoe + FieryEdgeAOESide = 37764, // Helper->self, no cast, range 20 width 5 rect, side aoe + StormyEdge = 37765, // FangBlade->self, no cast, single-target, visual (knockback line) + StormyEdgeAOE = 37766, // Helper->self, 0.5s cast, range 20 width 5 rect, central aoe + StormyEdgeKnockback = 37767, // Helper->self, 0.5s cast, range 10 width 20 rect, knockback 7 + ChasmOfVollokPlayer = 37769, // Helper->self, no cast, ???, aoe in player's cell + + ProjectionOfTriumph = 37770, // Boss->self, 5.0s cast, single-target, visual (lines with circles and donuts) + SiegeOfVollok = 37771, // FangVollokSmall->self, 0.5s cast, range ?-8 donut + WallsOfVollok = 37772, // FangVollokSmall->self, 0.5s cast, range 4 circle + ProjectionOfTurmoil = 39560, // Boss->self, 5.0s cast, single-target, visual (line with stacks) + MightOfVollok = 37773, // Helper->players, no cast, range 8 circle, shared damage with vuln + BitterWhirlwind = 39229, // Boss->self, 5.0s cast, single-target, visual (3-hit aoe tankbuster with swaps/invuln) + BitterWhirlwindAOEFirst = 39230, // Helper->player, 5.0s cast, range 5 circle, aoe tankbuster + BitterWhirlwindRest = 39231, // Boss->self, no cast, single-target, visual (second+ hits) + BitterWhirlwindAOERest = 39232, // Helper->player, no cast, range 5 circle, aoe tankbuster + + DrumOfVollok = 37774, // Boss->self, 7.4+0.6s cast, single-target, visual (enumerations) + DrumOfVollokAOE = 37775, // Helper->players, 8.0s cast, range 4 circle, 2-man stack, partner gets knockback 25 + VollokLarge = 37778, // Boss->self, 5.0s cast, single-target, visual (spawn 2 large fangs) + VollokLargeAOE = 37779, // FangVollokLarge->self, 8.0s cast, range 10 width 10 rect, aoe under large swords + ChasmOfVollokFangLargeAOE = 37780, // Helper->self, 1.0s cast, range 10 width 10 rect, mirrored to other platform + AeroIII = 37776, // Boss->self, 5.0s cast, single-target, visual (knockback zones) + AeroIIIAOE = 37777, // BitingWind->self, no cast, range 4 circle, voidzone with knockback + ForwardHalfLongR = 39322, // Boss->self, 9.0+1.0s cast, single-target, visual (jump forward, turn around, cleave right) + ForwardHalfLongL = 39323, // Boss->self, 9.0+1.0s cast, single-target, visual (jump forward, turn around, cleave left) + BackwardHalfLongR = 39324, // Boss->self, 9.0+1.0s cast, single-target, visual (jump back, cleave right) + BackwardHalfLongL = 39325, // Boss->self, 9.0+1.0s cast, single-target, visual (jump back, cleave left) + DutysEdge = 37748, // Boss->self, 4.9s cast, single-target, visual (line stack) + DutysEdgeTarget = 35567, // Helper->player, no cast, single-target, visual (target selection for line stack) + DutysEdgeRepeat = 37749, // Boss->self, no cast, single-target, visual (repeated hits) + DutysEdgeAOE = 38055, // Helper->self, no cast, range 100 width 8 rect + BurningChains = 37781, // Boss->self, 5.0s cast, single-target, visual (chains) + BurningChainsAOE = 37782, // Helper->self, no cast, ??? + + HalfCircuitCircle = 37739, // Boss->self, 7.0s cast, single-target + HalfCircuitDonut = 37740, // Boss->self, 7.0s cast, single-target + HalfCircuitAOERect = 37791, // Helper->self, 7.3s cast, range 60 width 120 rect + HalfCircuitAOEDonut = 37792, // Helper->self, 7.0s cast, range 10-30 donut + HalfCircuitAOECircle = 37793, // Helper->self, 7.0s cast, range 10 circle + + //_Ability_SmitingCircuit = 37733, // 42B9->self, no cast, single-target + //_Ability_SmitingCircuit = 37732, // 42B9->self, no cast, single-target +} + +public enum SID : uint +{ + //_Gen_ = 2397, // none->FangVollokSmall/42B9, extra=0x2C7/0x2CD + Projection = 4047, // none->player, extra=0x0 + //_Gen_Sprint = 481, // none->ProjectionOfTurmoil, extra=0x32 + //_Gen_Bleeding = 3077, // none->player, extra=0x0 + //_Gen_WindResistanceDownII = 2096, // Helper/BitingWind->player, extra=0x0 + //_Gen_BrinkOfDeath = 44, // none->player, extra=0x0 + //_Gen_Liftoff = 3262, // BitingWind->player, extra=0x0 + //_Gen_MagicVulnerabilityUp = 3516, // Helper->player, extra=0x1/0x2/0x3/0x4 + //_Gen_BurningChains = 769, // none->player, extra=0x0 + //_Gen_Bleeding = 3078, // none->player, extra=0x0 +} + +public enum IconID : uint +{ + ChasmOfVollok = 185, // player + BitterWhirlwind = 343, // player + DrumOfVollok = 539, // player +} + +public enum TetherID : uint +{ + RegicidalRage = 89, // player->Boss + ForgedTrack = 86, // FangBlade->Boss + BurningChains = 128, // player->player +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJaStates.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJaStates.cs new file mode 100644 index 0000000000..17f0bdfdba --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/Ex2ZoraalJaStates.cs @@ -0,0 +1,285 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +class Ex2ZoraalJaStates : StateMachineBuilder +{ + public Ex2ZoraalJaStates(BossModule module) : base(module) + { + DeathPhase(0, SinglePhase); + } + + private void SinglePhase(uint id) + { + Actualize(id, 10.7f); + MultidirectionalDivideHalf(id + 0x10000, 5.6f); + MultidirectionalDivideRegicidalRage(id + 0x20000, 4.5f); + DawnOfAnAge1(id + 0x30000, 8.4f); + ProjectionOfTriumph1(id + 0x40000, 7.2f); + ProjectionOfTurmoil1(id + 0x50000, 7.2f); + DawnOfAnAge2(id + 0x60000, 8.4f); + ProjectionOfTriumph2(id + 0x70000, 8.3f); + ProjectionOfTurmoil2(id + 0x80000, 7.2f); + DawnOfAnAge3(id + 0x90000, 8.4f); + + // TODO: never seen stuff below + MultidirectionalDivideHalf(id + 0xA0000, 10); + SimpleState(id + 0xFF0000, 10, "???"); + } + + private void Actualize(uint id, float delay) + { + Cast(id, AID.Actualize, delay, 5, "Raidwide") + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void MultidirectionalDivideHalf(uint id, float delay) + { + Cast(id, AID.MultidirectionalDivide, delay, 5, "Cross") + .ActivateOnEnter() + .DeactivateOnExit(); + CastMulti(id + 0x10, [AID.ForwardHalfR, AID.ForwardHalfL, AID.BackwardHalfR, AID.BackwardHalfL], 3.1f, 8, "Criss-cross") // criss-cross resolves around the cast end + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .DeactivateOnExit() + .DeactivateOnExit(); + ComponentCondition(id + 0x20, 1.1f, comp => comp.NumCasts > 0, "Cleaves") + .DeactivateOnExit(); + } + + private void MultidirectionalDivideRegicidalRage(uint id, float delay) + { + Cast(id, AID.MultidirectionalDivide, delay, 5, "Cross") + .ActivateOnEnter() + .DeactivateOnExit(); + CastStart(id + 0x10, AID.RegicidalRage, 3.2f) + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); // tethers appear ~0.1s before cast starts + ComponentCondition(id + 0x11, 7.8f, comp => comp.NumCasts > 0, "Criss-cross") + .DeactivateOnExit() + .DeactivateOnExit(); + CastEnd(id + 0x12, 0.2f); + ComponentCondition(id + 0x13, 0.1f, comp => comp.NumCasts > 0, "Tankbuster tethers") + .DeactivateOnExit(); + } + + private void HalfFull(uint id, float delay) + { + CastMulti(id, [AID.HalfFullR, AID.HalfFullL], delay, 6) + .ActivateOnEnter(); + ComponentCondition(id + 2, 0.3f, comp => comp.NumCasts > 0, "Side cleave") + .DeactivateOnExit(); + } + + private void DawnOfAnAge(uint id, float delay) + { + Cast(id, AID.DawnOfAnAge, delay, 7, "Raidwide + small arena") + .OnEnter(() => Module.Arena.Bounds = Ex2ZoraalJa.SmallBounds) + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void DutysEdgeResolve(uint id, float delay) + { + CastEnd(id, 4.9f); + ComponentCondition(id + 1, 0.4f, comp => comp.NumCasts >= 1, "Line stack 1") + .SetHint(StateMachine.StateHint.Raidwide); + ComponentCondition(id + 2, 2.1f, comp => comp.NumCasts >= 2, "Line stack 2") + .SetHint(StateMachine.StateHint.Raidwide); + ComponentCondition(id + 3, 2.1f, comp => comp.NumCasts >= 3, "Line stack 3") + .SetHint(StateMachine.StateHint.Raidwide); + ComponentCondition(id + 4, 2.1f, comp => comp.NumCasts >= 4, "Line stack 4") + .DeactivateOnExit() + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void DawnOfAnAge1(uint id, float delay) + { + DawnOfAnAge(id, delay); + + Cast(id + 0x1000, AID.VollokSmall, 10.2f, 4); + Cast(id + 0x1010, AID.Sync, 5.4f, 5); + ComponentCondition(id + 0x1020, 0.9f, comp => comp.AOEs.Count > 0) + .ActivateOnEnter(); + CastStartMulti(id + 0x1030, [AID.HalfFullR, AID.HalfFullL], 2.3f); + ComponentCondition(id + 0x1031, 5.8f, comp => comp.NumCasts > 0, "Swords") + .ActivateOnEnter() + .DeactivateOnExit(); + CastEnd(id + 0x1032, 0.2f); + ComponentCondition(id + 0x1033, 0.3f, comp => comp.NumCasts > 0, "Side cleave") + .DeactivateOnExit(); + + Cast(id + 0x2000, AID.GreaterGateway, 4.9f, 4) + .ActivateOnEnter(); // envc happen ~0.9s after cast end + Cast(id + 0x2010, AID.BladeWarp, 4.2f, 4) + .ActivateOnEnter(); + Cast(id + 0x2020, AID.ForgedTrack, 4.2f, 4); + ComponentCondition(id + 0x2030, 8.2f, comp => comp.NumCasts > 0, "Lanes") // wide aoe happens ~0.2s later, knockback ~0.6s later + .ActivateOnEnter() // icons appear ~1.2s before lanes resolve + .ExecOnExit(comp => comp.Active = true); + + CastStart(id + 0x3000, AID.Actualize, 4.9f, "Cells") + .DeactivateOnExit() + .DeactivateOnExit() + .DeactivateOnExit(); // this resolves right as cast starts + CastEnd(id + 0x3001, 5, "Raidwide + normal arena") + .OnExit(() => Module.Arena.Bounds = Ex2ZoraalJa.NormalBounds) + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void DawnOfAnAge2(uint id, float delay) + { + DawnOfAnAge(id, delay); + ComponentCondition(id + 0x10, 4.9f, comp => comp.Active) + .ActivateOnEnter() + .DeactivateOnExit(); + + Cast(id + 0x1000, AID.DrumOfVollok, 5.3f, 7.4f) + .ActivateOnEnter() + .ActivateOnEnter(); + ComponentCondition(id + 0x1010, 0.6f, comp => comp.NumFinishedStacks > 0, "Enumeration") + .DeactivateOnExit() + .DeactivateOnExit(); + + Cast(id + 0x2000, AID.VollokLarge, 6.1f, 5) + .ActivateOnEnter(); + Cast(id + 0x2010, AID.Sync, 4.2f, 5); + CastStart(id + 0x2020, AID.AeroIII, 4.2f) + .ActivateOnEnter() // icons appear ~1.2s before cast start + .ExecOnEnter(comp => comp.Active = true); + ComponentCondition(id + 0x2021, 4.7f, comp => comp.NumCasts > 0, "Swords") + .DeactivateOnExit(); + ComponentCondition(id + 0x2022, 0.1f, comp => comp.NumCasts > 0, "Cells") + .DeactivateOnExit(); + CastEnd(id + 0x2023, 0.2f); + ComponentCondition(id + 0x2030, 1.2f, comp => comp.Voidzones.Count > 0) + .ActivateOnEnter(); + + CastMulti(id + 0x3000, [AID.ForwardHalfLongR, AID.ForwardHalfLongL, AID.BackwardHalfLongR, AID.BackwardHalfLongL], 5.2f, 9) + .ActivateOnEnter(); + ComponentCondition(id + 0x3010, 1.1f, comp => comp.NumCasts > 0, "Cleaves") + .DeactivateOnExit(); + + CastStart(id + 0x4000, AID.DutysEdge, 2.1f) + .ActivateOnEnter(); + DutysEdgeResolve(id + 0x4010, 4.9f); + + Cast(id + 0x5000, AID.BurningChains, 2.1f, 5, "Chains") + .ActivateOnEnter(); + Cast(id + 0x6000, AID.Actualize, 15.2f, 5, "Raidwide + normal arena") + .DeactivateOnExit() + .DeactivateOnExit() + .OnExit(() => Module.Arena.Bounds = Ex2ZoraalJa.NormalBounds) + .OnExit(() => Module.Arena.Center = Ex2ZoraalJa.NormalCenter) + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void DawnOfAnAge3(uint id, float delay) + { + DawnOfAnAge(id, delay); + + Cast(id + 0x1000, AID.VollokSmall, 10.2f, 4); + Cast(id + 0x1010, AID.Sync, 4.2f, 5); + ComponentCondition(id + 0x1020, 0.9f, comp => comp.AOEs.Count > 0) + .ActivateOnEnter(); + ComponentCondition(id + 0x1030, 8, comp => comp.NumCasts > 0, "Swords") + .ActivateOnEnter() // icons appear ~2.7s before swords resolve + .DeactivateOnExit(); + CastStart(id + 0x1040, AID.DutysEdge, 3.3f, "Cells") // player cells resolve together with cast start + .ExecOnExit(comp => comp.Active = true) + .ActivateOnEnter() + .DeactivateOnExit(); + DutysEdgeResolve(id + 0x1050, 4.9f); + + Cast(id + 0x2000, AID.GreaterGateway, 5.1f, 4) + .ActivateOnEnter(); // envc happen ~0.9s after cast end + Cast(id + 0x2010, AID.BladeWarp, 4.2f, 4) + .ActivateOnEnter(); + Cast(id + 0x2020, AID.ForgedTrack, 4.2f, 4); + ComponentCondition(id + 0x2030, 8.2f, comp => comp.NumCasts > 0, "Lanes") // wide aoe happens ~0.2s later, knockback ~0.6s later + .ActivateOnEnter() // icons appear ~1.2s before lanes resolve + .ExecOnExit(comp => comp.Active = true); + + CastStart(id + 0x3000, AID.Actualize, 4.9f, "Cells") + .DeactivateOnExit() + .DeactivateOnExit() + .DeactivateOnExit(); // this resolves right as cast starts + CastEnd(id + 0x3001, 5, "Raidwide + normal arena") + .OnExit(() => Module.Arena.Bounds = Ex2ZoraalJa.NormalBounds) + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void ProjectionOfTriumph1(uint id, float delay) + { + Cast(id, AID.ProjectionOfTriumph, delay, 5) + .ActivateOnEnter(); + Cast(id + 0x10, AID.ProjectionOfTriumph, 5.2f, 5, "Swords 1"); // first set of circles/donuts happen right before cast ends + ComponentCondition(id + 0x20, 4.9f, comp => comp.NumCasts >= 16, "Swords 2"); + CastStartMulti(id + 0x30, [AID.ForwardHalfR, AID.ForwardHalfL, AID.BackwardHalfR, AID.BackwardHalfL], 4.5f); + ComponentCondition(id + 0x31, 0.6f, comp => comp.NumCasts >= 32, "Swords 3") + .ActivateOnEnter(); + ComponentCondition(id + 0x40, 5.0f, comp => comp.NumCasts >= 48, "Swords 4"); + CastEnd(id + 0x41, 2.4f); + ComponentCondition(id + 0x42, 1.1f, comp => comp.NumCasts > 0, "Cleaves") + .DeactivateOnExit(); + ComponentCondition(id + 0x50, 1.5f, comp => comp.NumCasts >= 56, "Swords 5"); + CastStart(id + 0x60, AID.Actualize, 3.6f); + ComponentCondition(id + 0x61, 1.5f, comp => comp.NumCasts >= 64, "Swords 6") + .DeactivateOnExit(); + CastEnd(id + 0x62, 3.5f, "Raidwide") + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void ProjectionOfTriumph2(uint id, float delay) + { + Cast(id, AID.ProjectionOfTriumph, delay, 5) + .ActivateOnEnter(); + Cast(id + 0x10, AID.ProjectionOfTriumph, 5.2f, 5, "Swords 1"); // first set of circles/donuts happen right before cast ends + CastStartMulti(id + 0x20, [AID.HalfCircuitCircle, AID.HalfCircuitDonut], 4.9f, "Swords 2"); // second set of circles/donuts happen together with cast start + ComponentCondition(id + 0x30, 5.1f, comp => comp.NumCasts >= 32, "Swords 3") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + CastEnd(id + 0x40, 1.9f, "In/out") + .DeactivateOnExit() + .DeactivateOnExit(); + ComponentCondition(id + 0x41, 0.3f, comp => comp.NumCasts > 0, "Side cleave") + .DeactivateOnExit(); + ComponentCondition(id + 0x50, 2.8f, comp => comp.NumCasts >= 48, "Swords 4"); + ComponentCondition(id + 0x60, 5, comp => comp.NumCasts >= 56, "Swords 5"); + CastStart(id + 0x61, AID.RegicidalRage, 0.1f) + .ActivateOnEnter(); + ComponentCondition(id + 0x70, 5.0f, comp => comp.NumCasts >= 64, "Swords 6") + .DeactivateOnExit(); + CastEnd(id + 0x71, 3); + ComponentCondition(id + 0x72, 0.1f, comp => comp.NumCasts > 0, "Tankbuster tethers") + .DeactivateOnExit(); + } + + private State BitterWhirlwind(uint id, float delay) + { + Cast(id, AID.BitterWhirlwind, delay, 5, "Tankbuster 1") + .ActivateOnEnter(); + ComponentCondition(id + 0x10, 3.3f, comp => comp.NumCasts >= 2, "Tankbuster 2"); + return ComponentCondition(id + 0x11, 3.1f, comp => comp.NumCasts >= 3, "Tankbuster 3") + .DeactivateOnExit(); + } + + private void ProjectionOfTurmoil1(uint id, float delay) + { + Cast(id, AID.ProjectionOfTurmoil, delay, 5, "Moving line with stacks") + .ActivateOnEnter(); + BitterWhirlwind(id + 0x100, 45.3f) + .DeactivateOnExit(); + } + + private void ProjectionOfTurmoil2(uint id, float delay) + { + Cast(id, AID.ProjectionOfTurmoil, delay, 5, "Moving line with stacks") + .ActivateOnEnter(); + HalfFull(id + 0x100, 16.6f); + HalfFull(id + 0x200, 2.8f); + HalfFull(id + 0x300, 4.1f); + BitterWhirlwind(id + 0x400, 3.9f) + .DeactivateOnExit(); + } +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ForgedTrack.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ForgedTrack.cs new file mode 100644 index 0000000000..78794e565a --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ForgedTrack.cs @@ -0,0 +1,222 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +// there are only 4 possible patterns for this mechanic, here are the findings: +// - wide/knockback are always NE/NW platforms, narrow are always SE/SW platforms +// - NE/NW are always mirrored (looking from platform to the main one, wide is on the left side on one of them and on the right side on the other); which one is which is random +// - SE/SW have two patterns (either inner or outer lanes change left/right side); which one is which is random +class ForgedTrack(BossModule module) : Components.GenericAOEs(module) +{ + public enum Pattern { Unknown, A, B } // B is always inverted + + public readonly List NarrowAOEs = []; + public readonly List WideAOEs = []; + public readonly List KnockbackAOEs = []; + private Pattern _patternN; + private Pattern _patternS; + + private static readonly AOEShapeRect _shape = new(10, 2.5f, 10); + private static readonly AOEShapeRect _shapeWide = new(10, 7.5f, 10); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => NarrowAOEs.Concat(WideAOEs).Concat(KnockbackAOEs); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID != AID.ForgedTrackPreview) + return; + + var casterOffset = caster.Position - Module.Center; + var rightDir = spell.Rotation.ToDirection().OrthoR(); + var laneOffset = casterOffset.Dot(rightDir); + var laneRight = laneOffset > 0; + var laneInner = laneOffset is > -5 and < 5; + var west = casterOffset.X > 0; + if (casterOffset.Z < 0) + { + // N => wide/knockback + if (_patternN == Pattern.Unknown) + return; + var rightIsWide = west == (_patternN == Pattern.A); + var adjustedLaneOffset = laneOffset + (laneRight ? -5 : 5); + if (rightIsWide == laneRight) + { + // wide + WideAOEs.Add(new(_shapeWide, Module.Center + rightDir * adjustedLaneOffset, spell.Rotation, spell.NPCFinishAt.AddSeconds(1.4f))); + } + else + { + // knockback + KnockbackAOEs.Add(new(_shape, Module.Center + rightDir * adjustedLaneOffset, spell.Rotation, spell.NPCFinishAt.AddSeconds(1.9f))); + } + } + else + { + // S => narrow + if (_patternS == Pattern.Unknown) + return; + var crossInner = west == (_patternS == Pattern.A); + var cross = crossInner == laneInner; + var adjustedRight = cross ^ laneRight; + var adjustedLaneOffset = (laneInner ? 7.5f : 2.5f) * (adjustedRight ? 1 : -1); + NarrowAOEs.Add(new(_shape, Module.Center + rightDir * adjustedLaneOffset, spell.Rotation, spell.NPCFinishAt.AddSeconds(1.3f))); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + switch ((AID)spell.Action.ID) + { + case AID.ForgedTrackAOE: + ++NumCasts; + NarrowAOEs.Clear(); + break; + case AID.FieryEdgeAOECenter: + if (WideAOEs.Count == 0) + Module.ReportError(this, "Unexpected wide aoe"); + WideAOEs.Clear(); + break; + case AID.StormyEdgeAOE: + if (KnockbackAOEs.Count == 0) + Module.ReportError(this, "Unexpected knockback"); + KnockbackAOEs.Clear(); + break; + } + } + + public override void OnEventEnvControl(byte index, uint state) + { + switch (index) + { + case 2: + AssignPattern(ref _patternS, 0x00800040, 0x02000100, state); + break; + case 3: + AssignPattern(ref _patternS, 0x02000100, 0x00800040, state); + break; + case 5: + AssignPattern(ref _patternN, 0x00020001, 0x00200010, state); + break; + case 8: + AssignPattern(ref _patternN, 0x00200010, 0x00020001, state); + break; + } + } + + private void AssignPattern(ref Pattern field, uint stateA, uint stateB, uint state) + { + if (state == 0x00080004) + return; // end + // we have two envcontrols that are always mirrored, don't know which is which + if (state != stateA && state != stateB) + { + Module.ReportError(this, $"Unknown pattern: {state:X8}, expected {stateA:X8} or {stateB:X8}"); + return; + } + var value = state == stateA ? Pattern.A : Pattern.B; + if (field != Pattern.Unknown && field != value) + Module.ReportError(this, $"Inconsistent pattern assignments: {field} vs {value}"); + field = value; + } + + //private void Print() + //{ + // var forgedDir = _forged[0].W.Radians(); + // if (!forgedDir.AlmostEqual(_forged[1].W.Radians(), 0.1f)) + // { + // Module.ReportError(this, $"Forged direction mismatch: {forgedDir} vs {_forged[1].W.Radians()}"); + // return; + // } + + // List srcs = [.. _initial.Where(x => x.W.Radians().AlmostEqual(forgedDir, 0.1f))]; + // if (srcs.Count != 2) + // { + // Module.ReportError(this, $"{srcs.Count} sources for {forgedDir}"); + // return; + // } + + // var bigDir = _big.W.Radians(); + // var bigSrc = _initial.FirstOrDefault(x => x.W.Radians().AlmostEqual(bigDir, 0.1f)); + // if (bigSrc == default) + // { + // Module.ReportError(this, $"Big source not found for {bigDir}"); + // return; + // } + + // var envS = (_envc[1], _envc[2]) switch + // { + // (0x00800040, 0x02000100) => "A", // -135 => cross-inner, +135 => cross-outer + // (0x02000100, 0x00800040) => "B", // inverse + // _ => $"{_envc[1]:X8}/{_envc[2]:X8}" + // }; + // var envN = (_envc[0], _envc[3]) switch + // { + // (0x00020001, 0x00200010) => "A", // -45 => R=Wide, +45 => R=KB + // (0x00200010, 0x00020001) => "B", + // _ => $"{_envc[0]:X8}/{_envc[3]:X8}" + // }; + + // var orthoBig = bigDir.ToDirection().OrthoR(); + // var srcBig = (new WPos(bigSrc.XZ()) - Module.Center).Dot(orthoBig); + // var resBig = (new WPos(_big.XZ()) - Module.Center).Dot(orthoBig); + // var ortho = forgedDir.ToDirection().OrthoR(); + // var src1 = (new WPos(srcs[0].XZ()) - Module.Center).Dot(ortho); + // var src2 = (new WPos(srcs[1].XZ()) - Module.Center).Dot(ortho); + // var res1 = (new WPos(_forged[0].XZ()) - Module.Center).Dot(ortho); + // var res2 = (new WPos(_forged[1].XZ()) - Module.Center).Dot(ortho); + + // static (bool left, bool inner) classify(float x) => (x < 0, Math.Abs(x) < 5); + // var s1 = classify(src1); + // var s2 = classify(src2); + // var r1 = classify(res1); + // var r2 = classify(res2); + // var sb = classify(srcBig); + // var rb = classify(resBig); + // if (!rb.inner || (sb.inner ? sb.left == rb.left : sb.left != rb.left)) + // { + // Module.ReportError(this, $"Unexpected big offset"); + // return; + // } + // if (s1.inner == s2.inner) + // { + // Module.ReportError(this, $"{envS}/{envN}, {forgedDir}deg -> Symmetrical {(s1.inner ? "inner" : "outer")}, {bigDir}deg -> {(sb.left ? "L" : "R")}={(_split ? "Wide" : "KB")}"); + // return; + // } + // if (r1.inner == r2.inner) + // { + // Module.ReportError(this, $"Weird: {src1}-{src2} > {res1}-{res2}"); + // return; + // } + + // // make order: inner/outer -> outer/inner + // if (!s1.inner) + // (s1, s2) = (s2, s1); + // if (r1.inner) + // (r1, r2) = (r2, r1); + + // var innerCross = s1.left != r1.left; + // var outerCross = s2.left != r2.left; + // Module.ReportError(this, $"{envS}/{envN}, {forgedDir}deg -> Cross {(innerCross ? (outerCross ? "both" : "inner") : (outerCross ? "outer" : "none"))}, {bigDir}deg -> {(sb.left ? "L" : "R")}={(_split ? "Wide" : "KB")}"); + + // Array.Fill(_envc, 0u); + // _initial.Clear(); + // _forged.Clear(); + // _big = default; + //} +} + +class ForgedTrackKnockback(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.StormyEdgeKnockback)) +{ + private readonly ForgedTrack? _main = module.FindComponent(); + + private static readonly AOEShapeRect _shape = new(20, 10); + + public override IEnumerable Sources(int slot, Actor actor) + { + if (_main == null) + yield break; + foreach (var aoe in _main.KnockbackAOEs) + { + yield return new(aoe.Origin, 7, aoe.Activation, _shape, aoe.Rotation + 90.Degrees(), Kind.DirForward); + yield return new(aoe.Origin, 7, aoe.Activation, _shape, aoe.Rotation - 90.Degrees(), Kind.DirForward); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ForwardBackwardHalf.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ForwardBackwardHalf.cs new file mode 100644 index 0000000000..1096d8fb51 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ForwardBackwardHalf.cs @@ -0,0 +1,47 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +class ForwardBackwardHalf(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.HalfFullShortAOE)) +{ + private readonly List _aoes = []; + + private static readonly AOEShapeRect _shapeEdge = new(50, 30, 10); + private static readonly AOEShapeRect _shapeSide = new(60, 60); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + var (relevant, front, left) = (AID)spell.Action.ID switch + { + AID.ForwardHalfR or AID.ForwardHalfLongR => (true, true, false), + AID.ForwardHalfL or AID.ForwardHalfLongL => (true, true, true), + AID.BackwardHalfR or AID.BackwardHalfLongR => (true, false, false), + AID.BackwardHalfL or AID.BackwardHalfLongL => (true, false, true), + _ => default + }; + if (!relevant) + return; + + var cleaveDir = spell.Rotation + (front ? 180 : 0).Degrees(); + _aoes.Add(new(_shapeEdge, caster.Position, cleaveDir, spell.FinishAt)); + _aoes.Add(new(_shapeSide, caster.Position, cleaveDir + (left ? 90 : -90).Degrees(), spell.FinishAt)); + } +} + +class HalfFull(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.HalfFullLongAOE)) +{ + private readonly List _aoes = []; + + private static readonly AOEShapeRect _shapeSide = new(60, 60); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.HalfFullR or AID.HalfFullL) + { + var cleaveDir = spell.Rotation + ((AID)spell.Action.ID == AID.HalfFullL ? 90 : -90).Degrees(); + _aoes.Add(new(_shapeSide, caster.Position, cleaveDir, spell.FinishAt)); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ProjectionOfTriumph.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ProjectionOfTriumph.cs new file mode 100644 index 0000000000..c2041b9617 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ProjectionOfTriumph.cs @@ -0,0 +1,66 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +class ProjectionOfTriumph(BossModule module) : Components.GenericAOEs(module) +{ + private record struct Line(WDir Direction, AOEShape Shape); + + private readonly List _lines = []; + private DateTime _nextActivation; + + private readonly static AOEShapeCircle _shapeCircle = new(4); + private readonly static AOEShapeDonut _shapeDonut = new(2, 8); // TODO: verify inner radius + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var nextOrder = NextOrder(); + for (int i = 0; i < _lines.Count; ++i) + { + var order = i >= 2 ? nextOrder - 2 : nextOrder; + if (order is >= 0 and < 4) + { + var line = _lines[i]; + var lineCenter = Module.Center + (-15 + 10 * order) * line.Direction; + var ortho = line.Direction.OrthoL(); + for (int j = -15; j <= 15; j += 10) + { + yield return new(line.Shape, lineCenter + j * ortho, default, _nextActivation); + } + } + } + } + + public override void OnActorCreated(Actor actor) + { + AOEShape? shape = (OID)actor.OID switch + { + OID.ProjectionOfTriumphCircle => _shapeCircle, + OID.ProjectionOfTriumphDonut => _shapeDonut, + _ => null + }; + if (shape != null) + { + _lines.Add(new(actor.Rotation.ToDirection(), shape)); + _nextActivation = WorldState.FutureTime(9); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.SiegeOfVollok or AID.WallsOfVollok) + { + ++NumCasts; + _nextActivation = WorldState.FutureTime(5); + } + } + + public int NextOrder() => NumCasts switch + { + < 8 => 0, + < 16 => 1, + < 32 => 2, + < 48 => 3, + < 56 => 4, + < 64 => 5, + _ => 6 + }; +} diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ProjectionOfTurmoil.cs b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ProjectionOfTurmoil.cs new file mode 100644 index 0000000000..bcd654e1b8 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Extreme/Ex2ZoraalJa/ProjectionOfTurmoil.cs @@ -0,0 +1,37 @@ +namespace BossMod.Dawntrail.Extreme.Ex2ZoraalJa; + +// TODO: consider improving this somehow? too many ways to resolve... +class ProjectionOfTurmoil(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.MightOfVollok)) +{ + private readonly IReadOnlyList _line = module.Enemies(OID.ProjectionOfTurmoil); + private BitMask _targets; + + public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot, Actor player, ref uint customColor) => _targets[playerSlot] ? PlayerPriority.Interesting : PlayerPriority.Normal; + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var slot in _targets.SetBits()) + { + var actor = Raid[slot]; + if (actor != null) + Arena.AddCircle(actor.Position, 8, ArenaColor.Safe); + } + foreach (var l in _line) + { + var off = new WDir(28.28427f - Math.Abs(l.Position.Z - Module.Center.Z), 0); + Arena.AddLine(l.Position - off, l.Position + off, ArenaColor.Danger); + } + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.Projection) + _targets.Set(Raid.FindSlot(actor.InstanceID)); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.Projection) + _targets.Clear(Raid.FindSlot(actor.InstanceID)); + } +}