From 95663ae8cd95cf736b7345685a6581c2636806c9 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Thu, 9 Jan 2025 22:21:54 +0000 Subject: [PATCH 1/4] Chaotic & FRU stuff --- .../Ch01CloudOfDarkness.cs | 2 +- .../Ch01CloudOfDarknessStates.cs | 44 ++++--- .../DiffusiveForceParticleBeam.cs | 7 +- .../Chaotic/Ch01CloudOfDarkness/Enaero.cs | 115 ------------------ .../{Endeath.cs => EnaeroEndeath.cs} | 87 ++++++++++--- .../Modules/Dawntrail/Ultimate/FRU/FRUAI.cs | 6 +- .../Dawntrail/Ultimate/FRU/FRUStates.cs | 4 +- .../Dawntrail/Ultimate/FRU/P1Explosion.cs | 5 +- .../Dawntrail/Ultimate/FRU/P2Banish.cs | 55 ++++++++- .../Dawntrail/Ultimate/FRU/P2LightRampant.cs | 78 +++++++++++- 10 files changed, 240 insertions(+), 163 deletions(-) delete mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs rename BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/{Endeath.cs => EnaeroEndeath.cs} (56%) diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs index 511b8792c3..b1b654b597 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarkness.cs @@ -12,7 +12,7 @@ class LoomingChaos(BossModule module) : Components.CastCounter(module, ActionID. // TODO: tankswap hints component for phase1 // TODO: phase 2 teleport zones? // TODO: grim embrace / curse of darkness prevent turning -[ModuleInfo(BossModuleInfo.Maturity.Verified, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1010, NameID = 13624, PlanLevel = 100)] public class Ch01CloudOfDarkness(WorldState ws, Actor primary) : BossModule(ws, primary, DefaultCenter, InitialBounds) { public static readonly WPos DefaultCenter = new(100, 100); diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessStates.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessStates.cs index c688c9c976..6263eab241 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessStates.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessStates.cs @@ -33,7 +33,7 @@ private void Fork1(uint id) ComponentCondition(id + 0x410000, 4, comp => comp.Casters.Count > 0); Subphase1Variant2End(id + 0x410000, 8); - Cast(id + 0x500000, AID.Enrage, 8.1f, 12, "Enrage"); // TODO: check delay + Cast(id + 0x500000, AID.Enrage, 11.2f, 12, "Enrage"); } private void Fork2(uint id) @@ -46,7 +46,7 @@ private void Fork2(uint id) ComponentCondition(id + 0x410000, 4, comp => comp.Casters.Count > 0); Subphase1Variant1End(id + 0x410000, 6.1f); - Cast(id + 0x500000, AID.Enrage, 8.1f, 12, "Enrage"); + Cast(id + 0x500000, AID.Enrage, 8, 12, "Enrage"); } private void Subphase1Variant1End(uint id, float delay) @@ -76,8 +76,8 @@ private void Subphase2(uint id, float delay) ThirdArtOfDarknessParticleConcentration(id + 0x20000, 4); // note: 3s after towers resolve, outer ring becomes normal GhastlyGloom(id + 0x30000, 12.3f); CurseOfDarkness(id + 0x40000, 8.3f); - EvilSeedChaosCondensedDiffusiveForceParticleBeam(id + 0x50000, 9.9f); - ActivePivotParticleBeam(id + 0x70000, 4.4f); + EvilSeedChaosCondensedDiffusiveForceParticleBeam(id + 0x50000, 10); + ActivePivotParticleBeam(id + 0x70000, 4.5f); LoomingChaos(id + 0x80000, 6.2f); CurseOfDarkness(id + 0x100000, 11.9f); @@ -85,9 +85,9 @@ private void Subphase2(uint id, float delay) DarkDominion(id + 0x120000, 1); // note: 1s after cast ends, outer ring becomes dangerous FeintParticleBeamThirdActOfDarkness(id + 0x130000, 3.1f); // note: 2.5s after act of darkness resolves, outer ring becomes normal GhastlyGloom(id + 0x140000, 11.4f); - PhaserChaosCondensedDiffusiveForceParticleBeam(id + 0x150000, 3.4f); - FloodOfDarknessAdds(id + 0x160000, 3); - FloodOfDarkness2(id + 0x170000, 8.5f); + PhaserChaosCondensedDiffusiveForceParticleBeam(id + 0x150000, 3.3f); + FloodOfDarknessAdds(id + 0x160000, 2.9f); + FloodOfDarkness2(id + 0x170000, 8.6f); } private void BladeOfDarkness(uint id, float delay) @@ -110,9 +110,8 @@ private void DelugeOfDarkness1(uint id, float delay) .ActivateOnEnter(); CastMulti(id + 0x110, [AID.GrimEmbraceForward, AID.GrimEmbraceBackward], 3.1f, 5, "Debuffs 2") .ActivateOnEnter() // has weird overlaps, easier to keep active for the entirety of the phase - .ActivateOnEnter() // we want to keep all these components active, so that they provide advance hints for delayed resolve + .ActivateOnEnter() // we want to keep all these components active, so that they provide advance hints for delayed resolve .ActivateOnEnter() // death has extra resolve steps, which make writing states weird - .ActivateOnEnter() .ActivateOnEnter(); } @@ -125,7 +124,7 @@ private void RazingVolleyParticleBeamStart(uint id, float delay) private void DeathAero(uint id, float delay) { CastMulti(id, [AID.Death, AID.Aero], delay, 5.6f); - Condition(id + 0x10, 0.5f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "Knockback/attract"); + ComponentCondition(id + 0x10, 0.5f, comp => comp.NumCasts > 0, "Knockback/attract"); } private void EndeathEnaero(uint id, float delay) @@ -136,7 +135,7 @@ private void EndeathEnaero(uint id, float delay) private void BladeOfDarknessEndeathEnaeroResolve(uint id, float delay) { BladeOfDarkness(id, delay); - Condition(id + 0x100, 2.2f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "Knockback/attract"); + ComponentCondition(id + 0x100, 2.2f, comp => comp.NumCasts > 0, "Knockback/attract"); } private void RazingVolleyParticleBeamBladeOfDarknessEndeathEnaeroResolve(uint id, float delay) @@ -204,9 +203,8 @@ private void FloodOfDarkness1(uint id, float delay) .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit() - .DeactivateOnExit() + .DeactivateOnExit() .DeactivateOnExit() - .DeactivateOnExit() .DeactivateOnExit() .OnExit(() => Module.Arena.Bounds = Ch01CloudOfDarkness.InitialBounds) .SetHint(StateMachine.StateHint.Raidwide); @@ -300,7 +298,7 @@ private void EvilSeedChaosCondensedDiffusiveForceParticleBeam(uint id, float del .ActivateOnEnter() .DeactivateOnExit() .DeactivateOnExit(); - Condition(id + 0x3010, 0.7f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.Spreads.Count == 0, "Spread/line stacks") + Condition(id + 0x3010, 0.8f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "Spread/line stacks") .DeactivateOnExit() .DeactivateOnExit(); // TODO: show second wave ... } @@ -308,13 +306,13 @@ private void EvilSeedChaosCondensedDiffusiveForceParticleBeam(uint id, float del private void ActivePivotParticleBeam(uint id, float delay) { CastStartMulti(id, [AID.ActivePivotParticleBeamCW, AID.ActivePivotParticleBeamCCW], delay); - ComponentCondition(id + 1, 0.9f, comp => comp.Casters.Count > 0) + ComponentCondition(id + 1, 0.8f, comp => comp.Casters.Count > 0) .ActivateOnEnter() .ActivateOnEnter(); ComponentCondition(id + 2, 8, comp => comp.NumCasts > 0, "Adds front/sides"); ComponentCondition(id + 3, 1.5f, comp => comp.NumCasts >= 6, "Adds sides/front") .DeactivateOnExit(); - CastEnd(id + 4, 3.6f); + CastEnd(id + 4, 3.7f); ComponentCondition(id + 0x10, 0.6f, comp => comp.NumCasts > 0, "Rotation start"); ComponentCondition(id + 0x20, 6.6f, comp => comp.NumCasts > 4, "Rotation end") .DeactivateOnExit(); @@ -336,10 +334,10 @@ private void LoomingChaos(uint id, float delay) private void ParticleConcentrationPhaser(uint id, float delay) { CastStart(id, AID.ParticleConcentration, delay); - ComponentCondition(id + 1, 1.1f, comp => comp.Casters.Count > 0) + ComponentCondition(id + 1, 1, comp => comp.Casters.Count > 0) .ActivateOnEnter(); - CastEnd(id + 2, 4.9f); - ComponentCondition(id + 0x10, 3.1f, comp => comp.NumCasts > 0, "Adds front/sides") + CastEnd(id + 2, 5); + ComponentCondition(id + 0x10, 3, comp => comp.NumCasts > 0, "Adds front/sides") .ActivateOnEnter(); // TODO: towers appear 1s after cast end ComponentCondition(id + 0x11, 1.5f, comp => comp.NumCasts >= 6, "Adds sides/front") .DeactivateOnExit(); @@ -372,14 +370,14 @@ private void PhaserChaosCondensedDiffusiveForceParticleBeam(uint id, float delay { ComponentCondition(id, delay, comp => comp.Casters.Count > 0) .ActivateOnEnter(); - CastStartMulti(id + 0x10, [AID.ChaosCondensedParticleBeam, AID.DiffusiveForceParticleBeam], 7.4f); - ComponentCondition(id + 0x11, 0.6f, comp => comp.NumCasts > 0, "Adds front/sides") + CastStartMulti(id + 0x10, [AID.ChaosCondensedParticleBeam, AID.DiffusiveForceParticleBeam], 7.5f); + ComponentCondition(id + 0x11, 0.5f, comp => comp.NumCasts > 0, "Adds front/sides") .ActivateOnEnter() .ActivateOnEnter(); ComponentCondition(id + 0x12, 1.5f, comp => comp.NumCasts >= 6, "Adds sides/front") .DeactivateOnExit(); - CastEnd(id + 0x13, 5.9f); - Condition(id + 0x20, 0.7f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.Spreads.Count == 0, "Spread/line stacks") + CastEnd(id + 0x13, 6); + Condition(id + 0x20, 0.8f, () => Module.FindComponent()?.NumCasts > 0 || Module.FindComponent()?.NumCasts > 0, "Spread/line stacks") .DeactivateOnExit() .DeactivateOnExit(); // TODO: show second wave ... } diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs index 5c5166af97..faaeccc062 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DiffusiveForceParticleBeam.cs @@ -7,6 +7,8 @@ // TODO: show second wave for players not hit by first wave class DiffusiveForceParticleBeam(BossModule module) : Components.UniformStackSpread(module, 0, 7) { + public int NumCasts; + public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.DiffusiveForceParticleBeam) @@ -16,6 +18,9 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) public override void OnEventCast(Actor caster, ActorCastEvent spell) { if ((AID)spell.Action.ID is AID.DiffusiveForceParticleBeamAOE1 or AID.DiffusiveForceParticleBeamAOE2) - Spreads.Clear(); + { + ++NumCasts; + Spreads.RemoveAll(s => s.Target.InstanceID == spell.MainTargetID); + } } } diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs deleted file mode 100644 index a49f46bc7b..0000000000 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Enaero.cs +++ /dev/null @@ -1,115 +0,0 @@ -namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; - -class EnaeroKnockback(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.EnaeroKnockback)) -{ - private Source? _source; - private bool _delayed; - - public override IEnumerable Sources(int slot, Actor actor) => Utils.ZeroOrOne(_source); - - public override void AddHints(int slot, Actor actor, TextHints hints) - { - if (!_delayed) - base.AddHints(slot, actor, hints); - } - - public override void AddGlobalHints(GlobalHints hints) - { - if (_delayed) - hints.Add("Delayed knockback"); - } - - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - switch ((AID)spell.Action.ID) - { - case AID.Aero: - Start(Module.CastFinishAt(spell, 0.5f)); - break; - case AID.Enaero: - _delayed = true; - break; - case AID.AeroKnockback: - case AID.EnaeroKnockback: - if (_source == null || !_source.Value.Origin.AlmostEqual(caster.Position, 1)) - ReportError("Aero knockback mispredicted"); - break; - case AID.BladeOfDarknessLAOE: - case AID.BladeOfDarknessRAOE: - case AID.BladeOfDarknessCAOE: - if (_delayed) - Start(Module.CastFinishAt(spell, 2.2f)); - break; - } - } - - public override void OnCastFinished(Actor caster, ActorCastInfo spell) - { - switch ((AID)spell.Action.ID) - { - case AID.AeroKnockback: - case AID.EnaeroKnockback: - ++NumCasts; - _source = null; - break; - case AID.BladeOfDarknessLAOE: - case AID.BladeOfDarknessRAOE: - case AID.BladeOfDarknessCAOE: - _delayed = false; - break; - } - } - - private void Start(DateTime activation) - { - NumCasts = 0; - _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation); - } -} - -class EnaeroAOE(BossModule module) : Components.GenericAOEs(module) -{ - private AOEInstance? _aoe; - private bool _delayed; - - private static readonly AOEShapeCircle _shape = new(8); - - public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); - - public override void OnCastStarted(Actor caster, ActorCastInfo spell) - { - switch ((AID)spell.Action.ID) - { - case AID.Aero: - Start(Module.CastFinishAt(spell, 0.5f)); - break; - case AID.Enaero: - _delayed = true; - break; - } - } - - public override void OnCastFinished(Actor caster, ActorCastInfo spell) - { - switch ((AID)spell.Action.ID) - { - case AID.AeroAOE: - case AID.EnaeroAOE: - _aoe = null; - break; - case AID.BladeOfDarknessLAOE: - case AID.BladeOfDarknessRAOE: - case AID.BladeOfDarknessCAOE: - if (_delayed) - Start(WorldState.FutureTime(2.2f)); - break; - } - } - - private void Start(DateTime activation) - { - NumCasts = 0; - _aoe = new(_shape, Ch01CloudOfDarkness.Phase1Midpoint, default, activation); - _delayed = false; - } -} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/EnaeroEndeath.cs similarity index 56% rename from BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs rename to BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/EnaeroEndeath.cs index 7650ae7445..a2aa3304b6 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Endeath.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/EnaeroEndeath.cs @@ -1,46 +1,56 @@ namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; -class EndeathVortex(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.EndeathVortex)) +class EnaeroEndeath(BossModule module) : Components.Knockback(module) { private Source? _source; - private bool _delayed; + private Kind _delayed; public override IEnumerable Sources(int slot, Actor actor) => Utils.ZeroOrOne(_source); - - public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => (pos - Ch01CloudOfDarkness.Phase1Midpoint).LengthSq() <= 36; + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => _source?.Kind == Kind.TowardsOrigin ? (pos - _source.Value.Origin).LengthSq() <= 36 : !Module.InBounds(pos); public override void AddHints(int slot, Actor actor, TextHints hints) { - if (!_delayed) + if (_delayed == Kind.None) base.AddHints(slot, actor, hints); } public override void AddGlobalHints(GlobalHints hints) { - if (_delayed) - hints.Add("Delayed attract"); + if (_delayed != Kind.None) + hints.Add($"Delayed {(_delayed == Kind.TowardsOrigin ? "attract" : "knockback")}"); } public override void OnCastStarted(Actor caster, ActorCastInfo spell) { switch ((AID)spell.Action.ID) { + case AID.Aero: + Start(Module.CastFinishAt(spell, 0.5f), Kind.AwayFromOrigin); + break; case AID.Death: - Start(Module.CastFinishAt(spell, 0.5f)); + Start(Module.CastFinishAt(spell, 0.5f), Kind.TowardsOrigin); + break; + case AID.Enaero: + _delayed = Kind.AwayFromOrigin; break; case AID.Endeath: - _delayed = true; + _delayed = Kind.TowardsOrigin; + break; + case AID.AeroKnockback: + case AID.EnaeroKnockback: + if (_source == null || _source.Value.Kind != Kind.AwayFromOrigin || !_source.Value.Origin.AlmostEqual(caster.Position, 1)) + ReportError("Aero knockback mispredicted"); break; case AID.DeathVortex: case AID.EndeathVortex: - if (_source == null || !_source.Value.Origin.AlmostEqual(caster.Position, 1)) + if (_source == null || _source.Value.Kind != Kind.TowardsOrigin || !_source.Value.Origin.AlmostEqual(caster.Position, 1)) ReportError("Death vortex mispredicted"); break; case AID.BladeOfDarknessLAOE: case AID.BladeOfDarknessRAOE: case AID.BladeOfDarknessCAOE: - if (_delayed) - Start(Module.CastFinishAt(spell, 2.2f)); + if (_delayed != Kind.None) + Start(Module.CastFinishAt(spell, 2.2f), _delayed); break; } } @@ -49,6 +59,8 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) { switch ((AID)spell.Action.ID) { + case AID.AeroKnockback: + case AID.EnaeroKnockback: case AID.DeathVortex: case AID.EndeathVortex: ++NumCasts; @@ -57,7 +69,53 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) case AID.BladeOfDarknessLAOE: case AID.BladeOfDarknessRAOE: case AID.BladeOfDarknessCAOE: - _delayed = false; + _delayed = Kind.None; + break; + } + } + + private void Start(DateTime activation, Kind kind) + { + NumCasts = 0; + _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation, Kind: kind); + } +} + +class EnaeroAOE(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + private bool _delayed; + + private static readonly AOEShapeCircle _shape = new(8); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.Aero: + Start(Module.CastFinishAt(spell, 0.5f)); + break; + case AID.Enaero: + _delayed = true; + break; + } + } + + public override void OnCastFinished(Actor caster, ActorCastInfo spell) + { + switch ((AID)spell.Action.ID) + { + case AID.AeroAOE: + case AID.EnaeroAOE: + _aoe = null; + break; + case AID.BladeOfDarknessLAOE: + case AID.BladeOfDarknessRAOE: + case AID.BladeOfDarknessCAOE: + if (_delayed) + Start(WorldState.FutureTime(2.2f)); break; } } @@ -65,7 +123,8 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) private void Start(DateTime activation) { NumCasts = 0; - _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation, Kind: Kind.TowardsOrigin); + _aoe = new(_shape, Ch01CloudOfDarkness.Phase1Midpoint, default, activation); + _delayed = false; } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs index 6651377699..f340edad99 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUAI.cs @@ -8,7 +8,7 @@ namespace BossMod.Dawntrail.Ultimate.FRU; sealed class FRUAI(RotationModuleManager manager, Actor player) : AIRotationModule(manager, player) { public enum Track { Movement } - public enum MovementStrategy { None, Pathfind, PathfindMeleeGreed, Explicit, Prepull, DragToCenter } + public enum MovementStrategy { None, Pathfind, PathfindMeleeGreed, Explicit, ExplicitMelee, Prepull, DragToCenter } public static RotationModuleDefinition Definition() { @@ -18,6 +18,7 @@ public static RotationModuleDefinition Definition() .AddOption(MovementStrategy.Pathfind, "Pathfind", "Use standard pathfinding to move") .AddOption(MovementStrategy.PathfindMeleeGreed, "PathfindMeleeGreed", "Melee greed: find closest safespot, then move to maxmelee closest to it") .AddOption(MovementStrategy.Explicit, "Explicit", "Move to specific point", supportedTargets: ActionTargets.Area) + .AddOption(MovementStrategy.ExplicitMelee, "ExplicitMelee", "Move to the point in maxmelee that is closest to specific point", supportedTargets: ActionTargets.Area) .AddOption(MovementStrategy.Prepull, "Prepull", "Pre-pull position: as close to the clock-spot as possible") .AddOption(MovementStrategy.DragToCenter, "DragToCenter", "Drag boss to the arena center"); return res; @@ -38,6 +39,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa MovementStrategy.Pathfind => PathfindPosition(null), MovementStrategy.PathfindMeleeGreed => PathfindPosition(ResolveTargetOverride(strategy.Value) ?? primaryTarget), MovementStrategy.Explicit => ResolveTargetLocation(strategy.Value), + MovementStrategy.ExplicitMelee => ExplicitMeleePosition(ResolveTargetLocation(strategy.Value), ResolveTargetOverride(strategy.Value) ?? primaryTarget), MovementStrategy.Prepull => PrepullPosition(module, assignment), MovementStrategy.DragToCenter => DragToCenterPosition(module), _ => null @@ -50,6 +52,8 @@ private WPos PathfindPosition(Actor? meleeGreedTarget) return meleeGreedTarget != null && res.Destination != null ? ClosestInMelee(res.Destination.Value, meleeGreedTarget) : (res.Destination ?? Player.Position); } + private WPos ExplicitMeleePosition(WPos ideal, Actor? target) => target != null ? ClosestInMelee(ideal, target) : ideal; + // assumption: pull range is 12; hitbox is 5, so maxmelee is 8, meaning we have approx 4m to move during pull - with sprint, speed is 7.8, accel is 30 => over 0.26s accel period we move 1.014m, then need another 0.38s to reach boss (but it also moves) private WPos PrepullPosition(FRU module, PartyRolesConfig.Assignment assignment) { diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index 00b3ec53b0..f9b99c19b7 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -296,7 +296,7 @@ private void P2MirrorMirror(uint id, float delay) .DeactivateOnExit(); ActorCastMulti(id + 0x100, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 0.5f, 5, true) - .ActivateOnEnter(); + .ActivateOnEnter(); ComponentCondition(id + 0x102, 0.1f, comp => !comp.Active, "Spread/Stack") .DeactivateOnExit(); } @@ -327,7 +327,7 @@ private void P2LightRampant(uint id, float delay) ActorCastStartMulti(id + 0x70, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 1.7f, true); ComponentCondition(id + 0x71, 1.9f, comp => comp.NumCasts > 0, "Central tower") - .ActivateOnEnter() + .ActivateOnEnter() .DeactivateOnExit() .DeactivateOnExit(); ActorCastEnd(id + 0x72, _module.BossP2, 3.1f, true); diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs index e14e742f98..1a6d53c853 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs @@ -23,7 +23,10 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { // tanks: stay opposite towers on N/S side (unless cheesing tankbusters) // tweak for WAR: if PR is up, assume player will want to maintain full uptime on wide line by using it right before resolve - we want to stay far to increase travel time - var horizOffset = _isWideLine && !_lineDone && actor.Class == Class.WAR && actor.FindStatus(WAR.SID.PrimalRend) != null ? 17 : 0; + // if doing tankbuster cheese, after line resolves, stay on maxmelee far from towers to give more space for melees + var horizOffset = !_lineDone + ? (_isWideLine && actor.Class == Class.WAR && actor.FindStatus(WAR.SID.PrimalRend) != null ? 17 : 0) + : (_config.P1ExplosionsTankbusterCheese ? 7 : 0); hints.AddForbiddenZone(ShapeDistance.HalfPlane(Module.Center - horizOffset * TowerDir, -TowerDir), Activation); if (!_config.P1ExplosionsTankbusterCheese) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs index 3a4cd8411e..97a3597ba8 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs @@ -1,6 +1,6 @@ namespace BossMod.Dawntrail.Ultimate.FRU; -class P2Banish(BossModule module) : Components.UniformStackSpread(module, 5, 5, 2, 2, true) +abstract class P2Banish(BossModule module) : Components.UniformStackSpread(module, 5, 5, 2, 2, true) { public override void OnCastStarted(Actor caster, ActorCastInfo spell) { @@ -29,3 +29,56 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) } } } + +// this variant provides hints after mirrors +class P2Banish1(BossModule module) : P2Banish(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var prepos = PrepositionLocation(assignment); + if (prepos != null) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(prepos.Value, 1), DateTime.MaxValue); + else + base.AddAIHints(slot, actor, assignment, hints); + } + + private WPos? PrepositionLocation(PartyRolesConfig.Assignment assignment) + { + // TODO: consider a different strategy for melee (left if more left) + if (Stacks.Count > 0 && Stacks[0].Activation > WorldState.FutureTime(2.5f)) + { + // preposition for stacks + var boss = Module.Enemies(OID.BossP2).FirstOrDefault(); + return assignment switch + { + PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.M1 => boss != null ? boss.Position + 6 * boss.Rotation.ToDirection().OrthoL() : null, + PartyRolesConfig.Assignment.OT or PartyRolesConfig.Assignment.M2 => boss != null ? boss.Position + 6 * boss.Rotation.ToDirection().OrthoR() : null, + _ => null // TODO: implement positioning for ranged + }; + } + else if (Spreads.Count > 0 && Spreads[0].Activation > WorldState.FutureTime(2.5f)) + { + // preposition for spreads + var boss = Module.Enemies(OID.BossP2).FirstOrDefault(); + return assignment switch + { + PartyRolesConfig.Assignment.MT => boss != null ? boss.Position + 6 * (boss.Rotation + 45.Degrees()).ToDirection() : null, + PartyRolesConfig.Assignment.OT => boss != null ? boss.Position + 6 * (boss.Rotation - 45.Degrees()).ToDirection() : null, + PartyRolesConfig.Assignment.M1 => boss != null ? boss.Position + 6 * (boss.Rotation + 135.Degrees()).ToDirection() : null, + PartyRolesConfig.Assignment.M2 => boss != null ? boss.Position + 6 * (boss.Rotation - 135.Degrees()).ToDirection() : null, + _ => null // TODO: implement positioning for ranged + }; + } + return null; + } +} + +// this variant provides hints after rampant +class P2Banish2(BossModule module) : P2Banish(module) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // TODO + base.AddAIHints(slot, actor, assignment, hints); + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs index feb3260076..fa22d4dbb9 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs @@ -31,15 +31,64 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) class P2LuminousHammer(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(6), (uint)IconID.LuminousHammer, ActionID.MakeSpell(AID.LuminousHammer), 7.1f, true) { private readonly int[] _baitsPerPlayer = new int[PartyState.MaxPartySize]; + private readonly WPos[] _prevBaitPos = new WPos[PartyState.MaxPartySize]; + + private const float FirstBaitOffset = 8; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + var bait = ActiveBaitsOn(actor).FirstOrDefault(); + if (bait.Target == null) + return; // no bait, don't care + + switch (_baitsPerPlayer[slot]) + { + case 0: + // position for first bait + var partner = ActiveBaitsNotOn(actor).FirstOrDefault().Target; + if (partner == null) + return; // we can't resolve the hint without knowing the partner + + // logic: + // - if actor and partner are north and south, stay on current side + // - if both are on the same side, the 'more clockwise' one (NE/SW) moves to the opposite side + // TODO: last rule is fuzzy in practice, see if we can adjust better + var north = actor.Position.Z < Module.Center.Z; + if (north == (partner.Position.Z < Module.Center.Z)) + { + // same side, see if we need to swap + var moreRight = actor.Position.X > partner.Position.X; + var moreCW = north == moreRight; + north ^= moreCW; + } + + var preposSpot = Module.Center + new WDir(0, north ? -FirstBaitOffset : FirstBaitOffset); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(preposSpot, 1), bait.Activation); + break; + case 1: + case 2: + // second/third bait - rotate 45 degrees CW + var secondSpot = Module.Center + (_prevBaitPos[slot] - Module.Center).Rotate(-45.Degrees()); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(secondSpot, 3)); + break; + case 3: + case 4: + // fourth/fifth bait - move towards N/S wall + var dest = Module.Center + new WDir(0, _prevBaitPos[slot].X > Module.Center.X ? +18 : -18); + var thirdSpot = _prevBaitPos[slot] + 6 * (dest - _prevBaitPos[slot]).Normalized(); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(thirdSpot, 3)); + break; + } + } public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if (spell.Action == WatchedAction) + if (spell.Action == WatchedAction && Raid.FindSlot(spell.MainTargetID) is var slot && slot >= 0) { ++NumCasts; - var slot = Raid.FindSlot(spell.MainTargetID); - if (slot >= 0 && ++_baitsPerPlayer[slot] >= 5) - CurrentBaits.RemoveAll(b => b.Target == Raid[slot]); + _prevBaitPos[slot] = Raid[slot]?.Position ?? default; + if (++_baitsPerPlayer[slot] == 5) + CurrentBaits.RemoveAll(b => b.Target == Raid[slot]); // last bait } } } @@ -49,6 +98,18 @@ class P2BrightHunger1(BossModule module) : Components.GenericTowers(module, Acti private readonly FRUConfig _config = Service.Config.Get(); private BitMask _forbidden; + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // if we have one tower assigned, stay inside it, somewhat closer to the edge + var assignedTowerIndex = Towers.FindIndex(t => !t.ForbiddenSoakers[slot]); + if (assignedTowerIndex >= 0 && Towers.FindIndex(assignedTowerIndex + 1, t => !t.ForbiddenSoakers[slot]) < 0) + { + ref var t = ref Towers.Ref(assignedTowerIndex); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + (t.Position - Module.Center) * 1.125f, 2), t.Activation); // center is at R16, x1.125 == R18 + } + // else: we either have no towers assigned (== doing puddles), or have multiple assigned (== assignments failed), so do nothing + } + public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) { if (iconID == (uint)IconID.LuminousHammer) @@ -107,6 +168,15 @@ private void RebuildTowers() class P2PowerfulLight(BossModule module) : Components.UniformStackSpread(module, 5, 0, 4, 4) { + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // TODO: there are several 'stages' here, which need different hints: + // 1. initially (before first holy light burst cast start / last puddle bait, which happen roughly at the same time), we want to stack strictly N/S (unless we are baiting puddles) + // 2. after that, we need to move CW closer to the holy light burst border; if we are stack target, we need to ensure we wait for puddle baiter, otherwise we can just follow stack target very closely + // 3. after this mechanic resolves, we need to dodge HLBs + base.AddAIHints(slot, actor, assignment, hints); + } + public override void OnStatusGain(Actor actor, ActorStatus status) { if ((SID)status.ID == SID.WeightOfLight) From bc89951570420669452fbe8380cf5c5da1a75e1c Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Fri, 10 Jan 2025 01:13:39 +0000 Subject: [PATCH 2/4] FRU LR AI WIP --- .../Dawntrail/Ultimate/FRU/FRUConfig.cs | 17 +- .../Dawntrail/Ultimate/FRU/FRUStates.cs | 17 +- .../Dawntrail/Ultimate/FRU/P2Banish.cs | 39 ++- .../Dawntrail/Ultimate/FRU/P2LightRampant.cs | 258 +++++++++++++----- 4 files changed, 252 insertions(+), 79 deletions(-) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs index 7fee41ace8..939db4a30e 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs @@ -64,12 +64,12 @@ public class FRUConfig() : ConfigNode() [PropertyDisplay("P3 Apocalypse: uptime swaps (only consider swaps within prio 1/2 and 3/4, assuming these are melee and ranged)")] public bool P3ApocalypseUptime; - [PropertyDisplay("P4 Darklit Dragonsong: assignments (lower prio stays more clockwise, lowest prio support takes N tower)", separator: true)] + [PropertyDisplay("P4 Darklit Dragonsong: assignments (lower prio stays more clockwise, lowest prio support takes N tower)")] [GroupDetails(["Support prio1", "Support prio2", "Support prio3", "Support prio4", "DD prio1", "DD prio2", "DD prio3", "DD prio4"])] [GroupPreset("Default (healer N)", [2, 3, 0, 1, 4, 5, 6, 7])] public GroupAssignmentUnique P4DarklitDragonsongAssignments = new() { Assignments = [2, 3, 0, 1, 4, 5, 6, 7] }; - [PropertyDisplay("P4 Crystallize Time: assignments for claws (lower prio goes west)")] + [PropertyDisplay("P4 Crystallize Time: assignments for claws (lower prio goes west)", separator: true)] [GroupDetails(["Prio 1", "Prio 2", "Prio 3", "Prio 4", "Prio 5", "Prio 6", "Prio 7", "Prio 8"])] [GroupPreset("Default HTMR", [3, 2, 1, 0, 4, 5, 6, 7])] public GroupAssignmentUnique P4CrystallizeTimeAssignments = new() { Assignments = [3, 2, 1, 0, 4, 5, 6, 7] }; @@ -111,4 +111,17 @@ public class FRUConfig() : ConfigNode() [GroupDetails(["Boss wall right", "Boss wall left", "Boss center", "Boss diagonal", "Mirror wall right", "Mirror wall left", "Mirror center", "Mirror diagonal"])] [GroupPreset("Default", [1, 0, 6, 7, 2, 3, 4, 5])] public GroupAssignmentUnique P2MirrorMirror2SpreadSpots = new() { Assignments = [1, 0, 6, 7, 2, 3, 4, 5] }; + + [PropertyDisplay("P2 Banish after Light Rampant: spread clock spots (supports should be near dd to resolve pairs)", tooltip: "Only used by AI")] + [GroupDetails(["N", "NE", "E", "SE", "S", "SW", "W", "NW"])] + [GroupPreset("Default", [0, 4, 6, 2, 5, 3, 7, 1])] + public GroupAssignmentUnique P2Banish2SpreadSpots = new() { Assignments = [0, 4, 6, 2, 5, 3, 7, 1] }; + + [PropertyDisplay("P2 Banish after Light Rampant: role that moves from their default spread spot to resolve pairs", tooltip: "Only used by AI")] + [PropertyCombo("DD", "Supports")] + public bool P2Banish2SupportsMoveToStack = true; + + [PropertyDisplay("P2 Banish after Light Rampant: direction to move to resolve pairs", tooltip: "Only used by AI")] + [PropertyCombo("CW", "CCW")] + public bool P2Banish2MoveCCWToStack = true; } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index f9b99c19b7..e601177670 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -309,25 +309,34 @@ private void P2LightRampant(uint id, float delay) .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .SetHint(StateMachine.StateHint.DowntimeStart); ComponentCondition(id + 0x20, 4.8f, comp => comp.NumCasts > 0, "Puddle bait"); ComponentCondition(id + 0x30, 3.3f, comp => comp.NumCasts > 0, "Towers") .ActivateOnEnter() + .DeactivateOnExit() .DeactivateOnExit(); - ComponentCondition(id + 0x40, 5.7f, comp => !comp.Active, "Stack") + ComponentCondition(id + 0x38, 3.2f, comp => comp.Casters.Count > 0) .ActivateOnEnter() .ActivateOnEnter() - .DeactivateOnExit() // last puddle is baited right before holy light burst casts start - .DeactivateOnExit(); + .ActivateOnEnter() + .DeactivateOnExit(); // last puddle is baited right before holy light burst casts start + ComponentCondition(id + 0x40, 2.5f, comp => !comp.Active, "Stack") + .DeactivateOnExit() + .DeactivateOnExit(); ComponentCondition(id + 0x50, 2.4f, comp => comp.NumCasts > 0, "Orbs 1") + .ActivateOnEnter() .ActivateOnEnter(); ComponentCondition(id + 0x60, 3, comp => comp.NumCasts > 3, "Orbs 2") + .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit(); // tethers resolve right after first orbs - ActorCastStartMulti(id + 0x70, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 1.7f, true); + ActorCastStartMulti(id + 0x70, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 1.7f, true) + .ActivateOnEnter(); ComponentCondition(id + 0x71, 1.9f, comp => comp.NumCasts > 0, "Central tower") .ActivateOnEnter() + .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit(); ActorCastEnd(id + 0x72, _module.BossP2, 3.1f, true); diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs index 97a3597ba8..a60249fab5 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs @@ -76,9 +76,44 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme // this variant provides hints after rampant class P2Banish2(BossModule module) : P2Banish(module) { + private readonly FRUConfig _config = Service.Config.Get(); + private bool _allowHints; + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - // TODO - base.AddAIHints(slot, actor, assignment, hints); + if (!_allowHints) + return; // don't interfere with tower hints until it's done + var prepos = PrepositionLocation(assignment); + if (prepos != null) + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(prepos.Value, 1), DateTime.MaxValue); + else + base.AddAIHints(slot, actor, assignment, hints); + } + + private WPos? PrepositionLocation(PartyRolesConfig.Assignment assignment) + { + var clockspot = _config.P2Banish2SpreadSpots[assignment]; + if (clockspot < 0) + return null; // no assignment + + var assignedDirection = (180 - 45 * clockspot).Degrees(); + if (Stacks.Count > 0 && Stacks[0].Activation > WorldState.FutureTime(1)) + { + var isSupport = assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.OT or PartyRolesConfig.Assignment.H1 or PartyRolesConfig.Assignment.H2; + if (_config.P2Banish2SupportsMoveToStack == isSupport) + assignedDirection += (_config.P2Banish2MoveCCWToStack ? 45 : -45).Degrees(); + return Module.Center + 10 * assignedDirection.ToDirection(); + } + else if (Spreads.Count > 0 && Spreads[0].Activation > WorldState.FutureTime(1)) + { + return Module.Center + 13 * assignedDirection.ToDirection(); + } + return null; + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.BrightHunger) + _allowHints = true; } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs index fa22d4dbb9..479ccbb23e 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs @@ -30,64 +30,18 @@ public override void OnUntethered(Actor source, ActorTetherInfo tether) class P2LuminousHammer(BossModule module) : Components.BaitAwayIcon(module, new AOEShapeCircle(6), (uint)IconID.LuminousHammer, ActionID.MakeSpell(AID.LuminousHammer), 7.1f, true) { - private readonly int[] _baitsPerPlayer = new int[PartyState.MaxPartySize]; - private readonly WPos[] _prevBaitPos = new WPos[PartyState.MaxPartySize]; + public readonly int[] BaitsPerPlayer = new int[PartyState.MaxPartySize]; + public readonly WDir[] PrevBaitOffset = new WDir[PartyState.MaxPartySize]; - private const float FirstBaitOffset = 8; - - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - var bait = ActiveBaitsOn(actor).FirstOrDefault(); - if (bait.Target == null) - return; // no bait, don't care - - switch (_baitsPerPlayer[slot]) - { - case 0: - // position for first bait - var partner = ActiveBaitsNotOn(actor).FirstOrDefault().Target; - if (partner == null) - return; // we can't resolve the hint without knowing the partner - - // logic: - // - if actor and partner are north and south, stay on current side - // - if both are on the same side, the 'more clockwise' one (NE/SW) moves to the opposite side - // TODO: last rule is fuzzy in practice, see if we can adjust better - var north = actor.Position.Z < Module.Center.Z; - if (north == (partner.Position.Z < Module.Center.Z)) - { - // same side, see if we need to swap - var moreRight = actor.Position.X > partner.Position.X; - var moreCW = north == moreRight; - north ^= moreCW; - } - - var preposSpot = Module.Center + new WDir(0, north ? -FirstBaitOffset : FirstBaitOffset); - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(preposSpot, 1), bait.Activation); - break; - case 1: - case 2: - // second/third bait - rotate 45 degrees CW - var secondSpot = Module.Center + (_prevBaitPos[slot] - Module.Center).Rotate(-45.Degrees()); - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(secondSpot, 3)); - break; - case 3: - case 4: - // fourth/fifth bait - move towards N/S wall - var dest = Module.Center + new WDir(0, _prevBaitPos[slot].X > Module.Center.X ? +18 : -18); - var thirdSpot = _prevBaitPos[slot] + 6 * (dest - _prevBaitPos[slot]).Normalized(); - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(thirdSpot, 3)); - break; - } - } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // there are dedicated components for hints public override void OnEventCast(Actor caster, ActorCastEvent spell) { if (spell.Action == WatchedAction && Raid.FindSlot(spell.MainTargetID) is var slot && slot >= 0) { ++NumCasts; - _prevBaitPos[slot] = Raid[slot]?.Position ?? default; - if (++_baitsPerPlayer[slot] == 5) + PrevBaitOffset[slot] = (Raid[slot]?.Position ?? Module.Center) - Module.Center; + if (++BaitsPerPlayer[slot] == 5) CurrentBaits.RemoveAll(b => b.Target == Raid[slot]); // last bait } } @@ -98,17 +52,7 @@ class P2BrightHunger1(BossModule module) : Components.GenericTowers(module, Acti private readonly FRUConfig _config = Service.Config.Get(); private BitMask _forbidden; - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - // if we have one tower assigned, stay inside it, somewhat closer to the edge - var assignedTowerIndex = Towers.FindIndex(t => !t.ForbiddenSoakers[slot]); - if (assignedTowerIndex >= 0 && Towers.FindIndex(assignedTowerIndex + 1, t => !t.ForbiddenSoakers[slot]) < 0) - { - ref var t = ref Towers.Ref(assignedTowerIndex); - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + (t.Position - Module.Center) * 1.125f, 2), t.Activation); // center is at R16, x1.125 == R18 - } - // else: we either have no towers assigned (== doing puddles), or have multiple assigned (== assignments failed), so do nothing - } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // there are dedicated components for hints public override void OnEventIcon(Actor actor, uint iconID, ulong targetID) { @@ -164,18 +108,14 @@ private void RebuildTowers() } // note: we can start showing aoes ~3s earlier if we check spawns, but it's not really needed -class P2HolyLightBurst(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HolyLightBurst), new AOEShapeCircle(11), 3); +class P2HolyLightBurst(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HolyLightBurst), new AOEShapeCircle(11), 3) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // there are dedicated components for hints +} class P2PowerfulLight(BossModule module) : Components.UniformStackSpread(module, 5, 0, 4, 4) { - public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) - { - // TODO: there are several 'stages' here, which need different hints: - // 1. initially (before first holy light burst cast start / last puddle bait, which happen roughly at the same time), we want to stack strictly N/S (unless we are baiting puddles) - // 2. after that, we need to move CW closer to the holy light burst border; if we are stack target, we need to ensure we wait for puddle baiter, otherwise we can just follow stack target very closely - // 3. after this mechanic resolves, we need to dodge HLBs - base.AddAIHints(slot, actor, assignment, hints); - } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // there are dedicated components for hints public override void OnStatusGain(Actor actor, ActorStatus status) { @@ -194,6 +134,8 @@ class P2BrightHunger2(BossModule module) : Components.GenericTowers(module, Acti { private BitMask _forbidden; + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // there are dedicated components for hints + public override void OnStatusGain(Actor actor, ActorStatus status) { if ((SID)status.ID == SID.Lightsteeped && status.Extra >= 3) @@ -219,3 +161,177 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) CurrentBaits.Add(new(caster, p, _shape, Module.CastFinishAt(spell, 0.9f))); } } + +// movement to soak towers and bait first 3 puddles (third puddle is baited right before towers resolve) +class P2LightRampantAITowers(BossModule module) : BossComponent(module) +{ + private readonly P2LuminousHammer? _puddles = module.FindComponent(); + private readonly P2BrightHunger1? _towers = module.FindComponent(); + + private const float BaitOffset = 8; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_puddles == null || _towers == null) + return; + + var bait = _puddles.ActiveBaitsOn(actor).FirstOrDefault(); + if (bait.Target != null) + { + if (_puddles.BaitsPerPlayer[slot] == 0) + { + // position for first bait + var partner = _puddles.ActiveBaitsNotOn(actor).FirstOrDefault().Target; + if (partner == null) + return; // we can't resolve the hint without knowing the partner + + // logic: + // - if actor and partner are north and south, stay on current side + // - if both are on the same side, the 'more clockwise' one (NE/SW) moves to the opposite side + // TODO: last rule is fuzzy in practice, see if we can adjust better + var north = actor.Position.Z < Module.Center.Z; + if (north == (partner.Position.Z < Module.Center.Z)) + { + // same side, see if we need to swap + var moreRight = actor.Position.X > partner.Position.X; + var moreCW = north == moreRight; + north ^= moreCW; + } + + var preposSpot = Module.Center + new WDir(0, north ? -BaitOffset : BaitOffset); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(preposSpot, 1), bait.Activation); + } + else + { + // each next bait is just previous position rotated CW by 45 degrees + // note that this is only really relevant for second and third puddles - after that towers resolve and we use different component + //var nextSpot = Module.Center + BaitOffset * _puddles.PrevBaitOffset[slot].Normalized().Rotate(-45.Degrees()); + //hints.AddForbiddenZone(ShapeDistance.InvertedCircle(nextSpot, 3)); + var shape = ShapeDistance.DonutSector(Module.Center, BaitOffset - 1, BaitOffset + 2, Angle.FromDirection(_puddles.PrevBaitOffset[slot]) - 45.Degrees(), 30.Degrees()); + hints.AddForbiddenZone(p => -shape(p), DateTime.MaxValue); + } + } + else + { + // if we have one tower assigned, stay inside it, somewhat closer to the edge + var assignedTowerIndex = _towers.Towers.FindIndex(t => !t.ForbiddenSoakers[slot]); + if (assignedTowerIndex >= 0 && _towers.Towers.FindIndex(assignedTowerIndex + 1, t => !t.ForbiddenSoakers[slot]) < 0) + { + ref var t = ref _towers.Towers.Ref(assignedTowerIndex); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + (t.Position - Module.Center) * 1.125f, 2), t.Activation); // center is at R16, x1.125 == R18 + } + // else: we either have no towers assigned (== doing puddles), or have multiple assigned (== assignments failed), so do nothing + } + } +} + +// movement to preposition for resolving stacks +class P2LightRampantAIStack(BossModule module) : BossComponent(module) +{ + private readonly P2LuminousHammer? _puddles = module.FindComponent(); + private readonly P2PowerfulLight? _stack = module.FindComponent(); + private readonly P2HolyLightBurst? _orbs = module.FindComponent(); + + public const float Radius = 18; + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_puddles == null || _stack == null || _orbs == null) + return; + + // initially we don't know whether first orbs will cover N/S, puddle baiter is still relatively far away and has two baits left + // when orbs start, baiter has just finished his puddles; he can still be relatively far away from N/S center, so we might need to wait for him + var northCamp = IsNorthCamp(actor); + var startingDir = (northCamp ? 180 : 0).Degrees(); + var startingPos = Module.Center + new WDir(0, northCamp ? -Radius : Radius); + if (_puddles.ActiveBaits.Any()) + { + // just move to starting position, until all puddles are resolved + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(startingPos, 1), DateTime.MaxValue); + return; + } + + var isStackTarget = _stack.IsStackTarget(actor); + var haveOrbs = _orbs.Casters.Count > 0; + var centerDangerous = haveOrbs && _orbs.ActiveCasters.Any(c => actor.Position.Z - Module.Center.Z is var off && (northCamp ? off < -15 : off > 15)); + var idealDestDir = startingDir - (centerDangerous ? 40 : 20).Degrees(); // alt: haveOrbs ? 20 : 30 (but i don't think it's how people really move...) + var idealPos = Module.Center + Radius * idealDestDir.ToDirection(); + + if (isStackTarget) + { + // as a stack target, our responsibility is to wait for everyone to stack up, then carefully move towards ideal dir + // note that we need to be careful to avoid oscillations + var toIdeal = idealPos - actor.Position; + foreach (var partner in Raid.WithoutSlot().Exclude(actor).Where(p => IsNorthCamp(p) == northCamp)) + { + var toPartner = partner.Position - actor.Position; + var distSq = toPartner.LengthSq(); + if (distSq > 9 && toIdeal.Dot(toPartner) < 0) + { + // partner is far enough away, and moving towards ideal pos will not bring us closer => just stay where we are + return; + } + } + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(idealPos, 1), DateTime.MaxValue); + } + else if (_stack.Stacks.FirstOrDefault(s => IsNorthCamp(s.Target) == northCamp).Target is var stackTarget && stackTarget != null) + { + // otherwise we just want to stay close to the stack target, slightly offset to the ideal position + var dirToIdeal = idealPos - stackTarget.Position; + var dest = dirToIdeal.LengthSq() <= 4 ? idealPos : stackTarget.Position + 2 * dirToIdeal.Normalized(); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(dest, 1), DateTime.MaxValue); + } + } + + private bool IsNorthCamp(Actor actor) => (_puddles?.ActiveBaitsOn(actor).Any() ?? false) ? actor.Position.X < Module.Center.X : actor.Position.Z < Module.Center.Z; +} + +// movement to dodge orbs after resolving stack +class P2LightRampantAIOrbs(BossModule module) : BossComponent(module) +{ + private readonly P2HolyLightBurst? _orbs = module.FindComponent(); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_orbs == null || _orbs.Casters.Count == 0) + return; + + if (_orbs.NumCasts == 0) + { + // dodge first orbs, while staying near edge + hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 16)); + } + else + { + // dodge second orbs, while trying to come closer to the center + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 6), DateTime.MaxValue); + } + + // actual orb aoes + foreach (var c in _orbs.ActiveCasters) + hints.AddForbiddenZone(_orbs.Shape.Distance(c.Position, default), Module.CastFinishAt(c.CastInfo, -1)); + } +} + +// movement to soak central tower (if needed) and preposition for banish +class P2LightRampantAIResolve(BossModule module) : BossComponent(module) +{ + private readonly FRUConfig _config = Service.Config.Get(); + private readonly P2BrightHunger2? _tower = module.FindComponent(); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_tower == null || _tower.Towers.Count == 0) + return; // no towers + + ref var t = ref _tower.Towers.Ref(0); + hints.AddForbiddenZone(t.ForbiddenSoakers[slot] ? ShapeDistance.Circle(t.Position, t.Radius) : ShapeDistance.InvertedCircle(t.Position, t.Radius), t.Activation); + + var clockspot = _config.P2Banish2SpreadSpots[assignment]; + if (clockspot >= 0) + { + var assignedDirection = (180 - 45 * clockspot).Degrees(); + hints.AddForbiddenZone(ShapeDistance.InvertedCone(t.Position, 50, assignedDirection, 30.Degrees()), DateTime.MaxValue); + } + } +} From a3657e77575bc513e976a33341d9ad96056a5a58 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Sat, 11 Jan 2025 02:53:00 +0100 Subject: [PATCH 3/4] BA Raiden module, Chaotic tile setting, random other changes --- BossMod/BossModule/AOEShapes.cs | 2 +- BossMod/BossModule/ArenaBounds.cs | 2 + BossMod/BossModule/BossModuleInfo.cs | 1 + BossMod/BossModule/Shapes.cs | 48 ++--- BossMod/BossModule/ZoneModule.cs | 2 +- BossMod/Components/Exaflare.cs | 2 +- BossMod/Components/Gaze.cs | 6 +- BossMod/Components/GenericAOEs.cs | 30 ++- BossMod/Config/ModuleViewer.cs | 2 + .../ActivePivotParticleBeam.cs | 2 +- .../Chaotic/Ch01CloudOfDarkness/Break.cs | 2 +- .../Ch01CloudOfDarknessConfig.cs | 8 + .../Ch01CloudOfDarknessEnums.cs | 4 +- .../Ch01CloudOfDarknessStates.cs | 4 +- .../Ch01CloudOfDarkness/DelugeOfDarkness.cs | 18 +- .../ParticleConcentration.cs | 2 +- .../D03SkydeepCenote/D031FeatherRay.cs | 5 +- .../Dungeon/D03SkydeepCenote/D033Maulskull.cs | 9 +- .../Dungeon/D06Alexandria/D063Eliminator.cs | 12 +- .../Ex3QueenEternal/Ex3QueenEternalConfig.cs | 2 +- .../E1OtisOathbroken.cs | 32 ++- .../TheProtectorAndTheDestroyer/E2Gwyddrud.cs | 143 ++++++++---- BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs | 10 +- .../Dawntrail/Ultimate/FRU/FRUEnums.cs | 3 +- .../Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs | 10 +- .../Dawntrail/Ultimate/FRU/P1Explosion.cs | 30 +-- .../Dawntrail/Ultimate/FRU/P1FallOfFaith.cs | 2 +- .../Dawntrail/Ultimate/FRU/P1UtopianSky.cs | 6 +- .../Dawntrail/Ultimate/FRU/P2DiamondDust.cs | 12 +- .../Dawntrail/Ultimate/FRU/P3Apocalypse.cs | 10 +- .../Ultimate/FRU/P3UltimateRelativity.cs | 16 +- .../Ultimate/FRU/P4CrystallizeTime.cs | 20 +- .../Ultimate/FRU/P4DarklitDragonsong.cs | 4 +- .../Extreme/Ex7Zeromus/Ex7ZeromusStates.cs | 5 +- .../Foray/BaldesionsArsenal/BA1Art/BA1Art.cs | 7 +- .../BaldesionsArsenal/BA1Art/BA1ArtEnums.cs | 4 +- .../BaldesionsArsenal/BA1Art/BA1ArtStates.cs | 7 +- .../BA1Art/LegendMythSpinnerCarver.cs | 2 +- .../BaldesionsArsenal/BA1Owain/BA1Owain.cs | 46 +--- .../BA1Owain/BA1OwainEnums.cs | 8 +- .../BA1Owain/BA1OwainStates.cs | 38 ++-- .../BA1Owain/ElementalMagicks.cs | 40 ++++ .../BaldesionsArsenal/BA1Owain/IvoryPalm.cs | 2 +- .../BaldesionsArsenal/BA1Owain/Spiritcull.cs | 2 +- .../BA2Raiden/ArenaChange.cs | 27 +++ .../BaldesionsArsenal/BA2Raiden/BA2Raiden.cs | 59 +++++ .../BA2Raiden/BA2RaidenEnums.cs | 50 +++++ .../BA2Raiden/BA2RaidenStates.cs | 204 ++++++++++++++++++ .../BA2Raiden/CloudToGround.cs | 26 +++ .../BA2Raiden/LancingBlow.cs | 33 +++ BossMod/Replay/Analysis/StatusInfo.cs | 2 +- BossMod/Replay/Visualization/OpList.cs | 7 +- BossMod/Util/WPosDir.cs | 12 ++ 53 files changed, 782 insertions(+), 260 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessConfig.cs create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/ElementalMagicks.cs create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/ArenaChange.cs create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2Raiden.cs create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2RaidenEnums.cs create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2RaidenStates.cs create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/CloudToGround.cs create mode 100644 BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/LancingBlow.cs diff --git a/BossMod/BossModule/AOEShapes.cs b/BossMod/BossModule/AOEShapes.cs index b398611002..f2f42f054d 100644 --- a/BossMod/BossModule/AOEShapes.cs +++ b/BossMod/BossModule/AOEShapes.cs @@ -99,7 +99,7 @@ public override Func Distance(WPos origin, Angle rotation) public sealed record class AOEShapeCross(float Length, float HalfWidth, Angle DirectionOffset = default, bool InvertForbiddenZone = false) : AOEShape { public override string ToString() => $"Cross: l={Length:f3}, w={HalfWidth * 2}, off={DirectionOffset}, ifz={InvertForbiddenZone}"; - public override bool Check(WPos position, WPos origin, Angle rotation) => position.InRect(origin, rotation + DirectionOffset, Length, Length, HalfWidth) || position.InRect(origin, rotation + DirectionOffset, HalfWidth, HalfWidth, Length); + public override bool Check(WPos position, WPos origin, Angle rotation) => position.InCross(origin, rotation + DirectionOffset, Length, HalfWidth); public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = 0) => arena.ZonePoly((GetType(), origin, rotation + DirectionOffset, Length, HalfWidth), ContourPoints(origin, rotation), color); public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = 0) { diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index df62fee713..7070549766 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -362,6 +362,8 @@ private Pathfinding.Map BuildMap() public sealed record class ArenaBoundsComplex : ArenaBoundsCustom { public readonly WPos Center; + public bool IsCircle; // can be used by gaze component for gazes outside of the arena + public ArenaBoundsComplex(Shape[] UnionShapes, Shape[]? DifferenceShapes = null, Shape[]? AdditionalShapes = null, float MapResolution = Half, float Offset = 0, float ScaleFactor = 1) : base(BuildBounds(UnionShapes, DifferenceShapes, AdditionalShapes, MapResolution, Offset, ScaleFactor, out var center, out var halfWidth, out var halfHeight)) { diff --git a/BossMod/BossModule/BossModuleInfo.cs b/BossMod/BossModule/BossModuleInfo.cs index 3c07066cf1..000a9e867d 100644 --- a/BossMod/BossModule/BossModuleInfo.cs +++ b/BossMod/BossModule/BossModuleInfo.cs @@ -59,6 +59,7 @@ public enum GroupType CFC, // group id is ContentFinderCondition row MaskedCarnivale, // group id is ContentFinderCondition row RemovedUnreal, // group id is ContentFinderCondition row + BaldesionArsenal, // group id is ContentFinderCondition row Quest, // group id is Quest row Fate, // group id is Fate row Hunt, // group id is HuntRank diff --git a/BossMod/BossModule/Shapes.cs b/BossMod/BossModule/Shapes.cs index 13822bdff3..d8192cd673 100644 --- a/BossMod/BossModule/Shapes.cs +++ b/BossMod/BossModule/Shapes.cs @@ -25,40 +25,39 @@ public override List Contour(WPos center) result.Add(Points[i] + offset); return result; } + public override string ToString() => $"Circle:{Center.X},{Center.Z},{Radius}"; } -// for custom polygons defined by an IEnumerable of vertices -public sealed record class PolygonCustom(IEnumerable Vertices) : Shape +// for custom polygons defined by an IReadOnlyList of vertices +public sealed record class PolygonCustom(IReadOnlyList Vertices) : Shape { - private readonly WPos[] points = [.. Vertices]; public override List Contour(WPos center) { - var len = points.Length; - var result = new List(len); - for (var i = 0; i < len; ++i) - result.Add(points[i] - center); + var count = Vertices.Count; + var result = new List(count); + for (var i = 0; i < count; ++i) + result.Add(Vertices[i] - center); return result; } public override string ToString() { - var len = points.Length; - var sb = new StringBuilder($"PolygonCustom:"); - for (var i = 0; i < len; ++i) + var count = Vertices.Count; + var sb = new StringBuilder("PolygonCustom:", 14 + count * 15); + for (var i = 0; i < count; ++i) { - sb.Append(points[i].X).Append(',').Append(points[i].Z); - if (i < len - 1) - sb.Append(','); + var vertex = Vertices[i]; + sb.Append(vertex.X).Append(',').Append(vertex.Z).Append(';'); } + --sb.Length; return sb.ToString(); } } -// for custom polygons defined by an IEnumerable of vertices with an offset, eg to account for hitbox radius -public sealed record class PolygonCustomO(IEnumerable Vertices, float Offset) : Shape +// for custom polygons defined by an IReadOnlyList of vertices with an offset, eg to account for hitbox radius +public sealed record class PolygonCustomO(IReadOnlyList Vertices, float Offset) : Shape { - private readonly WPos[] points = [.. Vertices]; private Path64? path; public override List Contour(WPos center) @@ -66,9 +65,9 @@ public override List Contour(WPos center) if (path == null) { var originalPath = new Path64(); - for (var i = 0; i < points.Length; ++i) + for (var i = 0; i < Vertices.Count; ++i) { - var v = points[i]; + var v = Vertices[i]; originalPath.Add(new Point64((long)(v.X * PolygonClipper.Scale), (long)(v.Z * PolygonClipper.Scale))); } @@ -93,15 +92,14 @@ public override List Contour(WPos center) public override string ToString() { - var len = points.Length; - var sb = new StringBuilder($"PolygonCustomO:"); - for (var i = 0; i < len; ++i) + var count = Vertices.Count; + var sb = new StringBuilder("PolygonCustomO:", 15 + count * 15); + for (var i = 0; i < count; ++i) { - sb.Append(points[i].X).Append(',').Append(points[i].Z); - if (i < len - 1) - sb.Append(','); + var vertex = Vertices[i]; + sb.Append(vertex.X).Append(',').Append(vertex.Z).Append(';'); } - sb.Append(',').Append(Offset); + sb.Append("Offset:").Append(Offset); return sb.ToString(); } } diff --git a/BossMod/BossModule/ZoneModule.cs b/BossMod/BossModule/ZoneModule.cs index 0c63d1939f..29aad5ab6d 100644 --- a/BossMod/BossModule/ZoneModule.cs +++ b/BossMod/BossModule/ZoneModule.cs @@ -25,7 +25,7 @@ public virtual void DrawExtra() { } public void DrawGlobalHints() { - using var color = ImRaii.PushColor(ImGuiCol.Text, 0xffffff00); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TextColor11); foreach (var hint in CalculateGlobalHints()) { ImGui.TextUnformatted(hint); diff --git a/BossMod/Components/Exaflare.cs b/BossMod/Components/Exaflare.cs index 2ed77bc9b9..4a2e8691ca 100644 --- a/BossMod/Components/Exaflare.cs +++ b/BossMod/Components/Exaflare.cs @@ -17,7 +17,7 @@ public class Line public readonly AOEShape Shape = shape; public uint ImminentColor = Colors.Danger; public uint FutureColor = Colors.AOE; - protected readonly List Lines = []; + public readonly List Lines = []; public bool Active => Lines.Count > 0; diff --git a/BossMod/Components/Gaze.cs b/BossMod/Components/Gaze.cs index 8f780811d6..209c250e67 100644 --- a/BossMod/Components/Gaze.cs +++ b/BossMod/Components/Gaze.cs @@ -74,14 +74,10 @@ public static void DrawEye(Vector2 eyeCenter, bool danger) private Vector2 IndicatorScreenPos(WPos eye) { - if (Arena.InBounds(eye) || Arena.Bounds is not ArenaBoundsCircle and not ArenaBoundsSquare) + if (Arena.InBounds(eye) || Arena.Bounds is not ArenaBoundsCircle && Arena.Bounds is ArenaBoundsComplex circle && !circle.IsCircle) { return Arena.WorldPositionToScreenPosition(eye); } - else if (Arena.Bounds is ArenaBoundsRect) - { - return Arena.WorldPositionToScreenPosition(Arena.ClampToBounds(eye) + 2 * (eye - Arena.Center).Normalized()); - } else { var dir = (eye - Arena.Center).Normalized(); diff --git a/BossMod/Components/GenericAOEs.cs b/BossMod/Components/GenericAOEs.cs index d6616e9799..8a5aa6da2f 100644 --- a/BossMod/Components/GenericAOEs.cs +++ b/BossMod/Components/GenericAOEs.cs @@ -3,7 +3,7 @@ // generic component that shows arbitrary shapes representing avoidable aoes public abstract class GenericAOEs(BossModule module, ActionID aid = default, string warningText = "GTFO from aoe!") : CastCounter(module, aid) { - public record struct AOEInstance(AOEShape Shape, WPos Origin, Angle Rotation = default, DateTime Activation = default, uint Color = 0, bool Risky = true) + public record struct AOEInstance(AOEShape Shape, WPos Origin, Angle Rotation = default, DateTime Activation = default, uint Color = 0, bool Risky = true, ulong? ActorID = null) { public readonly bool Check(WPos pos) => Shape.Check(pos, Origin, Rotation); } @@ -14,8 +14,14 @@ public record struct AOEInstance(AOEShape Shape, WPos Origin, Angle Rotation = d public override void AddHints(int slot, Actor actor, TextHints hints) { - if (ActiveAOEs(slot, actor).Any(c => c.Risky && c.Check(actor.Position))) - hints.Add(WarningText); + foreach (var aoe in ActiveAOEs(slot, actor)) + { + if (aoe.Risky && aoe.Check(actor.Position)) + { + hints.Add(WarningText); + break; + } + } } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) @@ -106,7 +112,7 @@ public class LocationTargetedAOEs(BossModule module, ActionID aid, AOEShape shap public uint Color; // can be customized if needed public bool Risky = true; // can be customized if needed public readonly bool TargetIsLocation = targetIsLocation; // can be customized if needed - public float RiskyWithSecondsLeft = riskyWithSecondsLeft; // can be used to delay risky status of AOEs, so AI waits longer to dodge, if 0 it will just use the bool Risky + public readonly float RiskyWithSecondsLeft = riskyWithSecondsLeft; // can be used to delay risky status of AOEs, so AI waits longer to dodge, if 0 it will just use the bool Risky public readonly List Casters = []; public IEnumerable ActiveCasters => Casters.Take(MaxCasts); @@ -127,13 +133,23 @@ public override IEnumerable ActiveAOEs(int slot, Actor actor) public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if (spell.Action == WatchedAction) - Casters.Add(new(Shape, (TargetIsLocation ? WorldState.Actors.Find(caster.CastInfo!.TargetID)?.Position : spell.LocXZ) ?? default, spell.Rotation, Module.CastFinishAt(spell), Color, Risky)); + Casters.Add(new(Shape, (TargetIsLocation ? WorldState.Actors.Find(caster.CastInfo!.TargetID)?.Position : spell.LocXZ) ?? default, spell.Rotation, Module.CastFinishAt(spell), Color, Risky, caster.InstanceID)); } public override void OnCastFinished(Actor caster, ActorCastInfo spell) { - if (Casters.Count != 0 && spell.Action == WatchedAction) - Casters.RemoveAt(0); + if (spell.Action == WatchedAction) + { + for (var i = 0; i < Casters.Count; ++i) + { + var aoe = Casters[i]; + if (aoe.ActorID == caster.InstanceID) + { + Casters.Remove(aoe); + break; + } + } + } } } diff --git a/BossMod/Config/ModuleViewer.cs b/BossMod/Config/ModuleViewer.cs index 82c01419a0..d69a5ceada 100644 --- a/BossMod/Config/ModuleViewer.cs +++ b/BossMod/Config/ModuleViewer.cs @@ -269,6 +269,8 @@ private void DrawModules(UITree tree, WorldState ws) return (new(mcName, groupId, mcSort), new(module, BNpcName(module.NameID), module.SortOrder)); case BossModuleInfo.GroupType.RemovedUnreal: return (new("Removed Content", groupId, groupId), new(module, BNpcName(module.NameID), module.SortOrder)); + case BossModuleInfo.GroupType.BaldesionArsenal: + return (new("Baldesion Arsenal", groupId, groupId), new(module, BNpcName(module.NameID), module.SortOrder)); case BossModuleInfo.GroupType.Quest: var questRow = Service.LuminaRow(module.GroupID)!.Value; groupId |= questRow.JournalGenre.RowId; diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs index 6052f981bf..707b8b86c5 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ActivePivotParticleBeam.cs @@ -18,7 +18,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if ((AID)spell.Action.ID == AID.ActivePivotParticleBeamAOE && Sequences.Count > 0) + if ((AID)spell.Action.ID == AID.ActivePivotParticleBeamAOE) AdvanceSequence(0, WorldState.CurrentTime); } } diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs index 75c7dc1f3c..a160cc165e 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Break.cs @@ -2,7 +2,7 @@ class Break(BossModule module) : Components.GenericGaze(module) { - public readonly List Eyes = []; + public readonly List Eyes = new(3); public override IEnumerable ActiveEyes(int slot, Actor actor) => Eyes; diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessConfig.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessConfig.cs new file mode 100644 index 0000000000..78e5ab0019 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessConfig.cs @@ -0,0 +1,8 @@ +namespace BossMod.Dawntrail.Chaotic.Ch01CloudOfDarkness; + +[ConfigDisplay(Order = 0x020, Parent = typeof(DawntrailConfig))] +class Ch01CloudOfDarknessConfig() : ConfigNode() +{ + [PropertyDisplay("Show occupied tiles on radar", tooltip: "Required for AI to not step on occupied tiles.")] + public bool ShowOccupiedTiles = true; +} diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs index e7def47810..b789d6c313 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessEnums.cs @@ -3,7 +3,6 @@ public enum OID : uint { Boss = 0x461E, // R23.000, x1 - Helper = 0x233C, // R0.500, x24, Helper type StygianShadow = 0x461F, // R4.000, x0 (spawn during fight), big add Atomos = 0x4620, // R2.800, x0 (spawn during fight), small add DeathsHand = 0x4621, // R2.000, x6, grim embrace hand @@ -14,6 +13,7 @@ public enum OID : uint SinisterEye = 0x4626, // R2.800, x2, break gaze source AtomosSpawnPoint = 0x1EBD7B, // R0.500, x0 (spawn during fight), EventObj type EvilSeed = 0x1E9B3B, // R0.500, x0 (spawn during fight), EventObj type + Helper = 0x233C } public enum AID : uint @@ -132,7 +132,7 @@ public enum AID : uint Evaporation = 40454, // StygianShadow->Boss, 2.0s cast, single-target, destroy add and transfer damage done to boss FloodOfDarkness2 = 40455, // Boss->location, 7.0s cast, range 60 circle, raidwide + arena transition to normal - Enrage = 40533, // Boss->location, 12.0s cast, range 100 circle, enrage + Enrage = 40533 // Boss->location, 12.0s cast, range 100 circle, enrage } public enum SID : uint diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessStates.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessStates.cs index b0d67c2f96..e029602237 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessStates.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/Ch01CloudOfDarknessStates.cs @@ -34,7 +34,7 @@ private void Fork1(uint id) ComponentCondition(id + 0x410000, 4, comp => comp.Casters.Count > 0); Subphase1Variant2End(id + 0x410000, 8); - Cast(id + 0x500000, AID.Enrage, 8.1f, 18.2f, "Enrage"); + Cast(id + 0x500000, AID.Enrage, 8.1f, 12, "Enrage"); } private void Fork2(uint id) @@ -47,7 +47,7 @@ private void Fork2(uint id) ComponentCondition(id + 0x410000, 4, comp => comp.Casters.Count > 0); Subphase1Variant1End(id + 0x410000, 6.1f); - Cast(id + 0x500000, AID.Enrage, 8.7f, 18.2f, "Enrage"); + Cast(id + 0x500000, AID.Enrage, 8.1f, 12, "Enrage"); } private void Subphase1Variant1End(uint id, float delay) diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DelugeOfDarkness.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DelugeOfDarkness.cs index fe5eed39f6..879de3c527 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DelugeOfDarkness.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/DelugeOfDarkness.cs @@ -7,23 +7,28 @@ // - 00040004 - remove telegraph (note that actual bounds are controlled by something else!) class Phase2InnerCells(BossModule module) : Components.GenericAOEs(module) { + private readonly Ch01CloudOfDarknessConfig _config = Service.Config.Get(); 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) { + if (!_config.ShowOccupiedTiles) + yield break; var cell = CellIndex(actor.Position - Arena.Center) - 3; for (var i = 0; i < 28; ++i) { - if (i == cell) + if (_breakTime[i] != default) { - if (Math.Max(0, (_breakTime[i] - WorldState.CurrentTime).TotalSeconds) < 10) - yield return new(square, CellCenter(i)); - continue; + if (i == cell) + { + if (Math.Max(0, (_breakTime[i] - WorldState.CurrentTime).TotalSeconds) < 6) + yield return new(square, CellCenter(i)); + } + else + yield return new(square, CellCenter(i), Color: Colors.FutureVulnerable); } - else if (_breakTime[i] != default) - yield return new(square, CellCenter(i), Color: Colors.FutureVulnerable); } } @@ -62,6 +67,7 @@ public override void OnEventEnvControl(byte index, uint state) { 0x00200010 => WorldState.FutureTime(44), 0x00800040 => WorldState.FutureTime(6), + 0x00080004 => WorldState.CurrentTime, _ => default, }; } diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ParticleConcentration.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ParticleConcentration.cs index c1d932f7d6..9ec6f53e61 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ParticleConcentration.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/ParticleConcentration.cs @@ -143,7 +143,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) if ((AID)spell.Action.ID is AID.ParticleBeam1 or AID.ParticleBeam2 or AID.ParticleBeam3) { ++NumCasts; - if (Towers.RemoveAll(t => t.Position.AlmostEqual(caster.Position, 1)) != 1) + if (Towers.RemoveAll(t => t.Position == caster.Position) != 1) ReportError($"Unexpected tower position @ {caster.Position}"); } } diff --git a/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D031FeatherRay.cs b/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D031FeatherRay.cs index 969fa552ec..1fbc78abcc 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D031FeatherRay.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D031FeatherRay.cs @@ -187,8 +187,9 @@ public D031FeatherRayStates(BossModule module) : base(module) } [ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus, LTS)", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 829, NameID = 12755)] -public class D031FeatherRay(WorldState ws, Actor primary) : BossModule(ws, primary, new(-105, -160), NormalBounds) +public class D031FeatherRay(WorldState ws, Actor primary) : BossModule(ws, primary, arenaCenter, NormalBounds) { + private static readonly WPos arenaCenter = new(-105, -160); public static readonly ArenaBoundsSquare NormalBounds = new(15.5f); - public static readonly ArenaBoundsCircle CircleBounds = new(12); + public static readonly ArenaBoundsComplex CircleBounds = new([new Polygon(arenaCenter, 12, 48)]); } diff --git a/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D033Maulskull.cs b/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D033Maulskull.cs index d90061cc87..7caf4a5eb9 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D033Maulskull.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D03SkydeepCenote/D033Maulskull.cs @@ -48,7 +48,8 @@ public enum AID : uint DeepThunderVisual1 = 36687, // Boss->self, 6.0s cast, single-target DeepThunderVisual2 = 36691, // Boss->self, no cast, single-target DeepThunderVisual3 = 36692, // Boss->self, no cast, single-target - DeepThunder2 = 36690, // Helper->self, no cast, range 6 circle + DeepThunderRepeat = 36690, // Helper->self, no cast, range 6 circle + BigBurst = 36693, // Helper->self, no cast, range 60 circle, tower fail RingingBlows1 = 36694, // Boss->self, 7.0+2.0s cast, single-target RingingBlows2 = 36695, // Boss->self, 7.0+2.0s cast, single-target @@ -210,14 +211,14 @@ class DestructiveHeat(BossModule module) : Components.SpreadFromCastTargets(modu public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (ActiveSpreads.Count != 0) + if (Spreads.Count != 0) { var source1 = _kb1.Sources(slot, actor).FirstOrDefault(); var source2 = _kb2.Sources(slot, actor).FirstOrDefault(); var source3 = _kb3.Sources(slot, actor).FirstOrDefault(); var knockback = source1 != default || source2 != default || source3 != default; if (source1 != default) - origin = actor.Role is Role.Melee or Role.Ranged ? new(100, -400) : source1.Origin; + origin = new(100, -400); else if (source2 != default) origin = source2.Origin; else if (source3 != default) @@ -225,7 +226,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (!knockback) { base.AddAIHints(slot, actor, assignment, hints); - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(origin, 15), ActiveSpreads.FirstOrDefault().Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(origin, 15), Spreads[0].Activation); } else { } diff --git a/BossMod/Modules/Dawntrail/Dungeon/D06Alexandria/D063Eliminator.cs b/BossMod/Modules/Dawntrail/Dungeon/D06Alexandria/D063Eliminator.cs index 3a996deb89..032dc9a268 100644 --- a/BossMod/Modules/Dawntrail/Dungeon/D06Alexandria/D063Eliminator.cs +++ b/BossMod/Modules/Dawntrail/Dungeon/D06Alexandria/D063Eliminator.cs @@ -124,10 +124,14 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { public override IEnumerable ActiveAOEs(int slot, Actor actor) { - var aoes = ActiveCasters.Select((c, index) => - new AOEInstance(Shape, c.Position, c.CastInfo!.Rotation, Module.CastFinishAt(c.CastInfo), - index < 2 ? Colors.Danger : Colors.AOE, index < 4)); - + var count = Casters.Count; + var aoes = new List(count); + for (var i = 0; i < count; ++i) + { + var caster = Casters[i]; + var instance = new AOEInstance(Shape, caster.Position, caster.CastInfo!.Rotation, Module.CastFinishAt(caster.CastInfo), i < 2 ? Colors.Danger : Colors.AOE, i < 4); + aoes.Add(instance); + } return aoes; } } diff --git a/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs b/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs index fe43c19c2e..9a502c754e 100644 --- a/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs +++ b/BossMod/Modules/Dawntrail/Extreme/Ex3QueenEternal/Ex3QueenEternalConfig.cs @@ -1,6 +1,6 @@ namespace BossMod.Dawntrail.Extreme.Ex3QueenEternal; -[ConfigDisplay(Order = 0x030, Parent = typeof(DawntrailConfig))] +[ConfigDisplay(Order = 0x010, Parent = typeof(DawntrailConfig))] class Ex3QueenEternalConfig() : ConfigNode() { [PropertyDisplay("Absolute Authority: ignore flares, stack together")] diff --git a/BossMod/Modules/Dawntrail/Quest/MSQ/TheProtectorAndTheDestroyer/E1OtisOathbroken.cs b/BossMod/Modules/Dawntrail/Quest/MSQ/TheProtectorAndTheDestroyer/E1OtisOathbroken.cs index cc6e355829..90b4fcfe44 100644 --- a/BossMod/Modules/Dawntrail/Quest/MSQ/TheProtectorAndTheDestroyer/E1OtisOathbroken.cs +++ b/BossMod/Modules/Dawntrail/Quest/MSQ/TheProtectorAndTheDestroyer/E1OtisOathbroken.cs @@ -13,10 +13,12 @@ public enum OID : uint public enum AID : uint { - AutoAttack = 870, // Boss->tank, no cast, single-target + AutoAttack1 = 870, // Boss->tank, no cast, single-target AutoAttack2 = 872, // EverkeepAerostat2->tank, no cast, single-target AutoAttack3 = 28538, // EverkeepTurret->tank, no cast, single-target AutoAttack4 = 36403, // EverkeepSentryR10->tank, no cast, single-target + AutoAttack5 = 873, // EverkeepSentryG10->tank, no cast, single-target + Teleport = 38193, // Boss->location, no cast, single-target FormationAlpha = 38194, // Boss->self, 5.0s cast, single-target ThrownFlames = 38205, // EverkeepAerostat2->self, 6.0s cast, range 8 circle @@ -40,25 +42,33 @@ public enum AID : uint class StormlitShockwave(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.StormlitShockwave)); class ValorousAscension(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.ValorousAscension)); class RendPower(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RendPower), new AOEShapeCone(40, 15.Degrees()), 6); -class ThrownFlames(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ThrownFlames), new AOEShapeCircle(8)); + class BastionBreaker(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.BastionBreaker), 6); class HolyBlade(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.HolyBlade), 6); -class SearingSlash(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SearingSlash), new AOEShapeCircle(8)); + +abstract class Circle8(BossModule module, AID aid) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(aid), new AOEShapeCircle(8)); +class SearingSlash(BossModule module) : Circle8(module, AID.SearingSlash); +class ThrownFlames(BossModule module) : Circle8(module, AID.ThrownFlames); + class Electrobeam(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Electrobeam), new AOEShapeRect(40, 2)); class Rush(BossModule module) : Components.GenericAOEs(module) { - private readonly List _aoes = []; + private readonly List _aoes = new(8); public override IEnumerable ActiveAOEs(int slot, Actor actor) { var count = _aoes.Count; - if (count > 0) - for (var i = 0; i < Math.Clamp(count, 0, 2); ++i) - yield return _aoes[i] with { Color = Colors.Danger }; - if (count > 2) - for (var i = 2; i < 4; ++i) - yield return _aoes[i]; + if (count == 0) + yield break; + for (var i = 0; i < (count > 4 ? 4 : count); ++i) + { + var aoe = _aoes[i]; + if (i <= 1) + yield return count > 2 ? aoe with { Color = Colors.Danger } : aoe; + else + yield return aoe with { Risky = false }; + } } public override void OnCastStarted(Actor caster, ActorCastInfo spell) @@ -99,7 +109,7 @@ public OtisOathbrokenStates(BossModule module) : base(module) [ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.Quest, GroupID = 70478, NameID = 13168)] public class OtisOathbroken(WorldState ws, Actor primary) : BossModule(ws, primary, new(349, -14), new ArenaBoundsCircle(19.5f)) { - protected override bool CheckPull() => Raid.WithoutSlot().Any(x => x.InCombat); + protected override bool CheckPull() => Raid.Player()!.InCombat; private static readonly uint[] all = [(uint)OID.Boss, (uint)OID.EverkeepTurret, (uint)OID.EverkeepAerostat, (uint)OID.EverkeepAerostat2, (uint)OID.EverkeepSentryG10, (uint)OID.EverkeepSentryR10]; diff --git a/BossMod/Modules/Dawntrail/Quest/MSQ/TheProtectorAndTheDestroyer/E2Gwyddrud.cs b/BossMod/Modules/Dawntrail/Quest/MSQ/TheProtectorAndTheDestroyer/E2Gwyddrud.cs index 3ec96d2b4d..46e11efa71 100644 --- a/BossMod/Modules/Dawntrail/Quest/MSQ/TheProtectorAndTheDestroyer/E2Gwyddrud.cs +++ b/BossMod/Modules/Dawntrail/Quest/MSQ/TheProtectorAndTheDestroyer/E2Gwyddrud.cs @@ -40,29 +40,35 @@ public enum AID : uint UntamedCurrentAOE5 = 38234, // Helper2->location, 3.1s cast, range 5 circle UntamedCurrentAOE6 = 19720, // Helper2->location, 3.2s cast, range 5 circle UntamedCurrentAOE7 = 19721, // Helper2->location, 3.3s cast, range 5 circle - UntamedCurrentAOE8 = 19728, // Helper2->location, 3.3s cast, range 5 circle - UntamedCurrentAOE9 = 19727, // Helper2->location, 3.2s cast, range 5 circle - UntamedCurrentAOE10 = 19179, // Helper2->location, 3.1s cast, range 5 circle + UntamedCurrentAOE8 = 19728, // Helper2->location, 3.3s cast, range 5 circle (outside arena) + UntamedCurrentAOE9 = 19727, // Helper2->location, 3.2s cast, range 5 circle (outside arena) + UntamedCurrentAOE10 = 19179, // Helper2->location, 3.1s cast, range 5 circle (outside arena) UntamedCurrentSpread = 19181, // Helper->all, 5.0s cast, range 5 circle UntamedCurrentStack = 19276, // Helper->Alisaie, 5.0s cast, range 6 circle } class Gnaw(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Gnaw)); class CracklingHowl(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.CracklingHowl)); -class UntamedCurrent(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.UntamedCurrent)); +class UntamedCurrentRaidwide(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.UntamedCurrent)); class VioletVoltage(BossModule module) : Components.GenericAOEs(module) { - private readonly List _aoes = []; + private readonly List _aoes = new(4); private static readonly AOEShapeCone cone = new(20, 90.Degrees()); public override IEnumerable ActiveAOEs(int slot, Actor actor) { var count = _aoes.Count; - if (count > 0) - yield return _aoes[0] with { Color = Colors.Danger }; - if (count > 1) - yield return _aoes[1] with { Risky = _aoes[1].Rotation.AlmostEqual(_aoes[0].Rotation + 180.Degrees(), Angle.DegToRad) }; + if (count == 0) + yield break; + for (var i = 0; i < (count > 2 ? 2 : count); ++i) + { + var aoe = _aoes[i]; + if (i == 0) + yield return count != 1 ? aoe with { Color = Colors.Danger } : aoe; + else + yield return aoe with { Risky = !aoe.Rotation.AlmostEqual(_aoes[0].Rotation + 180.Degrees(), Angle.DegToRad) }; + } } public override void OnCastStarted(Actor caster, ActorCastInfo spell) @@ -73,50 +79,112 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if (_aoes.Count > 0 && (AID)spell.Action.ID == AID.VioletVoltage) + if (_aoes.Count != 0 && (AID)spell.Action.ID == AID.VioletVoltage) _aoes.RemoveAt(0); } } class RoaringBoltKB(BossModule module) : Components.KnockbackFromCastTarget(module, ActionID.MakeSpell(AID.RoaringBoltKB), 12, stopAtWall: true) { - public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) => Module.FindComponent()?.ActiveAOEs(slot, actor).Any(z => z.Shape.Check(pos, z.Origin, z.Rotation)) ?? false; + private readonly RoaringBolt _aoe = module.FindComponent()!; + private static readonly Angle a25 = 25.Degrees(); + + public override bool DestinationUnsafe(int slot, Actor actor, WPos pos) + { + foreach (var z in _aoe.ActiveAOEs(slot, actor)) + if (z.Shape.Check(pos, z.Origin, z.Rotation)) + return true; + return false; + } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - var forbidden = new List>(); - var component = Module.FindComponent()?.ActiveAOEs(slot, actor)?.ToList(); - var source = Sources(slot, actor).FirstOrDefault(); - if (source != default && component != null) + if (Casters.Count == 0) + return; + + var forbidden = new List>(6); + var aoes = _aoe.Casters; + + Source source = default; + foreach (var s in Sources(slot, actor)) { - foreach (var c in component) - forbidden.Add(ShapeDistance.Cone(Arena.Center, 19.5f, Angle.FromDirection(c.Origin - Arena.Center), 25.Degrees())); - if (forbidden.Count > 0) - hints.AddForbiddenZone(p => forbidden.Min(f => f(p)), source.Activation); + source = s; + break; + } + var count = aoes.Count; + if (source != default && count != 0) + { + for (var i = 0; i < count; ++i) + forbidden.Add(ShapeDistance.Cone(Arena.Center, 20, Angle.FromDirection(aoes[i].Origin - Arena.Center), a25)); + float minDistanceFunc(WPos pos) + { + var minDistance = float.MaxValue; + for (var i = 0; i < forbidden.Count; ++i) + { + var distance = forbidden[i](pos); + if (distance < minDistance) + minDistance = distance; + } + return minDistance; + } + + hints.AddForbiddenZone(minDistanceFunc, source.Activation); } } } class RollingThunder(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.RollingThunder2), new AOEShapeCone(20, 22.5f.Degrees()), 6) { - public override IEnumerable ActiveAOEs(int slot, Actor actor) => ActiveCasters.Select((c, i) => new AOEInstance(Shape, c.Position, c.CastInfo!.Rotation, Module.CastFinishAt(c.CastInfo), i < 2 ? Colors.Danger : Colors.AOE)); + public override IEnumerable ActiveAOEs(int slot, Actor actor) + { + var count = Casters.Count; + var max = count > 6 ? 6 : count; + var aoes = new List(max); + for (var i = 0; i < max; ++i) + { + var caster = Casters[i]; + var instance = new AOEInstance(Shape, caster.Position, caster.CastInfo!.Rotation, Module.CastFinishAt(caster.CastInfo), i < 2 ? Colors.Danger : Colors.AOE); + aoes.Add(instance); + } + return aoes; + } } class RoaringBolt(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.RoaringBolt), 6); class UntamedCurrentSpread(BossModule module) : Components.SpreadFromCastTargets(module, ActionID.MakeSpell(AID.UntamedCurrentSpread), 5); class UntamedCurrentStack(BossModule module) : Components.StackWithCastTargets(module, ActionID.MakeSpell(AID.UntamedCurrentStack), 6); -class UntamedCurrents(BossModule module, AID aid) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(aid), 5); -class UntamedCurrentAOE1(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE1); -class UntamedCurrentAOE2(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE2); -class UntamedCurrentAOE3(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE3); -class UntamedCurrentAOE4(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE4); -class UntamedCurrentAOE5(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE5); -class UntamedCurrentAOE6(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE6); -class UntamedCurrentAOE7(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE7); -class UntamedCurrentAOE8(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE8); -class UntamedCurrentAOE9(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE9); -class UntamedCurrentAOE10(BossModule module) : UntamedCurrents(module, AID.UntamedCurrentAOE10); +class UntamedCurrentAOEs(BossModule module) : Components.GenericAOEs(module) +{ + private readonly List _aoes = new(11); + private static readonly AOEShapeCircle circle = new(5); + private static readonly HashSet casts = [AID.UntamedCurrentAOE1, AID.UntamedCurrentAOE2, AID.UntamedCurrentAOE3, AID.UntamedCurrentAOE4, + AID.UntamedCurrentAOE5, AID.UntamedCurrentAOE6, AID.UntamedCurrentAOE7, AID.UntamedCurrentAOE8, AID.UntamedCurrentAOE9, AID.UntamedCurrentAOE10]; + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if (casts.Contains((AID)spell.Action.ID)) + _aoes.Add(new(circle, spell.LocXZ, spell.Rotation, Module.CastFinishAt(spell), ActorID: caster.InstanceID)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (casts.Contains((AID)spell.Action.ID)) + { + for (var i = 0; i < _aoes.Count; ++i) + { + var aoe = _aoes[i]; + if (aoe.ActorID == caster.InstanceID) + { + _aoes.Remove(aoe); + break; + } + } + } + } +} class GwyddrudStates : StateMachineBuilder { @@ -124,17 +192,8 @@ public GwyddrudStates(BossModule module) : base(module) { TrivialPhase() .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() - .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs index ac9c9f46b2..ac17b8fcd8 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs @@ -4,12 +4,16 @@ class P2QuadrupleSlap(BossModule module) : Components.TankSwap(module, ActionID. class P2CrystalOfLight(BossModule module) : Components.Adds(module, (uint)OID.CrystalOfLight); class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.Junction)); class P3BlackHalo(BossModule module) : Components.CastSharedTankbuster(module, ActionID.MakeSpell(AID.BlackHalo), new AOEShapeCone(60, 45.Degrees())); // TODO: verify angle -class P4HallowedWingsL(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HallowedWingsL), new AOEShapeRect(80, 20, 0, 90.Degrees())); -class P4HallowedWingsR(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.HallowedWingsR), new AOEShapeRect(80, 20, 0, -90.Degrees())); + +abstract class P4HallowedWings(BossModule module, AID aid) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(80, 20)); +class P4HallowedWingsL(BossModule module) : P4HallowedWings(module, AID.HallowedWingsL); +class P4HallowedWingsR(BossModule module) : P4HallowedWings(module, AID.HallowedWingsR); [ModuleInfo(BossModuleInfo.Maturity.WIP, PrimaryActorOID = (uint)OID.BossP1, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 1006, NameID = 9707, PlanLevel = 100)] -public class FRU(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20)) +public class FRU(WorldState ws, Actor primary) : BossModule(ws, primary, arena.Center, arena with { IsCircle = true }) { + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(100, 100), 20, 64)]); + private Actor? _bossP2; private Actor? _iceVeil; private Actor? _bossP3; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index 6ef3fe1608..e75fa76439 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -3,7 +3,6 @@ public enum OID : uint { BossP1 = 0x459B, // R5.004, x1 - Helper = 0x233C, // R0.500, x24, Helper type FatebreakersImage = 0x459C, // R5.004, x15 FatebreakersImageHelper = 0x45B0, // R1.800, x8 HaloOfFlame = 0x459D, // R1.000, x0 (spawn during fight) @@ -37,6 +36,8 @@ public enum OID : uint 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 + + Helper = 0x233C } public enum AID : uint diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs index 4eb5fd9c6e..84b65e467a 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1BoundOfFaith.cs @@ -1,8 +1,12 @@ namespace BossMod.Dawntrail.Ultimate.FRU; -class P1TurnOfHeavensBurntStrikeFire(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurntStrikeFire), new AOEShapeRect(40, 5, 40)); -class P1TurnOfHeavensBurntStrikeLightning(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurntStrikeLightning), new AOEShapeRect(40, 5, 40)); -class P1TurnOfHeavensBurnout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.TurnOfHeavensBurnout), new AOEShapeRect(40, 10, 40)); +abstract class BurntStrike(BossModule module, AID aid) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(40, 5, 40)); +class P1TurnOfHeavensBurntStrikeFire(BossModule module) : BurntStrike(module, AID.TurnOfHeavensBurntStrikeFire); +class P1TurnOfHeavensBurntStrikeLightning(BossModule module) : BurntStrike(module, AID.TurnOfHeavensBurntStrikeLightning); + +abstract class BurntOut(BossModule module, AID aid) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(40, 10, 40)); +class P1TurnOfHeavensBurnout(BossModule module) : BurntOut(module, AID.TurnOfHeavensBurnout); + class P1BrightfireSmall(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BrightfireSmall), new AOEShapeCircle(5)); class P1BrightfireLarge(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.BrightfireLarge), new AOEShapeCircle(10)); diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs index e14e742f98..83d47b8b95 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1Explosion.cs @@ -1,8 +1,8 @@ namespace BossMod.Dawntrail.Ultimate.FRU; -class P1ExplosionBurntStrikeFire(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurntStrikeFire), new AOEShapeRect(40, 5, 40)); -class P1ExplosionBurntStrikeLightning(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurntStrikeLightning), new AOEShapeRect(40, 5, 40)); -class P1ExplosionBurnout(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ExplosionBurnout), new AOEShapeRect(40, 10, 40)); +class P1ExplosionBurntStrikeFire(BossModule module) : BurntStrike(module, AID.ExplosionBurntStrikeFire); +class P1ExplosionBurntStrikeLightning(BossModule module) : BurntStrike(module, AID.ExplosionBurntStrikeLightning); +class P1ExplosionBurnout(BossModule module) : BurntOut(module, AID.ExplosionBurnout); // TODO: non-fixed conga? class P1Explosion(BossModule module) : Components.GenericTowers(module) @@ -54,19 +54,19 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) { case AID.Explosion11: case AID.Explosion12: - AddTower(caster.Position, 1, Module.CastFinishAt(spell)); + AddTower(caster.Position, 1, spell); break; case AID.Explosion21: case AID.Explosion22: - AddTower(caster.Position, 2, Module.CastFinishAt(spell)); + AddTower(caster.Position, 2, spell); break; case AID.Explosion31: case AID.Explosion32: - AddTower(caster.Position, 3, Module.CastFinishAt(spell)); + AddTower(caster.Position, 3, spell); break; case AID.Explosion41: case AID.Explosion42: - AddTower(caster.Position, 4, Module.CastFinishAt(spell)); + AddTower(caster.Position, 4, spell); break; case AID.ExplosionBurnout: _isWideLine = true; @@ -87,7 +87,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) case AID.Explosion41: case AID.Explosion42: ++NumCasts; - Towers.RemoveAll(t => t.Position.AlmostEqual(caster.Position, 1)); + Towers.RemoveAll(t => t.Position == caster.Position); break; case AID.ExplosionBurnout: case AID.ExplosionBlastburn: @@ -96,10 +96,10 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } } - private void AddTower(WPos pos, int numSoakers, DateTime activation) + private void AddTower(WPos pos, int numSoakers, ActorCastInfo spell) { - Activation = activation; - Towers.Add(new(pos, 4, numSoakers, numSoakers, default, activation)); + Activation = Module.CastFinishAt(spell); + Towers.Add(new(pos, 4, numSoakers, numSoakers, default, Activation)); if (Towers.Count != 3) return; @@ -110,7 +110,7 @@ private void AddTower(WPos pos, int numSoakers, DateTime activation) return; } Towers.SortBy(t => t.Position.Z); - TowerDir.X = Towers.Sum(t => t.Position.X - Module.Center.X) > 0 ? 1 : -1; + TowerDir.X = Towers.Sum(t => t.Position.X - Arena.Center.X) > 0 ? 1 : -1; Span slotByGroup = [-1, -1, -1, -1, -1, -1, -1, -1]; foreach (var (slot, group) in _config.P1ExplosionsAssignment.Resolve(Raid)) @@ -118,7 +118,7 @@ private void AddTower(WPos pos, int numSoakers, DateTime activation) if (slotByGroup.Contains(-1)) return; var nextFlex = 5; - for (int i = 0; i < 3; ++i) + for (var i = 0; i < 3; ++i) { ref var tower = ref Towers.Ref(i); tower.ForbiddenSoakers.Raw = 0xFF; @@ -132,14 +132,14 @@ private void AddTower(WPos pos, int numSoakers, DateTime activation) tower.ForbiddenSoakers.Clear(slotByGroup[i + 5]); // if the tower requires >2 soakers, also assign each flex soaker that has natural 1-man tower (this works, because only patterns are 2-2-2, 1-2-3 and 1-1-4) if (tower.MinSoakers > 2) - for (int j = 0; j < 3; ++j) + for (var j = 0; j < 3; ++j) if (Towers[j].MinSoakers == 1) tower.ForbiddenSoakers.Clear(slotByGroup[j + 5]); } else { // conga fill strategy - grab next N flex soakers in priority order - for (int j = 1; j < tower.MinSoakers; ++j) + for (var j = 1; j < tower.MinSoakers; ++j) tower.ForbiddenSoakers.Clear(slotByGroup[nextFlex++]); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs index 13dfda710b..65fca06dac 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1FallOfFaith.cs @@ -135,7 +135,7 @@ private void InitAssignments() _minHelpMove = WorldState.FutureTime(1); } - private bool IsGroupEven(int order) => order is 2 or 4 or 7 or 8; + private static bool IsGroupEven(int order) => order is 2 or 4 or 7 or 8; private int NextAssignedBaitOrder(int slot) { diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs index a78c90b970..026df65aee 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs @@ -144,7 +144,7 @@ public override void Update() foreach (var (slot, group) in _config.P1UtopianSkyInitialSpots.Resolve(Raid)) { var spot = group & 3; - if (folded[spot] && !_seenDangerSpot[spot] && Raid[slot] is var p && p != null && !p.Position.InDonutCone(Module.Center, 12, 20, (180 - 45 * group).Degrees(), 30.Degrees())) + if (folded[spot] && !_seenDangerSpot[spot] && Raid[slot] is var p && p != null && !p.Position.InDonutCone(Arena.Center, 12, 20, (180 - 45 * group).Degrees(), 30.Degrees())) _seenDangerSpot.Set(spot); } } @@ -161,7 +161,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (clockSpot >= 0 && (_aoes.DangerousSpots[clockSpot] || _seenDangerSpot[clockSpot & 3])) { // our spot is dangerous, or our partner's is and he has moved - move to center - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 5), _aoes.Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center, 5), _aoes.Activation); } // else: we don't have a reason to move, stay where we are... } @@ -181,7 +181,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme _ => default }; var range = spreadSpot == 0 ? 13 : 19; - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + range * direction.ToDirection(), 1), _aoes.Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center + range * direction.ToDirection(), 1), _aoes.Activation); } } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs index fc55d0d7e5..c40ae9ac5e 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2DiamondDust.cs @@ -40,7 +40,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) private void MarkAsRisky(int start, int end) { - for (int i = start; i < end; ++i) + for (var i = start; i < end; ++i) AOEs.Ref(i).Risky = true; } } @@ -126,13 +126,13 @@ class P2DiamondDustSafespots(BossModule module) : BossComponent(module) public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { if (_safeOffs[slot] != default) - hints.AddForbiddenZone(ShapeDistance.PrecisePosition(Module.Center + _safeOffs[slot], new WDir(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f)); + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(Arena.Center + _safeOffs[slot], new WDir(0, 1), Arena.Bounds.MapResolution, actor.Position, 0.1f)); } public override void DrawArenaForeground(int pcSlot, Actor pc) { if (_safeOffs[pcSlot] != default) - Arena.AddCircle(Module.Center + _safeOffs[pcSlot], 1, Colors.Safe); + Arena.AddCircle(Arena.Center + _safeOffs[pcSlot], 1, Colors.Safe); } public override void OnCastStarted(Actor caster, ActorCastInfo spell) @@ -142,7 +142,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) case AID.IcicleImpact: if (_conesAtCardinals == null) { - _conesAtCardinals = IsCardinal(caster.Position - Module.Center); + _conesAtCardinals = IsCardinal(caster.Position - Arena.Center); InitIfReady(); } break; @@ -203,7 +203,7 @@ private void InitIfReady() } } - private bool IsCardinal(WDir off) => Math.Abs(off.X) < 1 || Math.Abs(off.Z) < 1; + private static bool IsCardinal(WDir off) => Math.Abs(off.X) < 1 || Math.Abs(off.Z) < 1; } class P2HeavenlyStrike(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.HeavenlyStrike)) @@ -213,7 +213,7 @@ class P2HeavenlyStrike(BossModule module) : Components.Knockback(module, ActionI public override IEnumerable Sources(int slot, Actor actor) { - yield return new(Module.Center, 12, _activation); + yield return new(Arena.Center, 12, _activation); } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs index 95cd3208b0..a9c9e290a0 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs @@ -13,8 +13,8 @@ public void Show(float delay) void addAOE(WPos pos, DateTime activation) => _aoes.Add(new(_shape, pos, default, activation)); void addPair(WDir offset, DateTime activation) { - addAOE(Module.Center + offset, activation); - addAOE(Module.Center - offset, activation); + addAOE(Arena.Center + offset, activation); + addAOE(Arena.Center - offset, activation); } void addAt(int position, DateTime activation) { @@ -42,8 +42,8 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) if (_aoes.Count > 0 && NumCasts < 16 && _starting != null) { var safeOff = 10 * (_starting.Value - _rotation).ToDirection(); - Arena.AddCircle(Module.Center + safeOff, 1, Colors.Safe); - Arena.AddCircle(Module.Center - safeOff, 1, Colors.Safe); + Arena.AddCircle(Arena.Center + safeOff, 1, Colors.Safe); + Arena.AddCircle(Arena.Center - safeOff, 1, Colors.Safe); } } @@ -206,7 +206,7 @@ private BitMask FindStandardSwap(ReadOnlySpan slotPerAssignment) { BitMask swap = default; Span assignmentPerOrder = [-1, -1, -1, -1]; - for (int role = 0; role < slotPerAssignment.Length; ++role) + for (var role = 0; role < slotPerAssignment.Length; ++role) { var slot = slotPerAssignment[role]; var order = States[slot].Order; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs index a894ec8f05..614e4e335f 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs @@ -165,7 +165,7 @@ private void InitAssignments() // calculate positions var northDir = Angle.FromDirection(_relNorth); - for (int i = 0; i < States.Length; ++i) + for (var i = 0; i < States.Length; ++i) { var player = Raid[i]; if (player == null) @@ -181,21 +181,21 @@ private void InitAssignments() } } - private Angle? SupportDir(int fireOrder, bool? preferEast) => fireOrder switch + private static Angle? SupportDir(int fireOrder, bool? preferEast) => fireOrder switch { 2 => 90.Degrees(), 3 => preferEast == null ? null : preferEast.Value ? -45.Degrees() : 45.Degrees(), _ => 0.Degrees() }; - private Angle? DDDir(int fireOrder, bool? preferEast) => fireOrder switch + private static Angle? DDDir(int fireOrder, bool? preferEast) => fireOrder switch { 1 => preferEast == null ? null : preferEast.Value ? -135.Degrees() : 135.Degrees(), 2 => -90.Degrees(), _ => 180.Degrees() }; - private bool IsBaitingLaser(in PlayerState state, int order) => order switch + private static bool IsBaitingLaser(in PlayerState state, int order) => order switch { 1 => state.LaserOrder == 1, 3 => state.LaserOrder == 2, @@ -203,7 +203,7 @@ private void InitAssignments() _ => false }; - private float RangeHint(in PlayerState state, bool isSupport, int order) => order switch + private static float RangeHint(in PlayerState state, bool isSupport, int order) => order switch { 0 => state.FireOrder == 1 ? RangeHintOut : RangeHintStack, 1 => state.LaserOrder == 1 ? RangeHintLaser : state.HaveDarkEruption ? RangeHintDarkEruption : RangeHintDarkWater, @@ -215,7 +215,7 @@ private void InitAssignments() }; // TODO: rethink this... - private string Hint(in PlayerState state, bool isSupport, int order) => order switch + private static string Hint(in PlayerState state, bool isSupport, int order) => order switch { 0 => state.FireOrder == 1 ? "Out" : "Stack", // 10s 1 => state.LaserOrder == 1 ? "Laser" : state.HaveDarkEruption ? "Hourglass" : "Mid", // 15s - at this point everyone either baits laser or does rewind (eruption or water) @@ -309,7 +309,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) _ => 0 }; - private WPos AssignedHourglass(int slot) => Module.Center + 9.5f * (_rel?.States[slot].AssignedDir ?? default); + private WPos AssignedHourglass(int slot) => Arena.Center + 9.5f * (_rel?.States[slot].AssignedDir ?? default); } class P3UltimateRelativitySinboundMeltdownAOE(BossModule module) : Components.GenericAOEs(module) @@ -390,7 +390,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) if (eye == pos) continue; - bool danger = HitByEye(pos, pc.Rotation, eye); + var danger = HitByEye(pos, pc.Rotation, eye); var eyeCenter = Arena.WorldPositionToScreenPosition(eye); Components.GenericGaze.DrawEye(eyeCenter, danger); diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs index 0fb686f6a8..f387afafb5 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs @@ -213,7 +213,7 @@ public override void OnStatusGain(Actor actor, ActorStatus status) BitMask forbidden = default; if (Module.FindComponent() is var ct && ct != null) { - for (int i = 0; i < ct.PlayerMechanics.Length; ++i) + for (var i = 0; i < ct.PlayerMechanics.Length; ++i) { // should not be shared by eruption and all claws except air on slow side forbidden[i] = ct.PlayerMechanics[i] switch @@ -277,7 +277,7 @@ public override void OnStatusGain(Actor actor, ActorStatus status) BitMask forbidden = default; if (Module.FindComponent() is var ct && ct != null) { - for (int i = 0; i < ct.PlayerMechanics.Length; ++i) + for (var i = 0; i < ct.PlayerMechanics.Length; ++i) { // should not be shared by all claws except blizzard on slow side forbidden[i] = ct.PlayerMechanics[i] switch @@ -420,8 +420,8 @@ private WDir SafeOffsetFangOther(int numHourglassesDone, float northSlowX) return SafeOffsetFangEruption(northSlowX); } - private WDir SafeOffsetDarknessStack(float northSlowX) => 19 * (northSlowX > 0 ? 140 : -140).Degrees().ToDirection(); - private WDir SafeOffsetFinalNonAir(float northSlowX) => 6 * (northSlowX > 0 ? -150 : 150).Degrees().ToDirection(); + private static WDir SafeOffsetDarknessStack(float northSlowX) => 19 * (northSlowX > 0 ? 140 : -140).Degrees().ToDirection(); + private static WDir SafeOffsetFinalNonAir(float northSlowX) => 6 * (northSlowX > 0 ? -150 : 150).Degrees().ToDirection(); } // TODO: better positioning hints @@ -467,7 +467,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) { if (!RewindDone && _ct != null && _exalines != null && _ct.Cleansed[pcSlot]) { - var vertices = KnockbackSpots(pc.Position).ToList(); + var vertices = KnockbackSpots(pc.Position); Arena.AddQuad(pc.Position, vertices[0], vertices[2], vertices[1], Colors.Danger); Arena.AddCircle(Arena.Center + 0.5f * _exalines.StartingOffset, 1, Colors.Safe); } @@ -486,15 +486,17 @@ public override void OnStatusGain(Actor actor, ActorStatus status) } } - private IEnumerable KnockbackSpots(WPos starting) + private List KnockbackSpots(WPos starting) { + List list = new(3); if (_exalines != null) { var dx = _exalines.StartingOffset.X > 0 ? -20 : +20; var dz = _exalines.StartingOffset.Z > 0 ? -20 : +20; - yield return starting + new WDir(dx, 0); - yield return starting + new WDir(0, dz); - yield return starting + new WDir(dx, dz); + list.Add(starting + new WDir(dx, 0)); + list.Add(starting + new WDir(0, dz)); + list.Add(starting + new WDir(dx, dz)); } + return list; } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs index 14f345f251..348cc0ee74 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4DarklitDragonsong.cs @@ -112,8 +112,8 @@ public override void OnTethered(Actor source, ActorTetherInfo tether) allowedN = allowedS = assignments.TowerSoakers; // no assignments, just mark both towers as good var towerOffset = new WDir(0, 8); - Towers.Add(new(Module.Center - towerOffset, 4, 4, 4, new BitMask(0xFF) ^ allowedN, WorldState.FutureTime(10.4f))); - Towers.Add(new(Module.Center + towerOffset, 4, 4, 4, new BitMask(0xFF) ^ allowedS, WorldState.FutureTime(10.4f))); + Towers.Add(new(Arena.Center - towerOffset, 4, 4, 4, new BitMask(0xFF) ^ allowedN, WorldState.FutureTime(10.4f))); + Towers.Add(new(Arena.Center + towerOffset, 4, 4, 4, new BitMask(0xFF) ^ allowedS, WorldState.FutureTime(10.4f))); } } } diff --git a/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/Ex7ZeromusStates.cs b/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/Ex7ZeromusStates.cs index 3602c16156..572cd0c4cd 100644 --- a/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/Ex7ZeromusStates.cs +++ b/BossMod/Modules/Endwalker/Extreme/Ex7Zeromus/Ex7ZeromusStates.cs @@ -5,9 +5,9 @@ class Ex7ZeromusStates : StateMachineBuilder public Ex7ZeromusStates(BossModule module) : base(module) { SimplePhase(0, Phase1, "P1") - .Raw.Update = () => Module.PrimaryActor.IsDestroyed || Module.PrimaryActor.IsDead || Module.PrimaryActor.HPMP.CurHP <= 1 || (Module.PrimaryActor.CastInfo?.IsSpell(AID.RendTheRift) ?? false); + .Raw.Update = () => Module.PrimaryActor.IsDeadOrDestroyed || Module.PrimaryActor.HPMP.CurHP <= 1 || (Module.PrimaryActor.CastInfo?.IsSpell(AID.RendTheRift) ?? false); DeathPhase(1, Phase2) // starts at around 25%, after current mechanic is resolved - .Raw.Update = () => Module.PrimaryActor.IsDestroyed || Module.PrimaryActor.IsDead || Module.PrimaryActor.HPMP.CurHP <= 1 || (Module.PrimaryActor.CastInfo?.IsSpell(AID.Enrage) ?? false); + .Raw.Update = () => Module.PrimaryActor.IsDeadOrDestroyed || Module.PrimaryActor.HPMP.CurHP <= 1 || (Module.PrimaryActor.CastInfo?.IsSpell(AID.Enrage) ?? false); DeathPhase(2, EnrageP2); // starts at around 660s after current mechanic is resolved } @@ -47,7 +47,6 @@ private void Phase2(uint id) FlowOfTheAbyss(id + 0x50000, 7.4f, true); FlowOfTheAbyss(id + 0x60000, 10.7f, true); DimensionalSurgeNostalgia(id + 0x70000, 7.7f); - // TODO: never seen stuff below... FlowOfTheAbyss(id + 0x80000, 7.4f, true); FlowOfTheAbyss(id + 0x90000, 10.7f, true); DimensionalSurgeNostalgia(id + 0xA0000, 7.7f); diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1Art.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1Art.cs index b9ab35075c..345caef5fe 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1Art.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1Art.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Art; +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA1Art; class Thricecull(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Thricecull)); class AcallamNaSenorach(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.AcallamNaSenorach)); @@ -43,8 +43,9 @@ public override void OnActorEAnim(Actor actor, uint state) 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)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.BaldesionArsenal, GroupID = 639, NameID = 7968, PlanLevel = 70, SortOrder = 1)] 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)]); + 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), + new Polygon(new(-123.5f, 778), 1.7f, 8), new Polygon(new(-134.5f, 778), 1.7f, 8), new Polygon(new(-123.5f, 718), 1.5f, 8), new Polygon(new(-134.5f, 718), 1.5f, 8)]); } diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtEnums.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtEnums.cs index 7998716d6d..ed04f786b9 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtEnums.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtEnums.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Art; +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA1Art; public enum OID : uint { @@ -17,7 +17,7 @@ public enum AID : uint 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 + AcallamNaSenorach = 14645, // Boss->self, 4.0s cast, range 60 circle, raidwide 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 diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtStates.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtStates.cs index 172ac61292..d6b1ab4e44 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtStates.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/BA1ArtStates.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Art; +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA1Art; class BA1ArtStates : StateMachineBuilder { @@ -40,6 +40,7 @@ private void SinglePhase(uint id) AcallamNaSenorach(id += 0xE0000 + pid, 3.2f); Mythcall2(id += 0xF0000 + pid, 6); } + SimpleState(id + 0xFF0000, 10, "???"); } private void Thricecull(uint id, float delay) @@ -86,8 +87,8 @@ private void LegendaryGeas(uint id, float delay) private void GloryUnearthedPitfall(uint id, float delay) { ComponentCondition(id, delay, comp => comp.Chasers.Count != 0, "Chasing AOE start"); - Cast(id + 0x10, AID.Pitfall, 2.5f, 5, "Distance based AOE start"); - CastEnd(id + 0x20, 5, "Distance AOE resolve"); + CastStart(id + 0x10, AID.Pitfall, 2.5f, "Proximity AOE start"); + CastEnd(id + 0x20, 5, "Proximity AOE resolve"); ComponentCondition(id + 0x30, 3.7f, comp => comp.Chasers.Count == 0, "Chasing AOE ends"); } } diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/LegendMythSpinnerCarver.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/LegendMythSpinnerCarver.cs index 406fcfdf67..ffcc693653 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/LegendMythSpinnerCarver.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Art/LegendMythSpinnerCarver.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Art; +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA1Art; class LegendMythSpinnerCarver(BossModule module) : Components.GenericAOEs(module) { diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1Owain.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1Owain.cs index c109a8ffbe..f8fbc2acfe 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1Owain.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1Owain.cs @@ -1,53 +1,15 @@ -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); - } -} +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA1Owain; 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)] +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.BaldesionArsenal, GroupID = 639, NameID = 7970, PlanLevel = 70, SortOrder = 2)] 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)]); + private static readonly ArenaBoundsComplex arena = new([new Polygon(new(128.98f, 748), 29.5f, 64)], [new Rectangle(new(129, 718), 20, 0.8f), new Rectangle(new(129, 778), 20, 0.825f), + new Polygon(new(123.5f, 778), 1.5f, 8), new Polygon(new(134.5f, 778), 1.5f, 8), new Polygon(new(123.5f, 718), 1.5f, 8), new Polygon(new(134.5f, 718), 1.5f, 8)]); protected override void DrawEnemies(int pcSlot, Actor pc) { diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainEnums.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainEnums.cs index 05ae642815..df409bfa31 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainEnums.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainEnums.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA1Owain; public enum OID : uint { @@ -17,13 +17,13 @@ public enum AID : uint 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 + AcallamNaSenorach = 14662, // Boss->self, 5.0s cast, range 60 circle, raidwide 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 + Thricecull = 14661, // Boss->player, 5.0s cast, single-target, tankbuster + 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 diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainStates.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainStates.cs index 409ba95e06..32d6b250c5 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainStates.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/BA1OwainStates.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA1Owain; class BA1OwainStates : StateMachineBuilder { @@ -19,7 +19,7 @@ public BA1OwainStates(BossModule module) : base(module) // all timings seem to have upto 1s variation private void SinglePhase(uint id) { - Thricecull(id, 100); + Thricecull(id, 10); AcallamNaSenorach(id + 0x10000, 7.1f); Mythcall(id + 0x20000, 7.9f); Thricecull(id + 0x30000, 10); @@ -27,30 +27,22 @@ private void SinglePhase(uint id) 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) + for (var i = 0; i < 12; ++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); + Thricecull(id += 0x80000 + pid, 5.1f); + AcallamNaSenorach(id += 0x90000 + pid, 6); + PiercingLight2(id += 0xA0000 + pid, 6.1f); + AcallamNaSenorach(id += 0xB0000 + pid, 9.7f); + AcallamNaSenorach(id += 0xC0000 + pid, 5.1f); + ElementalShiftSpiritcull(id + 0xD0000 + pid, 8.3f); + Thricecull(id += 0xE0000 + pid, 9.3f); + AcallamNaSenorach(id += 0xF0000 + pid, 17.2f); + IvoryPalmElementalMagicks(id += 0x100000 + pid, 1.2f); + Spiritcull(id += 0x110000 + pid, 5.1f); } + SimpleState(id + 0xFF0000, 10, "???"); } private void Thricecull(uint id, float delay) @@ -104,7 +96,7 @@ private void ElementalShiftSpiritcull(uint id, float delay) 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"); + CastStartMulti(id + 0x30, [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"); } diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/ElementalMagicks.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/ElementalMagicks.cs new file mode 100644 index 0000000000..8b77c274ab --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/ElementalMagicks.cs @@ -0,0 +1,40 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.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); + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs index f55a737b69..3b128df505 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/IvoryPalm.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA1Owain; class IvoryPalm(BossModule module) : Components.GenericGaze(module, inverted: true) { diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/Spiritcull.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/Spiritcull.cs index 33003c546b..e7b6dbdb73 100644 --- a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/Spiritcull.cs +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA1Owain/Spiritcull.cs @@ -1,4 +1,4 @@ -namespace BossMod.Stormblood.Foray.BaldesionsArsenal.BA1Owain; +namespace BossMod.Stormblood.Foray.BaldesionArsenal.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); diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/ArenaChange.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/ArenaChange.cs new file mode 100644 index 0000000000..3fe0d141a5 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/ArenaChange.cs @@ -0,0 +1,27 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA2Raiden; + +class ArenaChange(BossModule module) : Components.GenericAOEs(module) +{ + private AOEInstance? _aoe; + private static readonly AOEShapeDonut donut = new(30, 35); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.Thundercall) + _aoe = new(donut, Arena.Center, default, Module.CastFinishAt(spell, 3.4f)); + } + + public override void OnActorEAnim(Actor actor, uint state) + { + if ((OID)actor.OID == OID.Electricwall) + { + if (state == 0x00010002) + { + Arena.Bounds = BA2Raiden.DefaultArena; + _aoe = null; + } + } + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2Raiden.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2Raiden.cs new file mode 100644 index 0000000000..887eb0a5f7 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2Raiden.cs @@ -0,0 +1,59 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA2Raiden; + +class SpiritsOfTheFallen(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.SpiritsOfTheFallen)) +{ + public override bool KeepOnPhaseChange => true; +} +class Levinwhorl(BossModule module) : Components.RaidwideCast(module, ActionID.MakeSpell(AID.Levinwhorl)); +class Shingan(BossModule module) : Components.SingleTargetCast(module, ActionID.MakeSpell(AID.Shingan)) +{ + public override bool KeepOnPhaseChange => true; +} +class AmeNoSakahoko(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.AmeNoSakahoko), new AOEShapeCircle(25)) +{ + public override bool KeepOnPhaseChange => true; +} +class WhirlingZantetsuken(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.WhirlingZantetsuken), new AOEShapeDonut(5, 60)) +{ + public override bool KeepOnPhaseChange => true; +} +class Shock(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Shock), new AOEShapeCircle(8)); +class ForHonor(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.ForHonor), new AOEShapeCircle(11.4f)); + +abstract class LateralZantetsuken(BossModule module, AID aid) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(aid), new AOEShapeRect(75.4f, 19.5f)); +class LateralZantetsuken1(BossModule module) : LateralZantetsuken(module, AID.LateralZantetsuken1); +class LateralZantetsuken2(BossModule module) : LateralZantetsuken(module, AID.LateralZantetsuken2); + +class BitterBarbs(BossModule module) : Components.Chains(module, (uint)TetherID.Chains, ActionID.MakeSpell(AID.BitterBarbs)); +class BoomingLament(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.BoomingLament), new AOEShapeCircle(10)); +class SilentLevin(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.SilentLevin), new AOEShapeCircle(5)); + +class UltimateZantetsuken(BossModule module) : Components.CastHint(module, ActionID.MakeSpell(AID.UltimateZantetsuken), "Enrage, kill the adds!", true); + +[ModuleInfo(BossModuleInfo.Maturity.Verified, Contributors = "The Combat Reborn Team (Malediktus)", GroupType = BossModuleInfo.GroupType.BaldesionArsenal, GroupID = 639, NameID = 7973, PlanLevel = 70, SortOrder = 3)] +public class BA2Raiden(WorldState ws, Actor primary) : BossModule(ws, primary, startingArena.Center, startingArena) +{ + private static readonly WPos ArenaCenter = new(0, 458); + private static readonly ArenaBoundsComplex startingArena = new([new Polygon(ArenaCenter, 34.6f, 80)], [new Rectangle(new(35.3f, 458), 0.99f, 20), new Rectangle(new(-35.4f, 458), 1.65f, 20), + new Rectangle(new(0, 493), 20, 0.75f)]); + public static readonly ArenaBoundsComplex DefaultArena = new([new Polygon(ArenaCenter, 29.93f, 64)]); + + protected override void DrawEnemies(int pcSlot, Actor pc) + { + Arena.Actor(PrimaryActor); + Arena.Actors(Enemies(OID.StreakLightning)); + } + + 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.StreakLightning => 1, + _ => 0 + }; + } + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2RaidenEnums.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2RaidenEnums.cs new file mode 100644 index 0000000000..6a254522cf --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2RaidenEnums.cs @@ -0,0 +1,50 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA2Raiden; + +public enum OID : uint +{ + Boss = 0x2605, // R5.4 + BallLightning = 0x2606, // R1.0 + StreakLightning = 0x2607, // R1.2 + Electricwall = 0x1EA1A1, // R2.0 + Helper = 0x261B +} + +public enum AID : uint +{ + AutoAttack = 14777, // Boss->player, no cast, single-target + + SpiritsOfTheFallen = 14458, // Boss->self, 4.0s cast, range 35+R circle + Shingan = 14459, // Boss->player, 4.0s cast, single-target + Thundercall = 14463, // Boss->self, 3.0s cast, single-target, activates electrified wall + AmeNoSakahokoVisual = 14440, // Boss->self, 4.5s cast, single-target + AmeNoSakahoko = 14441, // Helper->self, 7.5s cast, range 25 circle + WhirlingZantetsuken = 14442, // Boss->self, 5.5s cast, range 5-60 donut + Shock = 14445, // BallLightning->self, 3.0s cast, range 8 circle + LateralZantetsuken1 = 14443, // Boss->self, 6.5s cast, range 70+R width 39 rect + LateralZantetsuken2 = 14444, // Boss->self, 6.5s cast, range 70+R width 39 rect + LancingBolt = 14454, // Boss->self, 3.0s cast, single-target, apply spread markers + LancingBlow = 14455, // StreakLightning->self, no cast, range 10 circle + BoomingLament = 14461, // Boss->location, 4.0s cast, range 10 circle + CloudToGroundVisual = 14448, // Boss->self, 4.0s cast, single-target + CloudToGroundFirst = 14449, // Helper->self, 5.0s cast, range 6 circle + CloudToGroundRest = 14450, // Helper->self, no cast, range 6 circle + BitterBarbs = 14452, // Boss->self, 4.0s cast, single-target, chains + Barbs = 14453, // Helper->self, no cast, ??? + SilentLevin = 14451, // Helper->location, 3.0s cast, range 5 circle + LevinwhorlVisual = 14446, // Boss->self, 8.0s cast, single-target + Levinwhorl = 14447, // Helper->self, 8.0s cast, range 80 circle + ForHonor = 14460, // Boss->self, 4.5s cast, range 6+R circle + UltimateZantetsuken = 14456, // Boss->self, 18.0s cast, range 80+R circle, enrage + UltimateZantetsukenRepeat = 14457 // Boss->self, no cast, range 80+R circle +} + +public enum IconID : uint +{ + Spreadmarker = 138, // player->self +} + +public enum TetherID : uint +{ + Chains = 18, // player->player +} + diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2RaidenStates.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2RaidenStates.cs new file mode 100644 index 0000000000..8ff6d2c28e --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/BA2RaidenStates.cs @@ -0,0 +1,204 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA2Raiden; + +class BA2RaidenStates : StateMachineBuilder +{ + public BA2RaidenStates(BossModule module) : base(module) + { + SimplePhase(0, Phase1, "P1") + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .Raw.Update = () => Module.PrimaryActor.IsDeadOrDestroyed || !Module.PrimaryActor.IsTargetable; + DeathPhase(1, Phase2) // starts at around 70% or after becoming untargetable + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter() + .ActivateOnEnter(); + } + + private void Phase1(uint id) // all timings seem to have lots of variation, maybe HP based conditions + { + SpiritsOfTheFallen(id, 7.8f); + Shingan(id + 0x10000, 6); + Thundercall(id + 0x20000, 8); + AmeNoSakahoko(id + 0x30000, 9.4f); + WhirlingZantetsuken(id + 0x40000, 1.1f); + SpiritsOfTheFallen(id + 0x50000, 9); // apparently this raidwide can be skipped without actually getting a phase change? + AmeNoSakahoko(id + 0x40000, 6.3f); + WhirlingZantetsuken(id + 0x50000, 1.1f); + IsTargetable(id + 0x60000, false, 3.6f); + } + + private void Phase2(uint id) // many timings seem to have lots of variation, no idea if they are predictable or just due to server load and latency + { + BallLightningLateralZantetsuken(id, 9); + Targetable(id + 0x10000, true, 4.3f); + SpiritsOfTheFallen(id + 0x20000, 2.6f); + UltimateZantetsuken(id + 0x30000, 12.8f); + SpiritsOfTheFallen(id + 0x40000, 6); + BoomingLamentCloudToGround(id + 0x50000, 2.4f); + WhirlingZantetsuken(id + 0x60000, 2.6f); + SpiritsOfTheFallen(id + 0x70000, 6.1f); + CloudToGroundLevinWhorl(id + 0x80000, 13); + AmeNoSakahoko(id + 0x90000, 5.4f); + WhirlingZantetsuken(id + 0xA0000, 1.1f); + SilentLevinBoomingLament(id + 0xB0000, 2); + SpiritsOfTheFallen(id + 0xC0000, 5.3f); + BallLightningLateralZantetsukenUltimateZantetsuken(id + 0xD0000, 8.2f); + SpiritsOfTheFallen(id + 0xE0000, 5.9f); + Shingan(id + 0xF0000, 1.9f); + AmeNoSakahoko(id + 0x100000, 7.1f); + ForHonor(id + 0x110000, 1.1f); + WhirlingZantetsukenSilentLevin(id + 0x120000, 2.1f); + SpiritsOfTheFallen(id + 0x130000, 7.8f); + SpiritsOfTheFallen(id + 0x140000, 2.1f); + CloudToGroundSilentLevin(id + 0x150000, 16.2f); + WhirlingZantetsuken(id + 0x160000, 0.2f); + SpiritsOfTheFallen(id + 0x170000, 6.2f); + CloudToGroundLevinWhorl(id + 0x180000, 13.1f); + AmeNoSakahoko(id + 0x190000, 1.3f); + WhirlingZantetsuken(id + 0x1A0000, 1.2f); + SimpleState(id + 0xFF0000, 10, "???"); + } + + private void Shingan(uint id, float delay) + { + Cast(id, AID.Shingan, delay, 4, "Tankbuster") + .SetHint(StateMachine.StateHint.Tankbuster); + } + + private void SpiritsOfTheFallen(uint id, float delay) + { + Cast(id, AID.SpiritsOfTheFallen, delay, 4, "Raidwide") + .SetHint(StateMachine.StateHint.Raidwide); + } + + private void Thundercall(uint id, float delay) + { + Cast(id, AID.Thundercall, delay, 3, "Arena wall"); + } + + private void AmeNoSakahoko(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Casters.Count != 0, "Circle AOE"); + ComponentCondition(id + 0x10, 7.5f, comp => comp.Casters.Count == 0, "AOE resolve"); + } + + private void WhirlingZantetsuken(uint id, float delay) + { + Cast(id, AID.WhirlingZantetsuken, delay, 5.5f, "Donut AOE"); + } + + private void IsTargetable(uint id, bool targetable, float delay) + { + Targetable(id, targetable, delay); + } + + private void BallLightningLateralZantetsuken(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Casters.Count != 0, "Baited circles"); + CastStartMulti(id + 0x10, [AID.LateralZantetsuken1, AID.LateralZantetsuken2], 0.8f, "Half room cleave starts"); + ComponentCondition(id + 0x20, 2.1f, comp => comp.Casters.Count == 0, "Circles resolve"); + CastEnd(id + 0x30, 4.3f, "Cleave resolves"); + } + + private void BallLightningLateralZantetsukenUltimateZantetsuken(uint id, float delay) + { + Targetable(id, false, delay); + ComponentCondition(id + 0x10, 3.3f, comp => comp.Spreads.Count != 0, "Spread markers appear"); + ComponentCondition(id + 0x20, 1.1f, comp => comp.Casters.Count != 0, "Baited circles"); + ComponentCondition(id + 0x30, 3, comp => comp.Casters.Count == 0, "Circles resolve"); + ComponentCondition(id + 0x40, 1.1f, comp => comp.Spreads.Count == 0, "Adds spawn"); + CastStartMulti(id + 0x50, [AID.LateralZantetsuken1, AID.LateralZantetsuken2], 0.1f, "Half room cleave starts"); + ComponentCondition(id + 0x60, 1, comp => comp.NumCasts != 0, "AOEs around adds"); + ComponentCondition(id + 0x70, 0.3f, comp => comp.Casters.Count != 0, "Baited circles 1"); + ComponentCondition(id + 0x80, 1.9f, comp => comp.Casters.Count > 3, "Baited circles 2"); + ComponentCondition(id + 0x90, 2.7f, comp => comp.NumCasts >= 3 && comp.Casters.Count > 3, "Baited circles 3"); + CastEnd(id + 0xA0, 1.6f, "Cleave resolves"); + Targetable(id + 0xB0, true, 4.7f); + CastStart(id + 0xC0, AID.UltimateZantetsuken, 2.1f, "Enrage start"); + CastEnd(id + 0xD0, 18, "Enrage") + .ResetComp() + .ResetComp(); + } + + private void UltimateZantetsuken(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Spreads.Count != 0, "Spread markers appear"); + ComponentCondition(id + 0x10, 4.7f, comp => comp.Spreads.Count == 0, "Adds spawn"); + ComponentCondition(id + 0x20, 1, comp => comp.NumCasts != 0, "AOEs around adds"); + Targetable(id + 0x30, false, 2); + Targetable(id + 0x40, true, 4); + CastStart(id + 0x50, AID.UltimateZantetsuken, 1.6f, "Enrage start"); + CastEnd(id + 0x60, 18, "Enrage") + .ResetComp(); + } + + private void BoomingLamentCloudToGround(uint id, float delay) + { + Cast(id, AID.BoomingLament, delay, 4, "Circle AOE"); + ComponentCondition(id + 0x10, 8, comp => comp.Lines.Count != 0, "Exaflares start"); + ComponentCondition(id + 0x20, 15, comp => comp.Casters.Count != 0, "Baited circles 1"); + ComponentCondition(id + 0x30, 0.5f, comp => comp.TethersAssigned, "Chains"); + ComponentCondition(id + 0x40, 1.2f, comp => comp.Casters.Count > 3, "Baited circles 2"); + ComponentCondition(id + 0x50, 2, comp => comp.NumCasts >= 3 && comp.Casters.Count > 3, "Baited circles 3"); + ComponentCondition(id + 0x60, 6.2f, comp => comp.Lines.Count == 0, "Exaflares end") + .ResetComp() + .ResetComp(); + } + + private void CloudToGroundSilentLevin(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Lines.Count != 0, "Exaflares start"); + ComponentCondition(id + 0x10, 15, comp => comp.Casters.Count != 0, "Baited circles 1"); + ComponentCondition(id + 0x20, 0.5f, comp => comp.TethersAssigned, "Chains"); + ComponentCondition(id + 0x30, 1.2f, comp => comp.Casters.Count > 3, "Baited circles 2"); + ComponentCondition(id + 0x40, 2, comp => comp.NumCasts >= 3 && comp.Casters.Count > 3, "Baited circles 3"); + ComponentCondition(id + 0x50, 6.2f, comp => comp.Lines.Count == 0, "Exaflares end") + .ResetComp() + .ResetComp(); + } + + private void CloudToGroundLevinWhorl(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Lines.Count != 0, "Exaflares start"); + Cast(id + 0x10, AID.LevinwhorlVisual, 6.3f, 8, "Raidwide") + .SetHint(StateMachine.StateHint.Raidwide); + ComponentCondition(id + 0x20, 13.4f, comp => comp.Lines.Count == 0, "Exaflares end"); + } + + private void SilentLevinBoomingLament(uint id, float delay) + { + ComponentCondition(id, delay, comp => comp.Casters.Count != 0, "Baited circles 1"); + ComponentCondition(id + 0x10, 1.1f, comp => comp.Casters.Count > 3, "Baited circles 2"); + ComponentCondition(id + 0x20, 2.1f, comp => comp.Casters.Count > 6 || comp.NumCasts >= 3 && comp.Casters.Count > 3, "Baited circles 3"); + ComponentCondition(id + 0x30, 2.7f, comp => comp.Casters.Count == 0, "Baits end") + .ResetComp(); + } + + private void ForHonor(uint id, float delay) + { + Cast(id, AID.ForHonor, delay, 4.5f, "Circle AOE"); + } + + private void WhirlingZantetsukenSilentLevin(uint id, float delay) + { + CastStart(id, AID.WhirlingZantetsuken, delay, "Donut AOE"); + ComponentCondition(id + 0x10, 5.1f, comp => comp.Casters.Count != 0, "Baited circles 1"); + CastEnd(id + 0x20, 0.4f, "Donut AOE"); + ComponentCondition(id + 0x30, 1.6f, comp => comp.Casters.Count > 3, "Baited circles 2"); + ComponentCondition(id + 0x40, 2.5f, comp => comp.Casters.Count > 6 || comp.NumCasts >= 3 && comp.Casters.Count > 3, "Baited circles 3"); + Cast(id + 0x50, AID.BoomingLament, 1.1f, 4, "Circle AOE") + .ResetComp(); + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/CloudToGround.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/CloudToGround.cs new file mode 100644 index 0000000000..0a1aabaaf6 --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/CloudToGround.cs @@ -0,0 +1,26 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA2Raiden; + +class CloudToGround(BossModule module) : Components.Exaflare(module, 6) +{ + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.CloudToGroundFirst) + { + var explosions = spell.LocXZ.InRect(Arena.Center, spell.Rotation, 35, 35, 15) ? 8 : spell.LocXZ.InRect(Arena.Center, spell.Rotation, 35, 35, 20) ? 6 : 4; + Lines.Add(new() { Next = spell.LocXZ, Advance = 8 * spell.Rotation.ToDirection(), NextExplosion = Module.CastFinishAt(spell), TimeToMove = 1.1f, ExplosionsLeft = explosions, MaxShownExplosions = 10 }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.CloudToGroundFirst or AID.CloudToGroundRest) + { + var index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1)); + if (index == -1) + return; + AdvanceLine(Lines[index], caster.Position); + if (Lines[index].ExplosionsLeft == 0) + Lines.RemoveAt(index); + } + } +} diff --git a/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/LancingBlow.cs b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/LancingBlow.cs new file mode 100644 index 0000000000..a37ab265fc --- /dev/null +++ b/BossMod/Modules/Stormblood/Foray/BaldesionsArsenal/BA2Raiden/LancingBlow.cs @@ -0,0 +1,33 @@ +namespace BossMod.Stormblood.Foray.BaldesionArsenal.BA2Raiden; + +class LancingBlowSpread(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.Spreadmarker, ActionID.MakeSpell(AID.LancingBlow), 10, 6) +{ + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.StreakLightning) + Spreads.Clear(); + } +} + +class LancingBlowAOE(BossModule module) : Components.GenericAOEs(module) +{ + private static readonly AOEShapeCircle circle = new(10); + public readonly List AOEs = new(6); + + public override IEnumerable ActiveAOEs(int slot, Actor actor) => AOEs; + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.StreakLightning) + AOEs.Add(new(circle, actor.Position, default, WorldState.FutureTime(1))); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.LancingBlow) + { + ++NumCasts; + AOEs.Clear(); + } + } +} diff --git a/BossMod/Replay/Analysis/StatusInfo.cs b/BossMod/Replay/Analysis/StatusInfo.cs index cffd0eda68..2b7bf46ead 100644 --- a/BossMod/Replay/Analysis/StatusInfo.cs +++ b/BossMod/Replay/Analysis/StatusInfo.cs @@ -26,7 +26,7 @@ public StatusInfo(List replays, uint oid) { foreach (var enc in replay.Encounters.Where(enc => enc.OID == oid)) { - foreach (var status in replay.EncounterStatuses(enc).Where(s => !(s.ID is 43 or 44 or 418) && !(s.Source?.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy) && !(s.Target.Type is ActorType.Pet or ActorType.Chocobo))) + foreach (var status in replay.EncounterStatuses(enc).Where(s => !ReplayVisualization.OpList.BoringSIDs.Contains(s.ID) && !(s.Source?.Type is ActorType.Player or ActorType.Pet or ActorType.Chocobo or ActorType.Buddy) && !(s.Target.Type is ActorType.Pet or ActorType.Chocobo))) { var data = _data.GetOrAdd(status.ID); if (status.Source != null) diff --git a/BossMod/Replay/Visualization/OpList.cs b/BossMod/Replay/Visualization/OpList.cs index 75185fb6a4..a42d2697e4 100644 --- a/BossMod/Replay/Visualization/OpList.cs +++ b/BossMod/Replay/Visualization/OpList.cs @@ -15,7 +15,8 @@ class OpList(Replay replay, Replay.Encounter? enc, BossModuleRegistry.Info? modu 0x2E7F, 0x2F33, 0x2F32, 0x2F38, 0x2E80, 0x2E82, 0x2E81, 0x2F36, 0x2E7D, 0x2F35, 0x2EB0, 0x2F31, 0x2F37, 0x2E7C, 0x2E7B, 0x2EAE, 0x2F3A, 0x2F30, 0x2E7E, 0x2EAF, 0x428B, 0x44B8, 0x43D2, 0x43D1, 0x41FD, 0x42A4, 0x41C5, 0x30B7, 0x4021, 0x4019, 0x401C, 0x401B, 0x401F, 0x40FB, 0x4105, 0x401D, 0x4102, 0x4629, 0x4628, 0x4631, 0x4630, 0x46D6, 0xF5B, 0xF5C, 0x2E20, 0x2E21, 0x318A, 0x2E1E, 0x3346, 0x3353, 0x31D4, 0x3345, 0x3355, 0x3326, 0x3344, 0x31B1, 0x3343, 0x1EB165, 0x1EB166, - 0x1EB167, 0x1EB168]; + 0x1EB167, 0x1EB168, 0x4339, 0x4144, 0x4146, 0x4348, 0x4339, 0x4337]; + public static readonly HashSet BoringSIDs = [43, 44, 418, 364, 902, 1050, 368, 362, 1086, 1461, 1463, 365, 1778, 1755, 360, 1411]; private readonly HashSet _filteredActions = []; private readonly HashSet _filteredStatuses = []; private readonly HashSet _filteredDirectorUpdateTypes = []; @@ -89,8 +90,8 @@ private bool FilterInterestingStatus(Replay.Status s) return false; // don't care about statuses applied by players if (s.Target.Type is ActorType.Pet) return false; // don't care about statuses applied to pets - if (s.ID is 43 or 44 or 418) - return false; // don't care about resurrect-related statuses + if (BoringSIDs.Contains(s.ID)) + return false; // don't care about resurrect-related and other trivial statuses if (_filteredOIDs.Contains(s.Target.OID)) return false; // don't care about filtered out targets if (_filteredStatuses.Contains(s.ID)) diff --git a/BossMod/Util/WPosDir.cs b/BossMod/Util/WPosDir.cs index 4b0fee0b5f..705c62a109 100644 --- a/BossMod/Util/WPosDir.cs +++ b/BossMod/Util/WPosDir.cs @@ -54,6 +54,15 @@ public readonly bool InRect(WDir direction, float lenFront, float lenBack, float var dotNormal = Dot(direction.OrthoL()); return dotDir >= -lenBack && dotDir <= lenFront && Math.Abs(dotNormal) <= halfWidth; } + + public readonly bool InCross(WDir direction, float length, float halfWidth) + { + var dotDir = Dot(direction); + var absDotNormal = Math.Abs(Dot(direction.OrthoL())); + var inVerticalArm = dotDir >= -length && dotDir <= length && absDotNormal <= halfWidth; + var inHorizontalArm = dotDir >= -halfWidth && dotDir <= halfWidth && absDotNormal <= length; + return inVerticalArm || inHorizontalArm; + } } // 2d vector that represents world-space position on XZ plane @@ -130,6 +139,9 @@ public readonly bool InRect(WPos origin, WDir startToEnd, float halfWidth) return InRect(origin, startToEnd / len, len, 0, halfWidth); } + public readonly bool InCross(WPos origin, Angle direction, float length, float halfWidth) => (this - origin).InCross(direction.ToDirection(), length, halfWidth); + public readonly bool InCross(WPos origin, WDir direction, float length, float halfWidth) => (this - origin).InCross(direction, length, halfWidth); + public readonly bool InCircle(WPos origin, float radius) => (this - origin).LengthSq() <= radius * radius; public readonly bool InDonut(WPos origin, float innerRadius, float outerRadius) => InCircle(origin, outerRadius) && !InCircle(origin, innerRadius); From 3a54b9eb624801d3bccecee18935e8c131affd84 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Sat, 11 Jan 2025 03:00:44 +0100 Subject: [PATCH 4/4] merge fix --- .../Chaotic/Ch01CloudOfDarkness/EnaeroEndeath.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/EnaeroEndeath.cs b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/EnaeroEndeath.cs index a2aa3304b6..a20b6214a4 100644 --- a/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/EnaeroEndeath.cs +++ b/BossMod/Modules/Dawntrail/Chaotic/Ch01CloudOfDarkness/EnaeroEndeath.cs @@ -77,7 +77,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) private void Start(DateTime activation, Kind kind) { NumCasts = 0; - _source = new(Ch01CloudOfDarkness.Phase1Midpoint, 15, activation, Kind: kind); + _source = new(Ch01CloudOfDarkness.Phase1BoundsCenter, 15, activation, Kind: kind); } } @@ -123,14 +123,14 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) private void Start(DateTime activation) { NumCasts = 0; - _aoe = new(_shape, Ch01CloudOfDarkness.Phase1Midpoint, default, activation); + _aoe = new(_shape, Ch01CloudOfDarkness.Phase1BoundsCenter, default, activation); _delayed = false; } } class EndeathAOE(BossModule module) : Components.GenericAOEs(module) { - private readonly List _aoes = []; + private readonly List _aoes = new(2); private bool _delayed; private static readonly AOEShapeCircle _shapeOut = new(6); @@ -159,7 +159,7 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) case AID.DeathAOE2: case AID.EndeathAOE1: case AID.EndeathAOE2: - if (_aoes.Count > 0) + if (_aoes.Count != 0) _aoes.RemoveAt(0); break; case AID.BladeOfDarknessLAOE: @@ -174,8 +174,8 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) private void Start(DateTime activation) { NumCasts = 0; - _aoes.Add(new(_shapeOut, Ch01CloudOfDarkness.Phase1Midpoint, default, activation.AddSeconds(2))); - _aoes.Add(new(_shapeIn, Ch01CloudOfDarkness.Phase1Midpoint, default, activation.AddSeconds(4))); + _aoes.Add(new(_shapeOut, Ch01CloudOfDarkness.Phase1BoundsCenter, default, activation.AddSeconds(2))); + _aoes.Add(new(_shapeIn, Ch01CloudOfDarkness.Phase1BoundsCenter, default, activation.AddSeconds(4))); _delayed = false; } }