From 9d62efb03960c9e5b0a6634ed49ebe9496895a98 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Sun, 12 Jan 2025 17:20:19 +0000 Subject: [PATCH 1/3] FRU: P5 start, AI for P3 complete. --- BossMod/Autorotation/Standard/StandardWAR.cs | 6 +- BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs | 6 +- .../Dawntrail/Ultimate/FRU/FRUConfig.cs | 9 + .../Dawntrail/Ultimate/FRU/FRUEnums.cs | 22 ++ .../Dawntrail/Ultimate/FRU/FRUStates.cs | 93 ++++-- .../Dawntrail/Ultimate/FRU/P1UtopianSky.cs | 2 +- .../Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs | 134 +++++++- .../Dawntrail/Ultimate/FRU/P2Banish.cs | 1 + .../Dawntrail/Ultimate/FRU/P2LightRampant.cs | 1 - .../Dawntrail/Ultimate/FRU/P3Apocalypse.cs | 285 +++++++++++++++--- .../Ultimate/FRU/P3UltimateRelativity.cs | 76 ++++- .../Dawntrail/Ultimate/FRU/P5AkhMorn.cs | 50 +++ .../Dawntrail/Ultimate/FRU/P5FulgentBlade.cs | 86 ++++++ .../Dawntrail/Ultimate/FRU/SpiritTaker.cs | 24 ++ 14 files changed, 708 insertions(+), 87 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs diff --git a/BossMod/Autorotation/Standard/StandardWAR.cs b/BossMod/Autorotation/Standard/StandardWAR.cs index 5a4f9bf512..7ec0bb4fe3 100644 --- a/BossMod/Autorotation/Standard/StandardWAR.cs +++ b/BossMod/Autorotation/Standard/StandardWAR.cs @@ -303,7 +303,7 @@ public override void Execute(StrategyValues strategy, Actor? primaryTarget, floa if (Player.InCombat && Unlocked(WAR.AID.Infuriate)) { var stratInf = strategy.Option(Track.Infuriate); - var inf = ShouldUseInfuriate(stratInf.As(), primaryTarget); + var inf = ShouldUseInfuriate(stratInf.As(), primaryTarget, burstStrategy == BurstStrategy.IgnoreST); if (inf.Use) QueueOGCD(WAR.AID.Infuriate, Player, stratInf.Value.PriorityOverride, OGCDPriority.Infuriate, inf.Delayable ? ActionQueue.Priority.VeryLow : ActionQueue.Priority.Low); } @@ -837,7 +837,7 @@ private bool ShouldUseBerserk(OffensiveStrategy strategy, Actor? target, int aoe } } - private (bool Use, bool Delayable) ShouldUseInfuriate(InfuriateStrategy strategy, Actor? target) + private (bool Use, bool Delayable) ShouldUseInfuriate(InfuriateStrategy strategy, Actor? target, bool ignoreST) { if (strategy == InfuriateStrategy.Delay || CanFitGCD(NascentChaosLeft)) return (false, false); // explicitly forbidden or NC still active @@ -915,7 +915,7 @@ private bool ShouldUseBerserk(OffensiveStrategy strategy, Actor? target, int aoe _ => (30, 4) }; // don't double infuriate during opener when NC is not yet unlocked (TODO: consider making it better) - if (Gauge + stRefreshGauge + 50 > 100 && !CanFitGCD(SurgingTempestLeft, stRefreshGCDs)) + if (!ignoreST && Gauge + stRefreshGauge + 50 > 100 && !CanFitGCD(SurgingTempestLeft, stRefreshGCDs)) return (false, false); // at this point, use under burst or delay outside (TODO: reconsider, we might want to be smarter here...) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs index 836c1645f5..1b5b8abad2 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRU.cs @@ -1,7 +1,6 @@ namespace BossMod.Dawntrail.Ultimate.FRU; class P2QuadrupleSlap(BossModule module) : Components.TankSwap(module, ActionID.MakeSpell(AID.QuadrupleSlapFirst), ActionID.MakeSpell(AID.QuadrupleSlapFirst), ActionID.MakeSpell(AID.QuadrupleSlapSecond), 4.1f, null, true); -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())); @@ -15,6 +14,7 @@ class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.Ma private Actor? _bossP3; private Actor? _bossP4Usurper; private Actor? _bossP4Oracle; + private Actor? _bossP5; public Actor? BossP1() => PrimaryActor; public Actor? BossP2() => _bossP2; @@ -22,6 +22,7 @@ class P3Junction(BossModule module) : Components.CastCounter(module, ActionID.Ma public Actor? BossP3() => _bossP3; public Actor? BossP4Usurper() => _bossP4Usurper; public Actor? BossP4Oracle() => _bossP4Oracle; + public Actor? BossP5() => _bossP5; protected override void UpdateModule() { @@ -32,15 +33,16 @@ protected override void UpdateModule() _bossP3 ??= StateMachine.ActivePhaseIndex == 2 ? Enemies(OID.BossP3).FirstOrDefault() : null; _bossP4Usurper ??= StateMachine.ActivePhaseIndex == 2 ? Enemies(OID.UsurperOfFrostP4).FirstOrDefault() : null; _bossP4Oracle ??= StateMachine.ActivePhaseIndex == 2 ? Enemies(OID.OracleOfDarknessP4).FirstOrDefault() : null; + _bossP5 ??= StateMachine.ActivePhaseIndex == 3 ? Enemies(OID.BossP5).FirstOrDefault() : null; } protected override void DrawEnemies(int pcSlot, Actor pc) { Arena.Actor(PrimaryActor, ArenaColor.Enemy); Arena.Actor(_bossP2, ArenaColor.Enemy); - Arena.Actor(_iceVeil, ArenaColor.Enemy); Arena.Actor(_bossP3, ArenaColor.Enemy); Arena.Actor(_bossP4Usurper, ArenaColor.Enemy); Arena.Actor(_bossP4Oracle, ArenaColor.Enemy); + Arena.Actor(_bossP5, ArenaColor.Enemy); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs index 939db4a30e..1eca31263c 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs @@ -124,4 +124,13 @@ public class FRUConfig() : ConfigNode() [PropertyDisplay("P2 Banish after Light Rampant: direction to move to resolve pairs", tooltip: "Only used by AI")] [PropertyCombo("CW", "CCW")] public bool P2Banish2MoveCCWToStack = true; + + [PropertyDisplay("P2 Intermission: clock spots (cardinals prioritize their crystals, intercardinals bait)", tooltip: "Only used by AI")] + [GroupDetails(["N", "NE", "E", "SE", "S", "SW", "W", "NW"])] + [GroupPreset("Default", [0, 2, 5, 3, 4, 6, 7, 1])] + public GroupAssignmentUnique P2IntermissionClockSpots = new() { Assignments = [0, 2, 5, 3, 4, 6, 7, 1] }; + + [PropertyDisplay("P3 Darkest Dance: baiter")] + [PropertyCombo("MT", "OT")] + public bool P3DarkestDanceOTBait; } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index 6ef3fe1608..1bca6f59da 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -37,6 +37,9 @@ 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 + + BossP5 = 0x45AF, // R7.000, x0 (spawn during fight) + FulgentBladeLine = 0x1EBBF7, // R0.500, x0 (spawn during fight), EventObj type } public enum AID : uint @@ -248,6 +251,24 @@ public enum AID : uint MemorysEndP4 = 40305, // OracleOfDarknessP4->self, 10.0s cast, range 100 circle, enrage AbsoluteZeroP4 = 40245, // UsurperOfFrostP4->self, 10.0s cast, range 100 circle, enrage ParadiseLost = 40263, // Helper->self, no cast, range 100 circle, wipe on p5 failure state + IntermissionP5Visual = 40231, // UsurperOfFrostP4->self, no cast, single-target, visual (intermission start) + IntermissionP5Start = 40232, // Helper->self, no cast, range 60 circle, stun + move players to a specific spot + + // P5 + AutoAttackP5 = 40114, // BossP5->self, no cast, single-target, visual (auto attack on both tanks) + AutoAttackP5AOE = 40115, // Helper->player, no cast, single-target, auto-attack + + FulgentBlade = 40306, // BossP5->self, 6.0s cast, range 100 circle, raidwide + mechanic start + PathOfLightFirst = 40307, // Helper->self, 7.0s cast, range 5 width 80 rect + PathOfLightRest = 40308, // Helper->self, no cast, range 5 width 80 rect + PathOfDarknessFirst = 40118, // Helper->self, 7.0s cast, range 5 width 80 rect + PathOfDarknessRest = 40309, // Helper->self, no cast, range 5 width 80 rect + + AkhMornPandora = 40310, // BossP5->self, 8.0s cast, single-target, visual (left/right stack) + AkhMornPandoraAOE1 = 40311, // Helper->players, no cast, range 4 circle, 4-man stack + AkhMornPandoraAOE2 = 40312, // Helper->players, no cast, range 4 circle, 4-man stack + + ParadiseRegained = 40319, // BossP5->self, 4.0s cast, single-target, visual (mechanic start) } public enum SID : uint @@ -307,4 +328,5 @@ public enum TetherID : uint UltimateRelativitySlow = 133, // DelightsHourglass->BossP3 UltimateRelativityQuicken = 134, // DelightsHourglass->BossP3 MornAfahHPCheck = 1, // UsurperOfFrostP4->OracleOfDarknessP4 + MornAfahHPFail = 2, // UsurperOfFrostP4->OracleOfDarknessP4 } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index c731939e52..905ebb0a6d 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -4,6 +4,8 @@ class FRUStates : StateMachineBuilder { private readonly FRU _module; + private static bool IsActorDead(Actor? a, bool valueIfNull) => a == null ? valueIfNull : (a.IsDeadOrDestroyed || a.HPMP.CurHP <= 1); + public FRUStates(FRU module) : base(module) { _module = module; @@ -11,10 +13,13 @@ public FRUStates(FRU module) : base(module) .Raw.Update = () => Module.PrimaryActor.IsDeadOrDestroyed; SimplePhase(1, Phase2, "P2: Usurper of Frost") .SetHint(StateMachine.PhaseHint.StartWithDowntime) - .Raw.Update = () => !Module.PrimaryActor.IsDead || (_module.BossP2()?.IsDeadOrDestroyed ?? false) || (_module.IceVeil()?.IsDeadOrDestroyed ?? false); + .Raw.Update = () => !Module.PrimaryActor.IsDead || (_module.BossP2()?.IsDestroyed ?? false) || (_module.IceVeil()?.IsDeadOrDestroyed ?? false); SimplePhase(2, Phase34, "P3/4: Oracle of Darkness & Both") .SetHint(StateMachine.PhaseHint.StartWithDowntime) - .Raw.Update = () => !Module.PrimaryActor.IsDead || (_module.BossP2()?.IsDeadOrDestroyed ?? false) && (_module.BossP3()?.IsDeadOrDestroyed ?? true) && (_module.BossP4Oracle()?.IsDeadOrDestroyed ?? true); + .Raw.Update = () => !Module.PrimaryActor.IsDead || (_module.BossP2()?.IsDestroyed ?? false) && (_module.BossP3()?.IsDestroyed ?? true) && IsActorDead(_module.BossP4Oracle(), true) && IsActorDead(_module.BossP4Usurper(), true); + SimplePhase(3, Phase5, "P5: Pandora") + .SetHint(StateMachine.PhaseHint.StartWithDowntime) + .Raw.Update = () => !Module.PrimaryActor.IsDead || (_module.BossP4Oracle()?.IsDeadOrDestroyed ?? true) && (_module.BossP5()?.IsDeadOrDestroyed ?? true); } private void Phase1(uint id) @@ -56,6 +61,13 @@ private void Phase34(uint id) P4CrystallizeTime(id + 0x130000, 4.6f); P4AkhMornMornAfah(id + 0x140000, 0.1f); P4Enrage(id + 0x150000, 2.3f); + } + + private void Phase5(uint id) + { + P5Start(id, 77); + P5FulgentBlade(id + 0x10000, 5.3f); + P5ParadiseRegained(id + 0x20000, 4.2f); SimpleState(id + 0xFF0000, 100, "???"); } @@ -365,14 +377,14 @@ private void P2AbsoluteZero(uint id, float delay) ComponentCondition(id + 3, 2.3f, comp => comp.NumCasts > 0, "Knockback") .DeactivateOnExit(); - ComponentCondition(id + 0x1000, 18.9f, comp => comp.ActiveActors.Any(), "Crystals appear") - .ActivateOnEnter() + ComponentCondition(id + 0x1000, 18.9f, comp => comp.CrystalsActive, "Crystals appear") + .ActivateOnEnter() + .ActivateOnEnter() .SetHint(StateMachine.StateHint.DowntimeEnd); ActorCast(id + 0x1010, _module.IceVeil, AID.EndlessIceAge, 4.7f, 40, true, "Enrage") - .ActivateOnEnter() .ActivateOnEnter() .ActivateOnEnter() - .DeactivateOnExit() + .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit(); @@ -452,40 +464,46 @@ private void P3Apocalypse(uint id, float delay) ComponentCondition(id + 0x12, 0.6f, comp => comp.NumStatuses >= 6) .ActivateOnEnter() .ActivateOnEnter() + .ActivateOnEnter() .ExecOnExit(comp => comp.ShowOrder(1)); ActorCast(id + 0x20, _module.BossP3, AID.Apocalypse, 2.6f, 4, true); ActorCastStart(id + 0x30, _module.BossP3, AID.SpiritTaker, 2.2f, true); - ComponentCondition(id + 0x31, 1.3f, comp => comp.Stacks.Count == 0, "Stack 1"); + ComponentCondition(id + 0x31, 1.3f, comp => comp.Stacks.Count == 0, "Stack 1") + .DeactivateOnExit(); ActorCastEnd(id + 0x32, _module.BossP3, 1.7f, true) - .ActivateOnEnter(); - ComponentCondition(id + 0x33, 0.3f, comp => comp.Spreads.Count == 0, "Jump") - .DeactivateOnExit(); + .ActivateOnEnter(); + ComponentCondition(id + 0x33, 0.3f, comp => comp.Spreads.Count == 0, "Jump") + .DeactivateOnExit(); ActorCastStart(id + 0x40, _module.BossP3, AID.ApocalypseDarkEruption, 6.2f, true) .ExecOnEnter(comp => comp.Show(8.5f)) .ActivateOnEnter(); - ComponentCondition(id + 0x41, 2.4f, comp => comp.NumCasts > 0, "Apocalypse start"); + ComponentCondition(id + 0x41, 2.4f, comp => comp.NumCasts >= 4, "Apocalypse start"); ActorCastEnd(id + 0x42, _module.BossP3, 1.6f, true); - ComponentCondition(id + 0x43, 0.4f, comp => comp.NumCasts > 4); + ComponentCondition(id + 0x43, 0.4f, comp => comp.NumCasts >= 10); ComponentCondition(id + 0x44, 0.7f, comp => comp.NumFinishedSpreads > 0, "Spread") .DeactivateOnExit() .ExecOnExit(comp => comp.ShowOrder(2)); - ComponentCondition(id + 0x45, 1.3f, comp => comp.NumCasts > 10); + ComponentCondition(id + 0x45, 1.3f, comp => comp.NumCasts >= 16) + .ActivateOnEnter(); ActorCastStart(id + 0x50, _module.BossP3, AID.DarkestDance, 1.3f, true); - ComponentCondition(id + 0x51, 0.7f, comp => comp.NumCasts > 16); - ComponentCondition(id + 0x52, 2.0f, comp => comp.NumCasts > 22); + ComponentCondition(id + 0x51, 0.7f, comp => comp.NumCasts >= 22); + ComponentCondition(id + 0x52, 2.0f, comp => comp.NumCasts >= 28); ComponentCondition(id + 0x53, 0.5f, comp => comp.Stacks.Count == 0, "Stack 2"); - ComponentCondition(id + 0x54, 1.5f, comp => comp.NumCasts > 28) + ComponentCondition(id + 0x54, 1.5f, comp => comp.NumCasts >= 34) .ActivateOnEnter() .DeactivateOnExit(); ActorCastEnd(id + 0x55, _module.BossP3, 0.3f, true); ComponentCondition(id + 0x56, 0.4f, comp => comp.NumCasts > 0, "Tankbuster") .ActivateOnEnter() + .DeactivateOnExit() .DeactivateOnExit() .ExecOnExit(comp => comp.ShowOrder(3)) .SetHint(StateMachine.StateHint.Tankbuster); ComponentCondition(id + 0x57, 2.8f, comp => comp.NumCasts > 0, "Knockback") + .ActivateOnEnter() .DeactivateOnExit(); ComponentCondition(id + 0x60, 4.1f, comp => comp.Stacks.Count == 0, "Stack 3") + .DeactivateOnExit() .DeactivateOnExit(); P3ShockwavePulsar(id + 0x1000, 0.3f); @@ -528,10 +546,10 @@ private void P4DarklitDragonsong(uint id, float delay) ComponentCondition(id + 0x21, 0.8f, comp => comp.NumCasts > 0, "Proteans") .DeactivateOnExit(); ActorCastEnd(id + 0x22, _module.BossP4Oracle, 2.2f, true) - .ActivateOnEnter(); + .ActivateOnEnter(); ActorCastStartMulti(id + 0x23, _module.BossP4Usurper, [AID.HallowedWingsL, AID.HallowedWingsR], 0.1f, true); - ComponentCondition(id + 0x24, 0.3f, comp => comp.Spreads.Count == 0, "Jump") - .DeactivateOnExit(); + ComponentCondition(id + 0x24, 0.3f, comp => comp.Spreads.Count == 0, "Jump") + .DeactivateOnExit(); ActorCastStart(id + 0x25, _module.BossP4Oracle, AID.SomberDance, 2.8f) .ActivateOnEnter() .ActivateOnEnter() @@ -612,10 +630,10 @@ private void P4CrystallizeTime(uint id, float delay) .DeactivateOnExit(); ActorCastStart(id + 0x90, _module.BossP4Oracle, AID.SpiritTaker, 0.4f); ActorCastStart(id + 0x91, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings1, 2.2f) - .ActivateOnEnter(); + .ActivateOnEnter(); ActorCastEnd(id + 0x92, _module.BossP4Oracle, 0.8f); - ComponentCondition(id + 0x93, 0.3f, comp => comp.Spreads.Count == 0, "Jump") - .DeactivateOnExit(); + ComponentCondition(id + 0x93, 0.3f, comp => comp.Spreads.Count == 0, "Jump") + .DeactivateOnExit(); ComponentCondition(id + 0x94, 3.3f, comp => comp.ReturnDone, "Rewind return") .DeactivateOnExit(); ActorCastEnd(id + 0x95, _module.BossP4Usurper, 0.3f); @@ -630,4 +648,35 @@ private void P4Enrage(uint id, float delay) { ActorCast(id, _module.BossP4Usurper, AID.AbsoluteZeroP4, delay, 10, true, "Enrage"); } + + private void P5Start(uint id, float delay) + { + ActorTargetable(id, _module.BossP5, true, delay, "Boss appears") + .SetHint(StateMachine.StateHint.DowntimeEnd); + } + + private void P5FulgentBlade(uint id, float delay) + { + ActorCast(id, _module.BossP5, AID.FulgentBlade, delay, 6, true, "Raidwide") + .ActivateOnEnter() + .SetHint(StateMachine.StateHint.Raidwide); + ComponentCondition(id + 0x10, 4.1f, comp => comp.Active); + ComponentCondition(id + 0x20, 7, comp => comp.NumCasts > 0, "Exaline 1"); + ComponentCondition(id + 0x21, 2, comp => comp.NumCasts > 1, "Exaline 2"); + ComponentCondition(id + 0x22, 2, comp => comp.NumCasts > 2, "Exaline 3"); + ComponentCondition(id + 0x23, 2, comp => comp.NumCasts > 3, "Exaline 4"); + ActorCastStart(id + 0x30, _module.BossP5, AID.AkhMornPandora, 1.8f, true); + ComponentCondition(id + 0x31, 0.2f, comp => comp.NumCasts > 4, "Exaline 5") + .ActivateOnEnter(); + ComponentCondition(id + 0x32, 2, comp => comp.NumCasts > 5, "Exaline 6"); + ActorCastEnd(id + 0x33, _module.BossP5, 5.8f, true); + ComponentCondition(id + 0x34, 0.1f, comp => comp.Source == null, "Left/right stack") + .DeactivateOnExit() + .DeactivateOnExit(); // TODO: there are still lines going, but they are far... + } + + private void P5ParadiseRegained(uint id, float delay) + { + ActorCast(id, _module.BossP5, AID.ParadiseRegained, delay, 4, true, "???"); + } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs index 379dadb651..45bc972a3c 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P1UtopianSky.cs @@ -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.PrecisePosition(Module.Center + range * direction.ToDirection(), new(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f), _aoes.Activation); } } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs index 7e3790a245..55c66b57d2 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs @@ -2,7 +2,7 @@ class P2AbsoluteZero(BossModule module) : Components.CastCounter(module, ActionID.MakeSpell(AID.AbsoluteZeroAOE)); -class P2SwellingFrost(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.SwellingFrost)) // TODO: verify whether it ignores KB +class P2SwellingFrost(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.SwellingFrost), true) { private readonly DateTime _activation = module.WorldState.FutureTime(3.2f); @@ -13,5 +13,135 @@ public override IEnumerable Sources(int slot, Actor actor) } class P2SinboundBlizzard(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.SinboundBlizzardAOE), new AOEShapeCone(50, 10.Degrees())); -class P2HiemalStorm(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HiemalStormAOE), 7); + +class P2HiemalStorm(BossModule module) : Components.LocationTargetedAOEs(module, ActionID.MakeSpell(AID.HiemalStormAOE), 7) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // storms are cast every 3s, ray voidzones appear every 2s; to place voidzones more tightly, we pretend radius is smaller during first half of cast + var deadline = WorldState.FutureTime(1.5f); + foreach (var c in Casters) + { + var activation = Module.CastFinishAt(c.CastInfo); + hints.AddForbiddenZone(ShapeDistance.Circle(c.CastInfo!.LocXZ, activation > deadline ? 4 : 7), activation); + } + } +} + class P2HiemalRay(BossModule module) : Components.PersistentVoidzoneAtCastTarget(module, 4, ActionID.MakeSpell(AID.HiemalRay), module => module.Enemies(OID.HiemalRayVoidzone).Where(z => z.EventState != 7), 0.7f); + +// TODO: show hint if ice veil is clipped +class P2Intermission(BossModule module) : Components.GenericBaitAway(module) +{ + private readonly FRUConfig _config = Service.Config.Get(); + private readonly P2SinboundBlizzard? _cones = module.FindComponent(); + private readonly IReadOnlyList _crystalsOfLight = module.Enemies(OID.CrystalOfLight); + private readonly IReadOnlyList _crystalsOfDarkness = module.Enemies(OID.CrystalOfDarkness); + private readonly IReadOnlyList _iceVeil = module.Enemies(OID.IceVeil); + private bool _iceVeilInvincible = true; + + public bool CrystalsActive => CrystalsOfLight.Any(); + + public override void Update() + { + IgnoreOtherBaits = true; + CurrentBaits.Clear(); + if (_cones == null) + return; + foreach (var c in _crystalsOfDarkness) + { + var baiter = Raid.WithoutSlot().Closest(c.Position); + if (baiter != null) + CurrentBaits.Add(new(c, baiter, _cones.Shape)); + } + } + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + // enemy priorities + var clockSpot = _config.P2IntermissionClockSpots[assignment]; + foreach (var e in hints.PotentialTargets) + { + e.Priority = (OID)e.Actor.OID switch + { + OID.CrystalOfLight => CrystalPriority(e.Actor, clockSpot), + OID.CrystalOfDarkness => AIHints.Enemy.PriorityForbidFully, + OID.IceVeil => _iceVeilInvincible ? AIHints.Enemy.PriorityForbidFully : 1, + _ => 0 + }; + } + + // don't stand inside light crystals, to avoid bad puddle baits + foreach (var c in CrystalsOfLight) + hints.AddForbiddenZone(ShapeDistance.Circle(c.Position, 4), WorldState.FutureTime(30)); + + // mechanic resolution + if (clockSpot < 0) + { + // no assignment, oh well... + } + else if ((clockSpot & 1) == 0) + { + // cardinals - bait puddles accurately + var assignedDir = (180 - 45 * clockSpot).Degrees(); + var assignedPosition = Module.Center + 15 * assignedDir.ToDirection(); // crystal is at R=15 + var assignedCrystal = CrystalsOfLight.FirstOrDefault(c => c.Position.AlmostEqual(assignedPosition, 2)); + if (assignedCrystal != null) + { + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(assignedPosition, 5), WorldState.FutureTime(60)); + hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 17), DateTime.MaxValue); // prefer to stay near border, unless everything else is covered with aoes + } + else + { + // go to the ice veil + // TODO: consider helping other melees with their crystals? a bit dangerous, can misbait + // TODO: consider helping nearby ranged to bait their cones? + hints.AddForbiddenZone(ShapeDistance.InvertedCone(Module.Center, 7, assignedDir, 10.Degrees()), DateTime.MaxValue); + } + } + else + { + // intercardinals - bait cones + if (_cones?.Casters.Count == 0) + { + var assignedPosition = Module.Center + 9 * (180 - 45 * clockSpot).Degrees().ToDirection(); // crystal is at R=8 + var assignedCrystal = CrystalsOfDarkness.FirstOrDefault(c => c.Position.AlmostEqual(assignedPosition, 2)); + if (assignedCrystal != null) + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(assignedPosition, new WDir(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f)); + } + // else: just dodge cones etc... + } + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + Arena.Actors(CrystalsOfLight, ArenaColor.Enemy); + Arena.Actors(CrystalsOfDarkness, ArenaColor.Object); + Arena.Actor(IceVeil, _iceVeilInvincible ? ArenaColor.Object : ArenaColor.Enemy); + } + + public override void OnStatusLose(Actor actor, ActorStatus status) + { + if ((SID)status.ID == SID.Invincibility) + _iceVeilInvincible = false; + } + + private IEnumerable ActiveActors(IReadOnlyList raw) => raw.Where(a => a.IsTargetable && !a.IsDead); + private IEnumerable CrystalsOfLight => ActiveActors(_crystalsOfLight); + private IEnumerable CrystalsOfDarkness => ActiveActors(_crystalsOfDarkness); + private Actor? IceVeil => ActiveActors(_iceVeil).FirstOrDefault(); + + private int CrystalPriority(Actor crystal, int clockSpot) + { + var offset = crystal.Position - Module.Center; + var priority = clockSpot switch + { + 0 => offset.Z < -10, + 2 => offset.X > +10, + 4 => offset.Z > +10, + 6 => offset.X < -10, + _ => false + }; + return priority ? 2 : 1; + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs index a60249fab5..a56860c428 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2Banish.cs @@ -113,6 +113,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme public override void OnEventCast(Actor caster, ActorCastEvent spell) { + base.OnEventCast(caster, 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 50a6922110..118558bb47 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2LightRampant.cs @@ -302,7 +302,6 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme return; // actual orb aoes - //var resolve = Module.CastFinishAt(_orbs.Casters[0].CastInfo, _orbs.NumCasts == 0 ? 0 : -1); // for second set, we want to dodge out asap without weird movement to the center foreach (var c in _orbs.ActiveCasters) hints.AddForbiddenZone(_orbs.Shape.Distance(c.Position, default), Module.CastFinishAt(c.CastInfo)); diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs index ec6c2c8e5c..39ed27a2b8 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs @@ -1,9 +1,11 @@ -namespace BossMod.Dawntrail.Ultimate.FRU; +using System.ComponentModel; + +namespace BossMod.Dawntrail.Ultimate.FRU; class P3Apocalypse(BossModule module) : Components.GenericAOEs(module) { - private Angle? _starting; - private Angle _rotation; + public Angle? Starting; + public Angle Rotation; private readonly List _aoes = []; private static readonly AOEShapeCircle _shape = new(9); @@ -18,14 +20,14 @@ void addPair(WDir offset, DateTime activation) } void addAt(int position, DateTime activation) { - if (position >= 0 && _starting != null) - addPair(14 * (_starting.Value + _rotation * position).ToDirection(), activation); + if (position >= 0 && Starting != null) + addPair(14 * (Starting.Value + Rotation * position).ToDirection(), activation); else if (position == -1) addPair(default, activation); } var activation = WorldState.FutureTime(delay); - for (int i = -1; i < 6; ++i) + for (int i = -1; i < 5; ++i) { addAt(i + 1, activation); addAt(i, activation); @@ -36,16 +38,7 @@ void addAt(int position, DateTime activation) public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes.Take(6); - public override void DrawArenaForeground(int pcSlot, Actor pc) - { - // draw safespots (TODO: improve - account for concrete assignments, show different spots at different mechanic stages) - if (_aoes.Count > 0 && NumCasts < 16 && _starting != null) - { - var safeOff = 10 * (_starting.Value - _rotation).ToDirection(); - Arena.AddCircle(Module.Center + safeOff, 1, ArenaColor.Safe); - Arena.AddCircle(Module.Center - safeOff, 1, ArenaColor.Safe); - } - } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // we have dedicated components for this... public override void OnActorCreated(Actor actor) { @@ -53,17 +46,17 @@ public override void OnActorCreated(Actor actor) { if (actor.Position.AlmostEqual(Module.Center, 1)) { - if (_starting == null) - _starting = actor.Rotation; - else if (!_starting.Value.AlmostEqual(actor.Rotation + 180.Degrees(), 0.1f)) + if (Starting == null) + Starting = actor.Rotation; + else if (!Starting.Value.AlmostEqual(actor.Rotation + 180.Degrees(), 0.1f)) ReportError($"Inconsistent starting dir"); } else { var rot = 0.5f * (actor.Rotation - Angle.FromDirection(actor.Position - Module.Center)).Normalized(); - if (_rotation == default) - _rotation = rot; - else if (!_rotation.AlmostEqual(rot, 0.1f)) + if (Rotation == default) + Rotation = rot; + else if (!Rotation.AlmostEqual(rot, 0.1f)) ReportError($"Inconsistent rotation dir"); } } @@ -89,6 +82,7 @@ public struct State { public int Order; public int AssignedGroup; + public int AssignedPosition; public DateTime Expiration; } @@ -122,6 +116,8 @@ public override void AddGlobalHints(GlobalHints hints) hints.Add(_swaps); } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // we have dedicated components for this... + public override void OnStatusGain(Actor actor, ActorStatus status) { if ((SID)status.ID == SID.SpellInWaitingDarkWater && Raid.FindSlot(actor.InstanceID) is var slot && slot >= 0) @@ -146,12 +142,11 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) private void InitAssignments() { - Span assignmentPerSlot = [-1, -1, -1, -1, -1, -1, -1, -1]; Span slotPerAssignment = [-1, -1, -1, -1, -1, -1, -1, -1]; foreach (var (slot, group) in _config.P3ApocalypseAssignments.Resolve(Raid)) { States[slot].AssignedGroup = group < 4 ? 1 : 2; - assignmentPerSlot[slot] = group; + States[slot].AssignedPosition = group & 3; slotPerAssignment[group] = slot; } @@ -159,18 +154,52 @@ private void InitAssignments() return; // no valid assignments var swap = _config.P3ApocalypseUptime ? FindUptimeSwap(slotPerAssignment) : FindStandardSwap(slotPerAssignment); + //var debugSwap = swap.Raw; for (int role = 0; role < slotPerAssignment.Length; ++role) { - if (swap[role]) + if (!swap[role]) + continue; + var slot = slotPerAssignment[role]; + ref var state = ref States[slot]; + + // find partner to swap with; prioritize same position > neighbour position (eg melee with melee) > anyone that also swaps + int partnerRole = -1, partnerQuality = -1; + for (int candidateRole = role + 1; candidateRole < slotPerAssignment.Length; ++candidateRole) { - var slot = slotPerAssignment[role]; - ref var state = ref States[slot]; - state.AssignedGroup = 3 - state.AssignedGroup; - if (_swaps.Length > 0) - _swaps += ", "; - _swaps += Raid[slot]?.Name ?? ""; + if (!swap[candidateRole]) + continue; // this guy doesn't want to swap, skip + var candidateSlot = slotPerAssignment[candidateRole]; + if (States[candidateSlot].AssignedGroup == state.AssignedGroup) + continue; // this guy is from same group, skip + var positionDiff = state.AssignedPosition ^ States[candidateSlot].AssignedPosition; + var candidateQuality = positionDiff switch + { + 0 => 2, // same position, best + 1 => 1, // melee with melee / ranged with ranged (assuming sane config) + _ => 0, // melee with ranged + }; + if (candidateQuality > partnerQuality) + { + partnerRole = candidateRole; + partnerQuality = candidateQuality; + } } + + if (partnerRole < 0) + { + ReportError($"Failed to find swap for {slot}"); + continue; + } + + swap.Clear(role); + swap.Clear(partnerRole); + var partnerSlot = slotPerAssignment[partnerRole]; + ref var partnerState = ref States[partnerSlot]; + Utils.Swap(ref state.AssignedGroup, ref partnerState.AssignedGroup); + Utils.Swap(ref state.AssignedPosition, ref partnerState.AssignedPosition); + _swaps += $"{(_swaps.Length > 0 ? ", " : "")}{Raid[slot]?.Name} <-> {Raid[partnerSlot]?.Name}"; } + //ReportError($"FOO: {debugSwap:X2} == {_swaps}"); _swaps = $"Swaps: {(_swaps.Length > 0 ? _swaps : "none")}"; } @@ -221,22 +250,82 @@ private BitMask FindStandardSwap(ReadOnlySpan slotPerAssignment) } } -class P3SpiritTaker(BossModule module) : Components.UniformStackSpread(module, 0, 5) +class P3ApocalypseSpiritTaker(BossModule module) : SpiritTaker(module) { - public override void OnCastStarted(Actor caster, ActorCastInfo spell) + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if ((AID)spell.Action.ID == AID.SpiritTaker) - AddSpreads(Raid.WithoutSlot(true), Module.CastFinishAt(spell, 0.3f)); + base.AddAIHints(slot, actor, assignment, hints); + hints.AddForbiddenZone(ShapeDistance.Circle(Module.Center, 6), DateTime.MaxValue); // don't dodge into center... } +} - public override void OnEventCast(Actor caster, ActorCastEvent spell) +class P3ApocalypseDarkEruption(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.DarkEruption, ActionID.MakeSpell(AID.DarkEruption), 6, 5.1f) +{ + private readonly P3Apocalypse? _apoc = module.FindComponent(); + private readonly P3ApocalypseDarkWater? _water = module.FindComponent(); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if ((AID)spell.Action.ID == AID.SpiritTakerAOE) - Spreads.Clear(); + var safeSpot = SafeOffset(slot, out _); + if (safeSpot != default) + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(Module.Center + safeSpot, new(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f), Spreads.Count > 0 ? Spreads[0].Activation : DateTime.MaxValue); } -} -class P3ApocalypseDarkEruption(BossModule module) : Components.SpreadFromIcon(module, (uint)IconID.DarkEruption, ActionID.MakeSpell(AID.DarkEruption), 6, 5.1f); + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + base.DrawArenaForeground(pcSlot, pc); + + // draw safespots + var safeSpot = SafeOffset(pcSlot, out var refSafeSpot); + if (safeSpot != default) + { + Arena.AddCircle(Module.Center + safeSpot, 1, ArenaColor.Safe); + if (refSafeSpot != safeSpot) + Arena.AddCircle(Module.Center + refSafeSpot, 1, ArenaColor.Danger); + } + else if (refSafeSpot != default) + { + // we don't have assignments, at least draw two reference ones + Arena.AddCircle(Module.Center + refSafeSpot, 1, ArenaColor.Danger); + Arena.AddCircle(Module.Center - refSafeSpot, 1, ArenaColor.Danger); + } + } + + private WDir SafeOffset(int slot, out WDir reference) + { + reference = default; + if (_apoc?.Starting == null || _water == null) + return default; + + var midDir = (_apoc.Starting.Value - _apoc.Rotation).Normalized(); + reference = 10 * midDir.ToDirection(); + + ref var state = ref _water.States[slot]; + if (state.AssignedGroup == 0) + return default; // no assignments - oh well, at least we know reference directions + + // G1 takes dir CCW from N, G2 takes 0/45/90/135 + var midIsForG2 = midDir.Deg is >= -20 and < 160; + if (midIsForG2 != (state.AssignedGroup == 2)) + { + midDir += 180.Degrees(); + reference = -reference; + } + + if ((state.AssignedPosition & 2) == 0) + { + // melee spot; note that non-reference melee goes in right after second apoc (max range is 14-9) + var altPos = _apoc.Rotation.Rad < 0 ? 1 : 0; + return state.AssignedPosition == altPos ? (_apoc.NumCasts > 4 ? 4.5f : 10) * (midDir - _apoc.Rotation).ToDirection() : reference; + } + else + { + // ranged spot + var offset = (state.AssignedPosition == 2 ? -15 : +15).Degrees(); + return 19 * (midDir + offset).ToDirection(); + } + } +} class P3DarkestDanceBait(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.DarkestDanceBait), centerAtTarget: true) { @@ -254,6 +343,8 @@ public override void Update() } } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // we have dedicated components for this... + public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.DarkestDance) @@ -267,13 +358,13 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) class P3DarkestDanceKnockback(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.DarkestDanceKnockback), true) { - private Actor? _source; - private DateTime _activation; + public Actor? Source; + public DateTime Activation; public override IEnumerable Sources(int slot, Actor actor) { - if (_source != null) - yield return new(_source.Position, 21, _activation); + if (Source != null) + yield return new(Source.Position, 21, Activation); } public override void OnEventCast(Actor caster, ActorCastEvent spell) @@ -281,13 +372,113 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) switch ((AID)spell.Action.ID) { case AID.DarkestDanceBait: - _source = caster; - _activation = WorldState.FutureTime(2.8f); + Source = caster; + Activation = WorldState.FutureTime(2.8f); break; case AID.DarkestDanceKnockback: - _source = null; ++NumCasts; break; } } } + +// position for first dark water - note that this is somewhat arbitrary (range etc) +class P3ApocalypseAIWater1(BossModule module) : BossComponent(module) +{ + private readonly P3ApocalypseDarkWater? _water = module.FindComponent(); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_water == null) + return; + ref var state = ref _water.States[slot]; + if (state.AssignedGroup == 0) + return; // no assignment (yet?) + + var (dir, range) = state.AssignedPosition switch + { + 0 => (-15.Degrees(), 5), + 1 => (15.Degrees(), 5), + 2 => (-10.Degrees(), 8), + 3 => (10.Degrees(), 8), + _ => (default, 0) + }; + dir += (state.AssignedGroup == 1 ? -90 : 90).Degrees(); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + range * dir.ToDirection(), 1), _water.Stacks.Count > 0 ? _water.Stacks[0].Activation : DateTime.MaxValue); + } +} + +// position for second dark water & darkest dance - for simplicity, we position in the direction tank would take darkest dance +class P3ApocalypseAIWater2(BossModule module) : BossComponent(module) +{ + private readonly FRUConfig _config = Service.Config.Get(); + private readonly P3Apocalypse? _apoc = module.FindComponent(); + private readonly P3ApocalypseDarkWater? _water = module.FindComponent(); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_apoc?.Starting == null || _water == null) + return; + + // add imminent apoc aoes + foreach (var aoe in _apoc.ActiveAOEs(slot, actor)) + hints.AddForbiddenZone(aoe.Shape.Distance(aoe.Origin, aoe.Rotation), aoe.Activation); + + ref var state = ref _water.States[slot]; + if (state.AssignedGroup == 0) + return; // no assignments - oh well + + var midDir = (_apoc.Starting.Value - _apoc.Rotation).Normalized(); + + // G1 takes dir CCW from N, G2 takes 0/45/90/135 + var midIsForG2 = midDir.Deg is >= -20 and < 160; + if (midIsForG2 != (state.AssignedGroup == 2)) + midDir += 180.Degrees(); + + var distance = 4.5f; + if (_apoc.NumCasts >= 28 && assignment == (_config.P3DarkestDanceOTBait ? PartyRolesConfig.Assignment.OT : PartyRolesConfig.Assignment.MT)) + { + // bait darkest dance (but make sure to share water first!) + distance = _water.Stacks.Count == 0 ? 19 : 8; + } + + var destOff = distance * (midDir - _apoc.Rotation).ToDirection(); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center + destOff, 1), DateTime.MaxValue); + } +} + +// position for darkest dance knockback & third dark water +class P3ApocalypseAIWater3(BossModule module) : BossComponent(module) +{ + private readonly P3ApocalypseDarkWater? _water = module.FindComponent(); + private readonly P3DarkestDanceKnockback? _knockback = module.FindComponent(); + + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (_water == null || _knockback?.Source == null) + return; + + var toCenter = Module.Center - _knockback.Source.Position; + if (toCenter.LengthSq() < 1) + return; // did not jump yet, wait... + + var angle = 30.Degrees(); //(_knockback.NumCasts == 0 ? 30 : 45).Degrees(); + var dir = Angle.FromDirection(toCenter) + _water.States[slot].AssignedGroup switch + { + 1 => -angle, + 2 => angle, + _ => default + }; + + if (_knockback.NumCasts == 0) + { + // preposition for knockback + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(_knockback.Source.Position + 2 * dir.ToDirection(), new(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f), _knockback.Activation); + } + else if (_water.Stacks.Count > 0) + { + // stack at maxmelee + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(_knockback.Source.Position + 10 * dir.ToDirection(), 1), _water.Stacks[0].Activation); + } + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs index 81b72dfbb6..e8034423e2 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs @@ -8,6 +8,7 @@ public struct PlayerState public int RewindOrder; public int LaserOrder; public bool HaveDarkEruption; // all dark eruptions have rewind order 1 + public bool HaveDarkBlizzard; public WDir AssignedDir; public WPos ReturnPos; } @@ -18,7 +19,7 @@ public struct PlayerState private readonly FRUConfig _config = Service.Config.Get(); private WDir _relNorth; private int _numYellowTethers; - private DateTime _nextProgress; + private DateTime _nextImminent = module.WorldState.FutureTime(21.9f - 2.5f); // approx 2.5s before next step resolves public const float RangeHintOut = 12; // explosion radius is 8 public const float RangeHintStack = 1; @@ -40,17 +41,55 @@ public override void AddHints(int slot, Actor actor, TextHints hints) hints.Add(hint, false); } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + if (States[slot].AssignedDir != default) + { + var range = RangeHint(States[slot], actor.Class.IsSupport(), NumCasts); + switch (range) + { + case RangeHintOut: + if (WorldState.CurrentTime < _nextImminent) + { + // there's still time, around maxmelee at assigned direction + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(SafeSpot(slot, 9), 1), _nextImminent); + } + else + { + // ok, out is imminent, gtfo - we need to avoid clipping people, avoid dark blizzard (if it's being resolved now), and avoid lasers (if any) + var avoidBlizzard = NumCasts == 2; + foreach (var (i, p) in Raid.WithSlot().Exclude(slot)) + { + var avoidRadius = avoidBlizzard && States[i].HaveDarkBlizzard ? 12 : 8; + hints.AddForbiddenZone(ShapeDistance.Circle(p.Position, avoidRadius + 1)); + } + var lasers = Module.FindComponent(); + if (lasers != null) + foreach (var laser in lasers.ActiveAOEs(slot, actor)) + hints.AddForbiddenZone(laser.Shape.Distance(laser.Origin, laser.Rotation), laser.Activation); + } + break; + case RangeHintLaser: + case RangeHintDarkEruption: + case RangeHintEye: + // go to exact safespot + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(SafeSpot(slot, range), new(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f), _nextImminent); + break; + default: + // go to mid + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 1), _nextImminent); + break; + } + } + } + public override void DrawArenaForeground(int pcSlot, Actor pc) { var assignedDir = States[pcSlot].AssignedDir; if (assignedDir != default && NumCasts < 6) { Arena.AddLine(Module.Center, Module.Center + Module.Bounds.Radius * assignedDir, ArenaColor.Safe); - - var safespot = Module.Center + RangeHint(States[pcSlot], pc.Class.IsSupport(), NumCasts) * assignedDir; - if (IsBaitingLaser(States[pcSlot], NumCasts) && LaserRotationAt(safespot) is var rot && rot != default) - safespot += 2 * (Angle.FromDirection(assignedDir) - 4.5f * rot).ToDirection(); - Arena.AddCircle(safespot, 1, ArenaColor.Safe); + Arena.AddCircle(SafeSpot(pcSlot, RangeHint(States[pcSlot], pc.Class.IsSupport(), NumCasts)), 1, ArenaColor.Safe); } } @@ -80,7 +119,10 @@ public override void OnStatusGain(Actor actor, ActorStatus status) case SID.SpellInWaitingDarkBlizzard: slot = Raid.FindSlot(actor.InstanceID); if (slot >= 0) + { States[slot].LaserOrder = actor.Class.IsSupport() ? 2 : 1; + States[slot].HaveDarkBlizzard = true; + } break; case SID.SpellInWaitingDarkEruption: slot = Raid.FindSlot(actor.InstanceID); @@ -137,10 +179,10 @@ public override void OnTethered(Actor source, ActorTetherInfo tether) public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if ((AID)spell.Action.ID is AID.UltimateRelativityUnholyDarkness or AID.UltimateRelativitySinboundMeltdownAOEFirst && WorldState.CurrentTime > _nextProgress) + if ((AID)spell.Action.ID is AID.UltimateRelativityUnholyDarkness or AID.UltimateRelativitySinboundMeltdownAOEFirst && WorldState.CurrentTime > _nextImminent) { ++NumCasts; - _nextProgress = WorldState.FutureTime(1); + _nextImminent = WorldState.FutureTime(2.5f); } } @@ -214,7 +256,6 @@ private void InitAssignments() _ => RangeHintChill }; - // TODO: rethink this... private string Hint(in PlayerState state, bool isSupport, int order) => order switch { 0 => state.FireOrder == 1 ? "Out" : "Stack", // 10s @@ -225,10 +266,21 @@ private void InitAssignments() 5 => state.LaserOrder == 3 ? "Laser" : "Chill", // 35s _ => "Look out" }; + + private WPos SafeSpot(int slot, float range) + { + var assignedDir = States[slot].AssignedDir; + var safespot = Module.Center + range * assignedDir; + if (IsBaitingLaser(States[slot], NumCasts) && LaserRotationAt(safespot) is var rot && rot != default) + safespot += 1.5f * (Angle.FromDirection(assignedDir) - 4.5f * rot).ToDirection(); + return safespot; + } } class P3UltimateRelativityDarkFireUnholyDarkness(BossModule module) : Components.UniformStackSpread(module, 6, 8, 5, alwaysShowSpreads: true) { + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // handled by main component + public override void OnStatusGain(Actor actor, ActorStatus status) { switch ((SID)status.ID) @@ -285,6 +337,8 @@ public override void AddHints(int slot, Actor actor, TextHints hints) hints.Add("GTFO from baited aoe!"); } + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // handled by main component + public override void DrawArenaForeground(int pcSlot, Actor pc) { base.DrawArenaForeground(pcSlot, pc); @@ -321,6 +375,8 @@ class P3UltimateRelativitySinboundMeltdownAOE(BossModule module) : Components.Ge public override IEnumerable ActiveAOEs(int slot, Actor actor) => _aoes; + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // handled by main component + public override void OnEventCast(Actor caster, ActorCastEvent spell) { switch ((AID)spell.Action.ID) @@ -349,6 +405,8 @@ class P3UltimateRelativityDarkBlizzard(BossModule module) : Components.GenericAO public override IEnumerable ActiveAOEs(int slot, Actor actor) => _sources.Select(s => new AOEInstance(_shape, s.Position, default, _activation)); + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { } // handled by main component + public override void OnStatusGain(Actor actor, ActorStatus status) { if ((SID)status.ID == SID.SpellInWaitingDarkBlizzard) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs new file mode 100644 index 0000000000..f6854d5948 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs @@ -0,0 +1,50 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P5AkhMorn(BossModule module) : Components.UniformStackSpread(module, 4, 0, 4) +{ + public Actor? Source; + private DateTime _activation; + + public override void Update() + { + Stacks.Clear(); + if (Source != null) + { + var left = Source.Rotation.ToDirection().OrthoL(); + Actor? targetL = null, targetR = null; + float distL = float.MaxValue, distR = float.MaxValue; + foreach (var p in Raid.WithoutSlot()) + { + var off = p.Position - Source.Position; + var side = left.Dot(off) > 0; + ref var target = ref side ? ref targetL : ref targetR; + ref var dist = ref side ? ref distL : ref distR; + var d = off.LengthSq(); + if (d < dist) + { + dist = d; + target = p; + } + } + if (targetL != null) + AddStack(targetL, _activation); + if (targetR != null) + AddStack(targetR, _activation); + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.AkhMornPandora) + { + Source = caster; + _activation = Module.CastFinishAt(spell, 0.1f); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.AkhMornPandoraAOE1 or AID.AkhMornPandoraAOE2) + Source = null; + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs new file mode 100644 index 0000000000..4a8b441574 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs @@ -0,0 +1,86 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P5FulgentBlade : Components.Exaflare +{ + private readonly List _lines = []; + private DateTime _nextBundle; + + public P5FulgentBlade(BossModule module) : base(module, new AOEShapeRect(5, 40)) + { + ImminentColor = ArenaColor.AOE; + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + var safespot = SafeSpots().FirstOrDefault(); + if (safespot != default) + Arena.AddCircle(safespot, 1, ArenaColor.Safe); + } + + public override void OnActorCreated(Actor actor) + { + if ((OID)actor.OID == OID.FulgentBladeLine) + { + _lines.Add(actor); + if (_lines.Count == 6) + _lines.SortByReverse(l => (l.Position - Module.Center).LengthSq()); + } + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID is AID.PathOfLightFirst or AID.PathOfDarknessFirst) + { + var dir = spell.Rotation.ToDirection(); + var distanceToBorder = Intersect.RayCircle(caster.Position - Module.Center, dir, 22); + Lines.Add(new() { Next = caster.Position, Advance = 5 * dir, Rotation = spell.Rotation, NextExplosion = Module.CastFinishAt(spell), TimeToMove = 2, ExplosionsLeft = (int)(distanceToBorder / 5) + 1, MaxShownExplosions = 1 }); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID is AID.PathOfLightFirst or AID.PathOfDarknessFirst or AID.PathOfLightRest or AID.PathOfDarknessRest) + { + if (WorldState.CurrentTime > _nextBundle) + { + ++NumCasts; + _nextBundle = WorldState.FutureTime(1); + } + + int index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1) && item.Rotation.AlmostEqual(spell.Rotation, 0.1f)); + if (index == -1) + { + ReportError($"Failed to find entry for {caster.InstanceID:X}"); + return; + } + + AdvanceLine(Lines[index], caster.Position); + if (Lines[index].ExplosionsLeft == 0) + Lines.RemoveAt(index); + } + } + + private IEnumerable SafeSpots() + { + if (_lines.Count != 6) + yield break; + if (NumCasts < 2) + yield return SafeSpot(_lines[0], _lines[1]); + if (NumCasts < 4) + yield return SafeSpot(_lines[2], _lines[3]); + if (NumCasts < 6) + yield return SafeSpot(_lines[4], _lines[5]); + } + + private WPos SafeSpot(Actor line1, Actor line2) + { + var d1 = (Module.Center - line1.Position).Normalized(); + var d2 = (Module.Center - line2.Position).Normalized(); + var n1 = d1.OrthoL(); + var n2 = d2.OrthoL(); + var p1 = line1.Position + 11 * d1 - 50 * n1; + var p2 = line2.Position + 11 * d2 - 50 * n2; + var t = Intersect.RayLine(p1, n1, p2, n2); + return t is > 0 and < 100 ? p1 + t * n1 : default; + } +} diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs new file mode 100644 index 0000000000..bddf7b0109 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs @@ -0,0 +1,24 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +abstract class SpiritTaker(BossModule module) : Components.UniformStackSpread(module, 0, 5) +{ + public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) + { + foreach (var spread in ActiveSpreads.Where(s => s.Target != actor)) + hints.AddForbiddenZone(ShapeDistance.Capsule(spread.Target.Position, spread.Target.LastFrameMovement.Normalized(), 2, spread.Radius + 1), spread.Activation); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + if ((AID)spell.Action.ID == AID.SpiritTaker) + AddSpreads(Raid.WithoutSlot(true), Module.CastFinishAt(spell, 0.3f)); + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if ((AID)spell.Action.ID == AID.SpiritTakerAOE) + Spreads.Clear(); + } +} + +class DefaultSpiritTaker(BossModule module) : SpiritTaker(module); // TODO: remove... From 35232dd7728bee04112afd912bcd30c770647315 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Mon, 13 Jan 2025 00:06:40 +0000 Subject: [PATCH 2/3] FRU improvements. --- .../Dawntrail/Ultimate/FRU/FRUEnums.cs | 8 + .../Dawntrail/Ultimate/FRU/FRUStates.cs | 11 +- .../Dawntrail/Ultimate/FRU/P2MirrorMirror.cs | 2 +- .../Dawntrail/Ultimate/FRU/P3Apocalypse.cs | 18 +- .../Dawntrail/Ultimate/FRU/P5FulgentBlade.cs | 53 ++++-- .../Ultimate/FRU/P5ParadiseRegained.cs | 170 ++++++++++++++++++ 6 files changed, 232 insertions(+), 30 deletions(-) create mode 100644 BossMod/Modules/Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index 1bca6f59da..a8358d5a26 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -269,6 +269,14 @@ public enum AID : uint AkhMornPandoraAOE2 = 40312, // Helper->players, no cast, range 4 circle, 4-man stack ParadiseRegained = 40319, // BossP5->self, 4.0s cast, single-target, visual (mechanic start) + WingsDarkAndLightDL = 40233, // BossP5->self, 6.9+0.1s cast, single-target, visual (dark > light) + WingsDarkAndLightLD = 40313, // BossP5->self, 6.9+0.1s cast, single-target, visual (light > dark) + WingsDarkAndLightExplosion = 40320, // Helper->self, no cast, range 3 circle, tower + WingsDarkAndLightUnmitigatedExplosion = 40321, // Helper->self, no cast, range 100 circle, tower fail + WingsDarkAndLightCleaveLight = 40314, // BossP5->self, no cast, range 100 240?-degree cone on target + WingsDarkAndLightCleaveDark = 40315, // BossP5->self, no cast, range 100 240?-degree cone on target + WingsDarkAndLightTetherLight = 39879, // Helper->players, no cast, range 4 circle on farthest + WingsDarkAndLightTetherDark = 39880, // Helper->player, no cast, range 4 circle on closest } public enum SID : uint diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index 905ebb0a6d..1b611df140 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -677,6 +677,15 @@ private void P5FulgentBlade(uint id, float delay) private void P5ParadiseRegained(uint id, float delay) { - ActorCast(id, _module.BossP5, AID.ParadiseRegained, delay, 4, true, "???"); + ActorCast(id, _module.BossP5, AID.ParadiseRegained, delay, 4, true) + .ActivateOnEnter(); // first tower appears ~0.9s after cast end, then every 3.5s + ActorCastMulti(id + 0x10, _module.BossP5, [AID.WingsDarkAndLightDL, AID.WingsDarkAndLightLD], 3.2f, 6.9f, true) + .ActivateOnEnter(); + ComponentCondition(id + 0x20, 0.5f, comp => comp.NumCasts > 0, "Light/dark"); // first tower resolve ~0.1s earlier + ComponentCondition(id + 0x30, 3.7f, comp => comp.NumCasts > 1, "Dark/light") // second tower resolves ~1s earlier + .DeactivateOnExit(); + // note: tethers resolve ~0.7s after cleave, but they won't happen if tether target dies to cleave + ComponentCondition(id + 0x40, 2.4f, comp => comp.NumCasts > 2, "Towers resolve") + .DeactivateOnExit(); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs index 7ae54c518f..1b15801ac2 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs @@ -108,7 +108,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (origin.activation > WorldState.FutureTime(3) && Module.Enemies(OID.BossP2).FirstOrDefault() is var boss && boss != null && boss.TargetID == actor.InstanceID) { var dirVec = dir.ToDirection(); - if (dirVec.Dot(boss.Position - origin.source.Position) > 2 && (origin.source.Position - 4 * dirVec - boss.Position).Length() > boss.HitboxRadius + 3) + if (dirVec.Dot(boss.Position - origin.source.Position) > 2.5f && (origin.source.Position - 3 * dirVec - boss.Position).Length() > boss.HitboxRadius + 3.5f) dir += 180.Degrees(); } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs index 39ed27a2b8..a97e3aadf5 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3Apocalypse.cs @@ -358,13 +358,13 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) class P3DarkestDanceKnockback(BossModule module) : Components.Knockback(module, ActionID.MakeSpell(AID.DarkestDanceKnockback), true) { - public Actor? Source; + public Actor? Caster; public DateTime Activation; public override IEnumerable Sources(int slot, Actor actor) { - if (Source != null) - yield return new(Source.Position, 21, Activation); + if (Caster != null) + yield return new(Caster.Position, 21, Activation); } public override void OnEventCast(Actor caster, ActorCastEvent spell) @@ -372,7 +372,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) switch ((AID)spell.Action.ID) { case AID.DarkestDanceBait: - Source = caster; + Caster = caster; Activation = WorldState.FutureTime(2.8f); break; case AID.DarkestDanceKnockback: @@ -455,14 +455,14 @@ class P3ApocalypseAIWater3(BossModule module) : BossComponent(module) public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - if (_water == null || _knockback?.Source == null) + if (_water == null || _knockback?.Caster == null) return; - var toCenter = Module.Center - _knockback.Source.Position; + var toCenter = Module.Center - _knockback.Caster.Position; if (toCenter.LengthSq() < 1) return; // did not jump yet, wait... - var angle = 30.Degrees(); //(_knockback.NumCasts == 0 ? 30 : 45).Degrees(); + var angle = 20.Degrees(); //(_knockback.NumCasts == 0 ? 30 : 45).Degrees(); var dir = Angle.FromDirection(toCenter) + _water.States[slot].AssignedGroup switch { 1 => -angle, @@ -473,12 +473,12 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme if (_knockback.NumCasts == 0) { // preposition for knockback - hints.AddForbiddenZone(ShapeDistance.PrecisePosition(_knockback.Source.Position + 2 * dir.ToDirection(), new(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f), _knockback.Activation); + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(_knockback.Caster.Position + 2 * dir.ToDirection(), new(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f), _knockback.Activation); } else if (_water.Stacks.Count > 0) { // stack at maxmelee - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(_knockback.Source.Position + 10 * dir.ToDirection(), 1), _water.Stacks[0].Activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(_knockback.Caster.Position + 10 * dir.ToDirection(), 1), _water.Stacks[0].Activation); } } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs index 4a8b441574..2e8784906b 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs @@ -12,6 +12,13 @@ class P5FulgentBlade : Components.Exaflare public override void DrawArenaForeground(int pcSlot, Actor pc) { + //// debug + //foreach (var l in _lines) + //{ + // var d = (Module.Center - l.Position).Normalized().OrthoL(); + // Arena.AddLine(l.Position - 50 * d, l.Position + 50 * d, ArenaColor.Object); + //} + var safespot = SafeSpots().FirstOrDefault(); if (safespot != default) Arena.AddCircle(safespot, 1, ArenaColor.Safe); @@ -22,8 +29,8 @@ public override void OnActorCreated(Actor actor) if ((OID)actor.OID == OID.FulgentBladeLine) { _lines.Add(actor); - if (_lines.Count == 6) - _lines.SortByReverse(l => (l.Position - Module.Center).LengthSq()); + //if (_lines.Count == 6) + // _lines.SortByReverse(l => (l.Position - Module.Center).LengthSq()); // TODO: this isn't right, there are lines with same distance... } } @@ -64,23 +71,31 @@ private IEnumerable SafeSpots() { if (_lines.Count != 6) yield break; - if (NumCasts < 2) - yield return SafeSpot(_lines[0], _lines[1]); - if (NumCasts < 4) - yield return SafeSpot(_lines[2], _lines[3]); - if (NumCasts < 6) - yield return SafeSpot(_lines[4], _lines[5]); - } + //if (NumCasts < 2) + // yield return SafeSpot(_lines[0], _lines[1]); + //if (NumCasts < 4) + // yield return SafeSpot(_lines[2], _lines[3]); + //if (NumCasts < 6) + // yield return SafeSpot(_lines[4], _lines[5]); - private WPos SafeSpot(Actor line1, Actor line2) - { - var d1 = (Module.Center - line1.Position).Normalized(); - var d2 = (Module.Center - line2.Position).Normalized(); - var n1 = d1.OrthoL(); - var n2 = d2.OrthoL(); - var p1 = line1.Position + 11 * d1 - 50 * n1; - var p2 = line2.Position + 11 * d2 - 50 * n2; - var t = Intersect.RayLine(p1, n1, p2, n2); - return t is > 0 and < 100 ? p1 + t * n1 : default; + if (NumCasts == 0) + { + WDir avgOff = default; + foreach (var l in _lines) + avgOff += (l.Position - Module.Center).Normalized(); + yield return Module.Center - 5 * avgOff; + } } + + //private WPos SafeSpot(Actor line1, Actor line2) + //{ + // var d1 = (Module.Center - line1.Position).Normalized(); + // var d2 = (Module.Center - line2.Position).Normalized(); + // var n1 = d1.OrthoL(); + // var n2 = d2.OrthoL(); + // var p1 = line1.Position + 11 * d1 - 50 * n1; + // var p2 = line2.Position + 11 * d2 - 50 * n2; + // var t = Intersect.RayLine(p1, n1, p2, n2); + // return t is > 0 and < 100 ? p1 + t * n1 : default; + //} } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs new file mode 100644 index 0000000000..da81c72e84 --- /dev/null +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs @@ -0,0 +1,170 @@ +namespace BossMod.Dawntrail.Ultimate.FRU; + +class P5ParadiseRegainedTowers(BossModule module) : Components.GenericTowers(module, ActionID.MakeSpell(AID.WingsDarkAndLightExplosion)) +{ + public override void OnEventEnvControl(byte index, uint state) + { + if (index is >= 51 and <= 53 && state == 0x00020001) + { + var dir = index switch + { + 51 => -120.Degrees(), + 52 => 120.Degrees(), + _ => 0.Degrees() + }; + var forbidden = Raid.WithSlot(true).WhereActor(p => p.Role == Role.Tank).Mask(); // TODO: assignments + Towers.Add(new(Module.Center + 7 * dir.ToDirection(), 3, 2, 2, forbidden, WorldState.FutureTime(9.5f))); + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + if (spell.Action == WatchedAction) + { + ++NumCasts; + if (Towers.Count == 0) + { + ReportError($"Unexpected expolosion @ {caster.Position}"); + } + else + { + if (!Towers[0].Position.AlmostEqual(caster.Position, 1)) + ReportError($"Unexpected position: {caster.Position} instead of {Towers[0].Position}"); + Towers.RemoveAt(0); + } + } + } +} + +class P5ParadiseRegainedBaits(BossModule module) : Components.GenericBaitAway(module) +{ + private readonly WDir _relSouth = module.FindComponent() is var towers && towers?.Towers.Count > 0 ? towers.Towers[0].Position - module.Center : default; + private Actor? _source; + private Actor? _firstTarget; + private AOEShapeCone? _curCleave; + private DateTime _activation; + private bool _tetherClosest; + + private static readonly AOEShapeCone _shapeCleaveL = new(19, 120.Degrees(), 60.Degrees()); // note: looks wrong with correct range... + private static readonly AOEShapeCone _shapeCleaveD = new(19, 120.Degrees(), -60.Degrees()); + private static readonly AOEShapeCircle _shapeTether = new(4); + + public override void Update() + { + CurrentBaits.Clear(); + if (_source != null && _curCleave != null) + { + var cleaveTarget = NumCasts == 0 ? _firstTarget : WorldState.Actors.Find(_source.TargetID); + if (cleaveTarget != null) + CurrentBaits.Add(new(_source, cleaveTarget, _curCleave, _activation)); + var tetherTarget = _tetherClosest ? Raid.WithoutSlot().Closest(_source.Position) : Raid.WithoutSlot().Farthest(_source.Position); + if (tetherTarget != null) + CurrentBaits.Add(new(tetherTarget, tetherTarget, _shapeTether, _activation)); // +0.7s? + } + } + + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!ForbiddenPlayers[slot] && CurrentBaits.Count == 2 && _source != null && _firstTarget != null) + { + var isFirstTank = actor == _firstTarget; + if (_source.TargetID == _firstTarget.InstanceID) + hints.Add(isFirstTank ? "Pass aggro!" : "Taunt!"); // TODO: can a tank bait both cleaves with invuln? + + var firstTankShouldBaitTether = NumCasts > 0; // TODO: can a tank bait cleave+tether with invuln? + var shouldBaitTether = isFirstTank == firstTankShouldBaitTether; + if (shouldBaitTether && CurrentBaits[1].Target != actor) + hints.Add(_tetherClosest ? "Go closer!" : "Go farther!"); + } + + base.AddHints(slot, actor, hints); + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + base.DrawArenaForeground(pcSlot, pc); + + var safeOffset = SafeOffset(pcSlot, pc); + if (safeOffset != default) + Arena.AddCircle(Module.Center + safeOffset, 1, ArenaColor.Safe); + } + + public override void OnCastStarted(Actor caster, ActorCastInfo spell) + { + (var shape, var closest) = (AID)spell.Action.ID switch + { + AID.WingsDarkAndLightDL => (_shapeCleaveD, true), + AID.WingsDarkAndLightLD => (_shapeCleaveL, false), + _ => (null, false) + }; + if (shape != null) + { + ForbiddenPlayers = Raid.WithSlot(true).WhereActor(p => p.Role != Role.Tank).Mask(); + _source = caster; + _firstTarget = WorldState.Actors.Find(caster.TargetID); + _curCleave = shape; + _activation = Module.CastFinishAt(spell, 0.5f); + _tetherClosest = closest; + } + } + + public override void OnEventCast(Actor caster, ActorCastEvent spell) + { + var nextShape = (AID)spell.Action.ID switch + { + AID.WingsDarkAndLightCleaveLight => _shapeCleaveD, + AID.WingsDarkAndLightCleaveDark => _shapeCleaveL, + _ => null + }; + if (nextShape != null) + { + ++NumCasts; + _curCleave = nextShape; + _activation = WorldState.FutureTime(3.7f); + _tetherClosest = !_tetherClosest; + } + } + + private WDir SafeOffset(int slot, Actor player) + { + if (_relSouth == default || _curCleave == null) + return default; // not initialized properly + + var southDir = Angle.FromDirection(_relSouth); + if (ForbiddenPlayers[slot]) + { + return default; // TODO: implement hints for non-tanks + } + else if (player == _firstTarget) + { + if (NumCasts == 0) + { + // bait cleave, so that south is safe + return 7 * (southDir + 2 * _curCleave.DirectionOffset).ToDirection(); + } + else + { + // bait tether across south + return (_tetherClosest ? 2 : 10) * (southDir + 180.Degrees()).ToDirection(); + } + } + else + { + if (NumCasts > 0) + { + // bait cleave, so that north is safe + return 7 * (southDir - _curCleave.DirectionOffset).ToDirection(); + } + else if (_tetherClosest) + { + // bait tether at south + return 2 * southDir.ToDirection(); + } + else + { + // bait tether at max melee at 45 degrees + return 10 * (southDir + 0.75f * _curCleave.DirectionOffset).ToDirection(); + } + } + } +} From 7145c92092eb747030f4d0e9eca1f4d8c93cef25 Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:29:27 +0100 Subject: [PATCH 3/3] merge fix --- .../Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs | 12 ++++++------ .../Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs | 6 +++--- BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs | 2 +- .../Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs | 12 ++++++------ .../Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs | 10 +++++----- .../Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs index 792067db7d..e1e72aaf28 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2AbsoluteZero.cs @@ -50,7 +50,7 @@ public override void Update() return; foreach (var c in _crystalsOfDarkness) { - var baiter = Raid.WithoutSlot().Closest(c.Position); + var baiter = Raid.WithoutSlot(false, true, true).Closest(c.Position); if (baiter != null) CurrentBaits.Add(new(c, baiter, _cones.Shape)); } @@ -104,10 +104,10 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme // intercardinals - bait cones if (_cones?.Casters.Count == 0) { - var assignedPosition = Module.Center + 9 * (180 - 45 * clockSpot).Degrees().ToDirection(); // crystal is at R=8 + var assignedPosition = Arena.Center + 9 * (180 - 45 * clockSpot).Degrees().ToDirection(); // crystal is at R=8 var assignedCrystal = CrystalsOfDarkness.FirstOrDefault(c => c.Position.AlmostEqual(assignedPosition, 2)); if (assignedCrystal != null) - hints.AddForbiddenZone(ShapeDistance.PrecisePosition(assignedPosition, new WDir(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f)); + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(assignedPosition, new WDir(0, 1), Arena.Bounds.MapResolution, actor.Position, 0.1f)); } // else: just dodge cones etc... } @@ -115,9 +115,9 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme public override void DrawArenaForeground(int pcSlot, Actor pc) { - Arena.Actors(CrystalsOfLight, ArenaColor.Enemy); - Arena.Actors(CrystalsOfDarkness, ArenaColor.Object); - Arena.Actor(IceVeil, _iceVeilInvincible ? ArenaColor.Object : ArenaColor.Enemy); + Arena.Actors(CrystalsOfLight, Colors.Enemy); + Arena.Actors(CrystalsOfDarkness, Colors.Object); + Arena.Actor(IceVeil, _iceVeilInvincible ? Colors.Object : Colors.Enemy); } public override void OnStatusLose(Actor actor, ActorStatus status) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs index b8c1a31d89..fd30e93d45 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs @@ -58,7 +58,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme { // ok, out is imminent, gtfo - we need to avoid clipping people, avoid dark blizzard (if it's being resolved now), and avoid lasers (if any) var avoidBlizzard = NumCasts == 2; - foreach (var (i, p) in Raid.WithSlot().Exclude(slot)) + foreach (var (i, p) in Raid.WithSlot(false, true, true).Exclude(slot)) { var avoidRadius = avoidBlizzard && States[i].HaveDarkBlizzard ? 12 : 8; hints.AddForbiddenZone(ShapeDistance.Circle(p.Position, avoidRadius + 1)); @@ -73,11 +73,11 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme case RangeHintDarkEruption: case RangeHintEye: // go to exact safespot - hints.AddForbiddenZone(ShapeDistance.PrecisePosition(SafeSpot(slot, range), new(0, 1), Module.Bounds.MapResolution, actor.Position, 0.1f), _nextImminent); + hints.AddForbiddenZone(ShapeDistance.PrecisePosition(SafeSpot(slot, range), new(0, 1), Arena.Bounds.MapResolution, actor.Position, 0.1f), _nextImminent); break; default: // go to mid - hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Module.Center, 1), _nextImminent); + hints.AddForbiddenZone(ShapeDistance.InvertedCircle(Arena.Center, 1), _nextImminent); break; } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs index f6854d5948..e2f72e9dcc 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5AkhMorn.cs @@ -13,7 +13,7 @@ public override void Update() var left = Source.Rotation.ToDirection().OrthoL(); Actor? targetL = null, targetR = null; float distL = float.MaxValue, distR = float.MaxValue; - foreach (var p in Raid.WithoutSlot()) + foreach (var p in Raid.WithoutSlot(false, true, true)) { var off = p.Position - Source.Position; var side = left.Dot(off) > 0; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs index 2e8784906b..bade6798ea 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5FulgentBlade.cs @@ -7,7 +7,7 @@ class P5FulgentBlade : Components.Exaflare public P5FulgentBlade(BossModule module) : base(module, new AOEShapeRect(5, 40)) { - ImminentColor = ArenaColor.AOE; + ImminentColor = Colors.AOE; } public override void DrawArenaForeground(int pcSlot, Actor pc) @@ -21,7 +21,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) var safespot = SafeSpots().FirstOrDefault(); if (safespot != default) - Arena.AddCircle(safespot, 1, ArenaColor.Safe); + Arena.AddCircle(safespot, 1, Colors.Safe); } public override void OnActorCreated(Actor actor) @@ -40,7 +40,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) { var dir = spell.Rotation.ToDirection(); var distanceToBorder = Intersect.RayCircle(caster.Position - Module.Center, dir, 22); - Lines.Add(new() { Next = caster.Position, Advance = 5 * dir, Rotation = spell.Rotation, NextExplosion = Module.CastFinishAt(spell), TimeToMove = 2, ExplosionsLeft = (int)(distanceToBorder / 5) + 1, MaxShownExplosions = 1 }); + Lines.Add(new() { Next = spell.LocXZ, Advance = 5 * dir, Rotation = spell.Rotation, NextExplosion = Module.CastFinishAt(spell), TimeToMove = 2, ExplosionsLeft = (int)(distanceToBorder / 5) + 1, MaxShownExplosions = 1 }); } } @@ -54,7 +54,7 @@ public override void OnEventCast(Actor caster, ActorCastEvent spell) _nextBundle = WorldState.FutureTime(1); } - int index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1) && item.Rotation.AlmostEqual(spell.Rotation, 0.1f)); + var index = Lines.FindIndex(item => item.Next.AlmostEqual(caster.Position, 1) && item.Rotation.AlmostEqual(spell.Rotation, 0.1f)); if (index == -1) { ReportError($"Failed to find entry for {caster.InstanceID:X}"); @@ -82,8 +82,8 @@ private IEnumerable SafeSpots() { WDir avgOff = default; foreach (var l in _lines) - avgOff += (l.Position - Module.Center).Normalized(); - yield return Module.Center - 5 * avgOff; + avgOff += (l.Position - Arena.Center).Normalized(); + yield return Arena.Center - 5 * avgOff; } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs index da81c72e84..022c399cef 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P5ParadiseRegained.cs @@ -12,8 +12,8 @@ public override void OnEventEnvControl(byte index, uint state) 52 => 120.Degrees(), _ => 0.Degrees() }; - var forbidden = Raid.WithSlot(true).WhereActor(p => p.Role == Role.Tank).Mask(); // TODO: assignments - Towers.Add(new(Module.Center + 7 * dir.ToDirection(), 3, 2, 2, forbidden, WorldState.FutureTime(9.5f))); + var forbidden = Raid.WithSlot(true, true, true).WhereActor(p => p.Role == Role.Tank).Mask(); // TODO: assignments + Towers.Add(new(Arena.Center + 7 * dir.ToDirection(), 3, 2, 2, forbidden, WorldState.FutureTime(9.5f))); } } @@ -57,7 +57,7 @@ public override void Update() var cleaveTarget = NumCasts == 0 ? _firstTarget : WorldState.Actors.Find(_source.TargetID); if (cleaveTarget != null) CurrentBaits.Add(new(_source, cleaveTarget, _curCleave, _activation)); - var tetherTarget = _tetherClosest ? Raid.WithoutSlot().Closest(_source.Position) : Raid.WithoutSlot().Farthest(_source.Position); + var tetherTarget = _tetherClosest ? Raid.WithoutSlot(false, true, true).Closest(_source.Position) : Raid.WithoutSlot(false, true, true).Farthest(_source.Position); if (tetherTarget != null) CurrentBaits.Add(new(tetherTarget, tetherTarget, _shapeTether, _activation)); // +0.7s? } @@ -86,7 +86,7 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) var safeOffset = SafeOffset(pcSlot, pc); if (safeOffset != default) - Arena.AddCircle(Module.Center + safeOffset, 1, ArenaColor.Safe); + Arena.AddCircle(Arena.Center + safeOffset, 1, Colors.Safe); } public override void OnCastStarted(Actor caster, ActorCastInfo spell) @@ -99,7 +99,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) }; if (shape != null) { - ForbiddenPlayers = Raid.WithSlot(true).WhereActor(p => p.Role != Role.Tank).Mask(); + ForbiddenPlayers = Raid.WithSlot(true, true, true).WhereActor(p => p.Role != Role.Tank).Mask(); _source = caster; _firstTarget = WorldState.Actors.Find(caster.TargetID); _curCleave = shape; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs index bddf7b0109..8bba78b75a 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/SpiritTaker.cs @@ -11,7 +11,7 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme public override void OnCastStarted(Actor caster, ActorCastInfo spell) { if ((AID)spell.Action.ID == AID.SpiritTaker) - AddSpreads(Raid.WithoutSlot(true), Module.CastFinishAt(spell, 0.3f)); + AddSpreads(Raid.WithoutSlot(true, true, true), Module.CastFinishAt(spell, 0.3f)); } public override void OnEventCast(Actor caster, ActorCastEvent spell)