From 6d9145524e6b4226dd0a253d68c3a80b8b8bccb2 Mon Sep 17 00:00:00 2001 From: sourpuh <150582765+sourpuh@users.noreply.github.com> Date: Fri, 3 May 2024 11:37:38 -0700 Subject: [PATCH 1/3] Add WIP Lyon duel 6 module --- .../Foray/Duel/Duel6Lyon/Duel6Lyon.cs | 42 +++++ .../Foray/Duel/Duel6Lyon/Duel6LyonEnums.cs | 47 +++++ .../Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs | 178 ++++++++++++++++++ .../Foray/Duel/Duel6Lyon/FlamesMeet.cs | 33 ++++ .../Duel6Lyon/TasteOfBloodAndDuelOrDie.cs | 54 ++++++ 5 files changed, 354 insertions(+) create mode 100644 BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs create mode 100644 BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonEnums.cs create mode 100644 BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs create mode 100644 BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/FlamesMeet.cs create mode 100644 BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/TasteOfBloodAndDuelOrDie.cs diff --git a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs new file mode 100644 index 000000000..5f2b4ebf4 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs @@ -0,0 +1,42 @@ +namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon; + +class Duel6LyonStates : StateMachineBuilder +{ + public Duel6LyonStates(BossModule module) : base(module) + { + TrivialPhase() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } +} + +[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "SourP", GroupType = BossModuleInfo.GroupType.BozjaDuel, GroupID = 778, NameID = 31)] +public class Duel6Lyon(WorldState ws, Actor primary) : BossModule(ws, primary, new ArenaBoundsCircle(new(50f, -410f), 20)) +{ + protected override void DrawEnemies(int pcSlot, Actor pc) + { + var tasteOfBlood = FindComponent(); + if (tasteOfBlood?.Casters.Count > 0) + { + foreach (var caster in tasteOfBlood.Casters) + { + bool isDueler = tasteOfBlood.Duelers.Contains(caster); + Arena.Actor(caster, isDueler ? ArenaColor.Danger : ArenaColor.Enemy, true); + } + } + else + { + base.DrawEnemies(pcSlot, pc); + } + } +} diff --git a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonEnums.cs b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonEnums.cs new file mode 100644 index 000000000..c5ea71f47 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonEnums.cs @@ -0,0 +1,47 @@ +namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon; + +public enum OID : uint +{ + Boss = 0x31C1, + Helper = 0x233C, + VermillionFlame = 0x2E8F, +} + +public enum AID : uint +{ + AutoAttack = 6497, // Boss->player, no cast, single-target + WildfiresFury = 0x5D39, // Damage + HarnessFire = 0x5D38, // Boss->self, 3,0s cast, single-target + HeartOfNature = 0x5D24, // Boss->self, 3,0s cast, range 80 circle + CagedHeartOfNature = 0x5D1D, // Boss->self, 3,0s cast, range 10 circle + NaturesPulse1 = 0x5D25, // Helper->self, 4,0s cast, range 10 circle + NaturesPulse2 = 0x5D26, // Helper->self, 5,5s cast, range 10-20 donut + NaturesPulse3 = 0x5D27, // Helper->self, 7,0s cast, range 20-30 donut + TasteOfBlood = 0x5D23, // Boss->self, 4,0s cast, range 40 180-degree cone + SoulAflame = 0x5D2C, + FlamesMeet1 = 0x5D2D, // VermillionFlame->self, 6.5s cast; makes the orb light up + FlamesMeet2 = 0x5D2E, // VermillionFlame->self, 11s cast; actual AOE + HeavenAndEarthCW = 0x5D2F, + HeavenAndEarthCCW = 0x5FEA, + HeavenAndEarthRotate = 0x5D30, // Unused by module + HeavenAndEarthStart = 0x5D31, + HeavenAndEarthMove = 0x5D32, + MoveMountains1 = 0x5D33, // Boss->self, 5s cast, first attack + MoveMountains2 = 0x5D34, // Boss->self, no cast, second attack + MoveMountains3 = 0x5D35, // Helper->self, 5s cast, first line + MoveMountains4 = 0x5D36, // Helper->self, no cast, second line + WindsPeak1 = 0x5D2A, // Boss->self, 3,0s cast, range 5 circle + WindsPeak2 = 0x5D2B, // Helper->self, 4,0s cast, range 50 circle + NaturesBlood1 = 0x5D28, // Helper->self, 7,5s cast, range 4 circle + NaturesBlood2 = 0x5D29, // Helper->self, no cast, range 4 circle + SplittingRage = 0x5D37, // Boss->self, 3,0s cast, range 50 circle + DuelOrDie = 0x5D1C, + WildfireCrucible = 0x5D3B, //enrage, 25s cast time +} + +public enum SID : uint +{ + OnFire = 0x9F3, // Boss->Boss + TemporaryMisdirection = 1422, // Boss->player, extra=0x2D0 + DuelOrDie = 0x9F1, // Boss/Helper->player +} diff --git a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs new file mode 100644 index 000000000..18c74bdaa --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs @@ -0,0 +1,178 @@ +namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon; + +class OnFire(BossModule module) : BossComponent(module) +{ + private bool _hasBuff; + private bool _isCasting; + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (_isCasting) + hints.Add("Applies On Fire to Lyon. Use Dispell to remove it"); + if (_hasBuff) + hints.Add("Lyon has 'On Fire'. Use Dispell to remove it!"); + } + + public override void OnStatusGain(Actor actor, ActorStatus status) + { + if (actor == Module.PrimaryActor && (SID)status.ID == SID.OnFire) + _hasBuff = true; + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.HarnessFire) + _isCasting = true; + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.HarnessFire) + _isCasting = false; + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if (actor == Module.PrimaryActor && (SID)status.ID == SID.OnFire) + _hasBuff = false; + } +} + +class WildfiresFury(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.WildfiresFury)); + +class HeavenAndEarth(BossModule module) : Components.GenericRotatingAOE(module) +{ + private Angle _increment; + + private static readonly AOEShapeCone _shape = new(20, 15.Degrees()); + + private int _index = 0; + + private void UpdateIncrement(Angle increment) + { + _increment = increment; + for (int i = 0; i < Sequences.Count; i++) + { + var sequence = Sequences[i]; + sequence.Increment = _increment; + Sequences[i] = sequence; + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.HeavenAndEarthCW) + UpdateIncrement(-30.Degrees()); + else if ((AID)spell.Action.ID == AID.HeavenAndEarthCCW) + UpdateIncrement(30.Degrees()); + + if ((AID)spell.Action.ID == AID.HeavenAndEarthStart) + Sequences.Add(new(_shape, caster.Position, spell.Rotation, _increment, spell.NPCFinishAt, 1.2f, 4)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.HeavenAndEarthMove && Sequences.Count > 0) + { + AdvanceSequence(_index++ % Sequences.Count, WorldState.CurrentTime); + } + } +} + +class HeartOfNatureConcentric(BossModule module) : Components.ConcentricAOEs(module, _shapes) +{ + private static readonly AOEShape[] _shapes = [new AOEShapeCircle(10), new AOEShapeDonut(10, 20), new AOEShapeDonut(20, 30)]; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.NaturesPulse1) + AddSequence(caster.Position, spell.NPCFinishAt); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (Sequences.Count > 0) + { + var order = (AID)spell.Action.ID switch + { + AID.NaturesPulse1 => 0, + AID.NaturesPulse2 => 1, + AID.NaturesPulse3 => 2, + _ => -1 + }; + AdvanceSequence(order, caster.Position); + } + } +} + +class CagedHeartOfNature(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.CagedHeartOfNature), new AOEShapeCircle(6)); + +class WindsPeak(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WindsPeak1), new AOEShapeCircle(5)); + +class WindsPeakKB(BossModule module) : Components.Knockback(module) +{ + private DateTime _time; + private bool _watched; + private DateTime _activation; + + public override IEnumerable Sources(int slot, Actor actor) + { + if (_watched && WorldState.CurrentTime < _time.AddSeconds(4.4f)) + yield return new(Module.PrimaryActor.Position, 15, _activation); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.WindsPeak1) + { + _watched = true; + _time = WorldState.CurrentTime; + _activation = spell.NPCFinishAt; + } + } +} + +class SplittingRage(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.SplittingRage), "Applies temporary misdirection"); + +class NaturesBlood(BossModule module) : Components.Exaflare(module, 4) +{ + class LineWithActor : Line + { + public Actor Caster; + + public LineWithActor(Actor caster) + { + Next = caster.Position; + Advance = 6 * caster.Rotation.ToDirection(); + NextExplosion = caster.CastInfo!.NPCFinishAt; + TimeToMove = 1.1f; + ExplosionsLeft = 7; + MaxShownExplosions = 3; + Caster = caster; + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.NaturesBlood1) + Lines.Add(new LineWithActor(caster)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (Lines.Count > 0 && (AID)spell.Action.ID is AID.NaturesBlood1 or AID.NaturesBlood2) + { + int index = Lines.FindIndex(item => ((LineWithActor)item).Caster == caster); + AdvanceLine(Lines[index], caster.Position); + if (Lines[index].ExplosionsLeft == 0) + Lines.RemoveAt(index); + } + } +} + +class MoveMountains(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MoveMountains3), new AOEShapeRect(40, 3)) +{ + // TODO predict rotation +} + +class WildfireCrucible(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.WildfireCrucible), "Enrage!", true); diff --git a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/FlamesMeet.cs b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/FlamesMeet.cs new file mode 100644 index 000000000..1cef83972 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/FlamesMeet.cs @@ -0,0 +1,33 @@ + +namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon; +class FlamesMeet(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = []; + private static readonly AOEShapeCross _shape = new(40, 7); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + for (int i = 0; i < _aoes.Count; i++) + { + AOEInstance aoe = _aoes[i]; + if (i == 0) + aoe.Color = ArenaColor.Danger; + yield return aoe; + // Only show the first 2 so it's obvious which one to go to. + if (i == 1) + break; + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.FlamesMeet2) + _aoes.Add(new(_shape, caster.Position)); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.FlamesMeet2 && _aoes.Count > 0) + _aoes.RemoveAt(0); + } +} diff --git a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/TasteOfBloodAndDuelOrDie.cs b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/TasteOfBloodAndDuelOrDie.cs new file mode 100644 index 000000000..d8e5a2a77 --- /dev/null +++ b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/TasteOfBloodAndDuelOrDie.cs @@ -0,0 +1,54 @@ +using BossMod.Components; + +namespace BossMod.Shadowbringers.Foray.Duel.Duel6Lyon; + +class TasteOfBloodAndDuelOrDie(BossModule module) : GenericAOEs(module) +{ + private readonly AOEShape _tasteOfBloodShape = new AOEShapeCone(40, 90.Degrees()); + public readonly List Casters = []; + public readonly List Duelers = []; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + foreach (var caster in Casters) + { + // If the caster did Duel Or Die, the player must get hit by their attack. + // This is represented by pointing the AOE behind the caster so their front is safe. + Angle angle = Duelers.Contains(caster) ? caster.Rotation + 180.Degrees() : caster.Rotation; + yield return new AOEInstance(_tasteOfBloodShape, caster.Position, angle); + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TasteOfBlood) + Casters.Add(caster); + + if ((AID)spell.Action.ID == AID.DuelOrDie) + Duelers.Add(caster); + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.TasteOfBlood) + { + Casters.Remove(caster); + Duelers.Remove(caster); + } + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var caster in Casters) + { + bool isDueler = Duelers.Contains(caster); + Arena.Actor(caster, isDueler ? ArenaColor.Danger : ArenaColor.Enemy, true); + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (Duelers.Count > 0) + hints.Add($"Get hit by {Duelers.Count} Duel or Die Taste Of Blood"); + } +} From 99e1d0b752cc49ac70b62caf76b8841012028d25 Mon Sep 17 00:00:00 2001 From: sourpuh <150582765+sourpuh@users.noreply.github.com> Date: Fri, 3 May 2024 11:43:43 -0700 Subject: [PATCH 2/3] Fix build issue after merge --- .../Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs index 5f2b4ebf4..730db48cb 100644 --- a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs +++ b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6Lyon.cs @@ -21,7 +21,7 @@ public Duel6LyonStates(BossModule module) : base(module) } [ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "SourP", GroupType = BossModuleInfo.GroupType.BozjaDuel, GroupID = 778, NameID = 31)] -public class Duel6Lyon(WorldState ws, Actor primary) : BossModule(ws, primary, new ArenaBoundsCircle(new(50f, -410f), 20)) +public class Duel6Lyon(WorldState ws, Actor primary) : BossModule(ws, primary, new(50f, -410f), new ArenaBoundsCircle(20)) { protected override void DrawEnemies(int pcSlot, Actor pc) { From c06c49fe71698a9987deef2e590976b4f668290d Mon Sep 17 00:00:00 2001 From: sourpuh <150582765+sourpuh@users.noreply.github.com> Date: Fri, 3 May 2024 11:47:53 -0700 Subject: [PATCH 3/3] Fix CodeAnalysis build issue --- .../Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs index 18c74bdaa..074220cf5 100644 --- a/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs +++ b/BossMod/Modules/Shadowbringers/Foray/Duel/Duel6Lyon/Duel6LyonGenericAttacks.cs @@ -46,7 +46,7 @@ class HeavenAndEarth(BossModule module) : Components.GenericRotatingAOE(module) private static readonly AOEShapeCone _shape = new(20, 15.Degrees()); - private int _index = 0; + private int _index; private void UpdateIncrement(Angle increment) {