diff --git a/BossMod/Components/GenericAOEs.cs b/BossMod/Components/GenericAOEs.cs index d9795a0ca8..7ebbe7a2bf 100644 --- a/BossMod/Components/GenericAOEs.cs +++ b/BossMod/Components/GenericAOEs.cs @@ -33,11 +33,11 @@ public override void DrawArenaBackground(int pcSlot, Actor pc) } // self-targeted aoe that happens at the end of the cast -public class SelfTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, int maxCasts = int.MaxValue) : GenericAOEs(module, aid) +public class SelfTargetedAOEs(BossModule module, ActionID aid, AOEShape shape, int maxCasts = int.MaxValue, uint color = ArenaColor.AOE) : GenericAOEs(module, aid) { public AOEShape Shape { get; init; } = shape; public int MaxCasts = maxCasts; // used for staggered aoes, when showing all active would be pointless - public uint Color = ArenaColor.AOE; // can be customized if needed + public uint Color = color; public bool Risky = true; // can be customized if needed public readonly List Casters = []; diff --git a/BossMod/Debug/DebugGraphics.cs b/BossMod/Debug/DebugGraphics.cs index cf3dba333a..35f07f5811 100644 --- a/BossMod/Debug/DebugGraphics.cs +++ b/BossMod/Debug/DebugGraphics.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using System.Text; namespace BossMod; @@ -18,6 +17,7 @@ private class WatchedRenderObject private Vector2 _overlayCenter = new(100, 100); private Vector2 _overlayStep = new(2, 2); private Vector2 _overlayMaxOffset = new(20, 20); + private Angle _overlayRotation = new(0); public unsafe void DrawSceneTree() { @@ -285,17 +285,27 @@ public void DrawOverlay() ImGui.DragFloat2("Step", ref _overlayStep); ImGui.DragFloat2("Max offset", ref _overlayMaxOffset); - int mx = (int)(_overlayMaxOffset.X / _overlayStep.X); - int mz = (int)(_overlayMaxOffset.Y / _overlayStep.Y); - float y = Service.ClientState.LocalPlayer.Position.Y; + var rotationDegrees = _overlayRotation.Deg; + ImGui.SliderFloat("Rotation (Degrees)", ref rotationDegrees, -180, 180, "%.3f"); + ImGui.SameLine(); + ImGui.InputFloat("##RotationInput", ref rotationDegrees, 0.1f, 1, "%.3f"); + _overlayRotation = new Angle(rotationDegrees * Angle.DegToRad); + + var mx = (int)(_overlayMaxOffset.X / _overlayStep.X); + var mz = (int)(_overlayMaxOffset.Y / _overlayStep.Y); + var y = Service.ClientState.LocalPlayer.Position.Y; + + var rotationMatrix = Matrix3x2.CreateRotation(-_overlayRotation.Rad); + Vector2 TransformPoint(Vector2 point) => Vector2.Transform(point - _overlayCenter, rotationMatrix) + _overlayCenter; + if (_overlayCircle) { var center = new Vector3(_overlayCenter.X, y, _overlayCenter.Y); - for (int ir = 0; ir <= mx; ++ir) + for (var ir = 0; ir <= mx; ++ir) { Camera.Instance.DrawWorldCircle(center, ir * _overlayStep.X, ArenaColor.PC); } - for (int ia = 0; ia < 8; ++ia) + for (var ia = 0; ia < 8; ++ia) { var offset = ((ia * 22.5f.Degrees()).ToDirection() * _overlayMaxOffset.X).ToVec3(); Camera.Instance.DrawWorldLine(center - offset, center + offset, ArenaColor.PC); @@ -303,15 +313,19 @@ public void DrawOverlay() } else { - for (int ix = -mx; ix <= mx; ++ix) + for (var ix = -mx; ix <= mx; ++ix) { var x = _overlayCenter.X + ix * _overlayStep.X; - Camera.Instance.DrawWorldLine(new(x, y, _overlayCenter.Y - _overlayMaxOffset.Y), new(x, y, _overlayCenter.Y + _overlayMaxOffset.Y), ArenaColor.PC); + var start = TransformPoint(new Vector2(x, _overlayCenter.Y - _overlayMaxOffset.Y)); + var end = TransformPoint(new Vector2(x, _overlayCenter.Y + _overlayMaxOffset.Y)); + Camera.Instance.DrawWorldLine(new(start.X, y, start.Y), new(end.X, y, end.Y), ArenaColor.PC); } - for (int iz = -mz; iz <= mz; ++iz) + for (var iz = -mz; iz <= mz; ++iz) { var z = _overlayCenter.Y + iz * _overlayStep.Y; - Camera.Instance.DrawWorldLine(new(_overlayCenter.X - _overlayMaxOffset.X, y, z), new(_overlayCenter.X + _overlayMaxOffset.X, y, z), ArenaColor.PC); + var start = TransformPoint(new Vector2(_overlayCenter.X - _overlayMaxOffset.X, z)); + var end = TransformPoint(new Vector2(_overlayCenter.X + _overlayMaxOffset.X, z)); + Camera.Instance.DrawWorldLine(new(start.X, y, start.Y), new(end.X, y, end.Y), ArenaColor.PC); } } } diff --git a/BossMod/Modules/Dawntrail/Quests/MSQ/TakingAStand.cs b/BossMod/Modules/Dawntrail/Quests/MSQ/TakingAStand.cs index 392f778613..0025037b70 100644 --- a/BossMod/Modules/Dawntrail/Quests/MSQ/TakingAStand.cs +++ b/BossMod/Modules/Dawntrail/Quests/MSQ/TakingAStand.cs @@ -210,12 +210,21 @@ class Roar2(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSp class Fireflood(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.Fireflood), 18); class TuraliStoneIII(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.TuraliStoneIII), 4); class TuraliQuake(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.TuraliQuake), 9, maxCasts: 5); +class StayInBounds(BossModule module) : BossComponent(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (!Module.InBounds(actor.Position)) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 3)); + } +} class TakingAStandStates : StateMachineBuilder { public TakingAStandStates(BossModule module) : base(module) { TrivialPhase() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Dawntrail/Quests/MSQ/TheFeatOfBrotherhood.cs b/BossMod/Modules/Dawntrail/Quests/MSQ/TheFeatOfBrotherhood.cs new file mode 100644 index 0000000000..3441173362 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Quests/MSQ/TheFeatOfBrotherhood.cs @@ -0,0 +1,265 @@ +namespace BossMod.Dawntrail.Quest.MSQ.TheFeatOfBrotherhood; + +public enum OID : uint +{ + Boss = 0x4206, // R5.0 + WukLamat = 0x4209, // R1.0 + EphemeralSword = 0x4228, // R2.5 + BallOfFire = 0x44B6, // R1.0-2.5 + OathOfFire = 0x4229, // R2.500 + Helper = 0x233C +} + +public enum AID : uint +{ + AutoAttack = 870, // Boss->tank, no cast, single-target + Teleport = 37199, // Boss->location, no cast, single-target + + RoaringStarVisual = 39304, // Boss->self, 8.0s cast, single-target + RoaringStarRaidwide = 39305, // Helper->self, 8.0s cast, range 40 circle + RoaringStarRect = 39302, // BallOfFire->self, 6.0s cast, range 50 width 10 rect + + CoiledStrikeVisual1 = 37186, // Boss->self, 5.0+1,0s cast, single-target + CoiledStrikeVisual2 = 37187, // Boss->self, 5.0+1,0s cast, single-target + CoiledStrike = 37188, // Helper->self, 6.0s cast, range 30 150-degree cone + + CelestialFlameVisual = 37175, // Boss->self, 4.0s cast, single-target + CelestialFlame = 37206, // EphemeralSword->self, no cast, range 40 circle + SublimeHeat = 37176, // EphemeralSword->self, 5.0s cast, range 10 circle + Burn = 39299, // Helper->self, 5.0s cast, range 46 width 5 rect + + FallenStar = 37205, // Helper->everyone, 5.0s cast, range 6 circle, spread + + FirstLight = 39298, // Helper->location, 6.0s cast, range 6 circle + SteelfoldStrikeVisual1 = 37182, // Boss->location, 5.0+1,0s cast, single-target + SteelfoldStrikeVisual2 = 39300, // Boss->location, no cast, single-target + SteelfoldStrikeVisual3 = 39532, // Helper->self, 6.0s cast, range 30 width 8 cross + SteelfoldStrike = 37183, // Helper->self, 6.0s cast, range 30 width 8 cross + + LayOfTheSunA1 = 37207, // Boss->WukLamat, 4.9+3,1s cast, range 6 circle + LayOfTheSunA2 = 37208, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunA3 = 37209, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunA4 = 37210, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunA5 = 37211, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunA6 = 37212, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunA7 = 37213, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunA8 = 37214, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunA9 = 37215, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunB1 = 40065, // Boss->WukLamat, 4.9+3,1s cast, range 6 circle + LayOfTheSunB2 = 40066, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunB3 = 40067, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunB4 = 40068, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunB5 = 40069, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunB6 = 40070, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunB7 = 40071, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunB8 = 40072, // Helper->WukLamat, no cast, range 6 circle + LayOfTheSunB9 = 40073, // Helper->WukLamat, no cast, range 6 circle + + OathbindVisual = 37216, // Boss->self, 11.0s cast, single-target + Oathbind = 40064, // Helper->Urianger/WukLamat/Alphinaud/Thancred, no cast, single-target + OathbindPull = 37217, // Helper->Urianger/WukLamat/Alphinaud/Thancred, no cast, single-target, pull 10 between centers + Oathbind2 = 39810, // Helper->self, no cast, single-target + NobleTrail = 37218, // Boss->location, 50.0s cast, width 20 rect charge + + DualPyresVisual1 = 37177, // Boss->self, 7.0s cast, single-target + DualPyresVisual2 = 37178, // Boss->self, no cast, single-target + DualPyresVisual3 = 37181, // Boss->self, 7.0s cast, single-target + DualPyresVisual4 = 37309, // Boss->self, no cast, single-target + DualPyres1 = 37310, // Helper->self, 8.0s cast, range 30 180-degree cone + DualPyres2 = 37311, // Helper->self, 10.5s cast, range 30 180-degree cone + DualPyres3 = 37179, // Helper->self, 8.0s cast, range 30 180-degree cone + DualPyres4 = 37180, // Helper->self, 10.5s cast, range 30 180-degree cone + + InnerWakeVisual = 37200, // Boss->self, 2.5+2,5s cast, single-target + InnerWake = 37201, // Helper->self, 5.0s cast, range 10 circle + OuterWakeVisual = 37202, // Boss->self, 2.5+2,5s cast, single-target + OuterWake = 37203, // Helper->self, 5.0s cast, range 6-40 donut + + BattleBreaker = 37192, // Boss->self, 5.0s cast, range 40 width 40 rect, raidwide, knockback 10 forward + SagaOfDawnAndDutyVisual = 37193, // Boss->location, no cast, ??? + SagaOfDawnAndDuty = 37196, // Helper->self, no cast, range 60 circle + Shockwave = 39711, // Helper->self, no cast, range 60 circle + HeavyImpact = 37197, // Helper->self, no cast, range 60 circle + HeartOfTural = 37195, // WukLamat->self, 8.0s cast, range 20 width 40 rect +} + +class DualPyresSteelfoldStrike(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + + private static readonly AOEShapeCone cone = new(30, 90.Degrees()); + private static readonly AOEShapeCross cross = new(30, 4); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (_aoes.Count > 0) + yield return new(_aoes[0].Shape, _aoes[0].Origin, _aoes[0].Rotation, _aoes[0].Activation, ArenaColor.Danger); + if (_aoes.Count > 1) + yield return new(_aoes[1].Shape, _aoes[1].Origin, _aoes[1].Rotation, _aoes[1].Activation, Risky: false); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.DualPyres1 or AID.DualPyres2 or AID.DualPyres3 or AID.DualPyres4) + { + _aoes.Add(new(cone, caster.Position, spell.Rotation, spell.NPCFinishAt)); + _aoes.SortBy(x => x.Activation); + } + else if ((AID)spell.Action.ID == AID.SteelfoldStrike) + _aoes.Add(new(cross, caster.Position, spell.Rotation, spell.NPCFinishAt)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.DualPyres1 or AID.DualPyres2 or AID.DualPyres3 or AID.DualPyres4 or AID.SteelfoldStrike) + { + if (_aoes.Count > 0) + _aoes.RemoveAt(0); + } + } +} + +class RoaringStarRect(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + private static readonly AOEShapeRect rect = new(50, 5); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnActorPlayActionTimelineEvent(Actor actor, ushort id) + { + if (id == 0x1E46) + _aoes.Add(new(rect, actor.Position, actor.Rotation, Module.WorldState.FutureTime(8.5f))); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.RoaringStarRect) + _aoes.Clear(); + } +} + +class SublimeHeat(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + private static readonly AOEShapeCircle circle = new(10); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes.Take(6); + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.CelestialFlame) + _aoes.Add(new(circle, caster.Position, default, Module.WorldState.FutureTime(7.5f))); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if (_aoes.Count > 0 && (AID)spell.Action.ID == AID.SublimeHeat) + _aoes.RemoveAt(0); + } +} + +class NobleTrail(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance _aoe; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + if (_aoe != default && Module.PrimaryActor.IsTargetable) + yield return new(_aoe.Shape, _aoe.Origin, _aoe.Rotation, _aoe.Activation); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.NobleTrail) + { + var dir = spell.LocXZ - caster.Position; + _aoe = new(new AOEShapeRect(dir.Length(), 10), caster.Position, Angle.FromDirection(dir), spell.NPCFinishAt); + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.NobleTrail) + _aoe = default; + } +} + +class LayOfTheSun(BossModule module) : Components.UniformStackSpread(module, 6, 0, 8) +{ + private int numCasts; + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action.ID is >= 37207 and <= 37215 or >= 40065 and <= 40073) + { + ++numCasts; + if (numCasts == 9) + { + Stacks.Clear(); + numCasts = 0; + } + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.LayOfTheSunA1 or AID.LayOfTheSunB1) + AddStack(WorldState.Actors.Find(spell.TargetID)!); + } +} + +class RoaringStar(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.RoaringStarRaidwide)); +class CoiledStrike(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CoiledStrike), new AOEShapeCone(30, 75.Degrees())); +class Burn(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Burn), new AOEShapeRect(46, 2.5f), 8); +class FallenStar(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.FallenStar), 6); +class FirstLight(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.FirstLight), 6); +class InnerWake(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.InnerWake), new AOEShapeCircle(10)); +class OuterWake(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.OuterWake), new AOEShapeDonut(6, 40)); +class BattleBreaker(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.BattleBreaker)); +class HeartOfTuralRaidwides(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.HeartOfTural), "Raidwides x7"); +class HeartOfTural(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HeartOfTural), new AOEShapeRect(20, 20, InvertForbiddenZone: true), color: ArenaColor.SafeFromAOE) +{ + private const string hint = "Wait in safe area!"; + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (ActiveAOEs(slot, actor).Any(c => c.Risky && !c.Check(actor.Position))) + hints.Add(hint); + else if (ActiveAOEs(slot, actor).Any(c => c.Risky && c.Check(actor.Position))) + hints.Add(hint, false); + } +} + +class TheFeatOfBrotherhoodStates : StateMachineBuilder +{ + public TheFeatOfBrotherhoodStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70444, NameID = 12734)] +public class TheFeatOfBrotherhood(WorldState ws, Actor primary) : BossModule(ws, primary, new(353.5f, 596.4f), new ArenaBoundsSquare(19.75f, -77.5f.Degrees())) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actor(PrimaryActor, ArenaColor.Enemy); + foreach (var s in Enemies(OID.OathOfFire)) + Arena.Actor(s, ArenaColor.Enemy); + } +}