From 45f6801be7d18c77de97c1e59d38e1f7021fb496 Mon Sep 17 00:00:00 2001 From: Andrew Gilewsky Date: Tue, 21 Jan 2025 23:29:26 +0000 Subject: [PATCH] A bunch of FRU improvements. --- .../Dawntrail/Ultimate/FRU/FRUConfig.cs | 6 +- .../Dawntrail/Ultimate/FRU/FRUStates.cs | 7 +- .../Dawntrail/Ultimate/FRU/P2MirrorMirror.cs | 153 +++++++++--------- .../Ultimate/FRU/P3UltimateRelativity.cs | 24 ++- .../Ultimate/FRU/P4CrystallizeTime.cs | 2 +- .../Dawntrail/Ultimate/FRU/P4Preposition.cs | 22 +++ TODO | 13 +- UIDev/UIDev.csproj | 2 +- 8 files changed, 132 insertions(+), 97 deletions(-) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs index 66f5107ee6..5992f63c1d 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUConfig.cs @@ -12,9 +12,9 @@ public class FRUConfig() : ConfigNode() [PropertyDisplay("P1 Fall of Faith (cone tethers): conga priority (two people without tethers with lower priorities join odd group)")] [GroupDetails(["1", "2", "3", "4", "5", "6", "7", "8"])] - [GroupPreset("TTHHMMRR", [0, 1, 2, 3, 4, 5, 6, 7])] - [GroupPreset("RHMTTMHR", [3, 4, 1, 6, 2, 5, 0, 7])] - public GroupAssignmentUnique P1FallOfFaithAssignment = GroupAssignmentUnique.DefaultRoles(); + [GroupPreset("HHTTMMRR", [2, 3, 0, 1, 4, 5, 6, 7])] + [GroupPreset("HRMTTMRH", [3, 4, 0, 7, 2, 5, 1, 6])] + public GroupAssignmentUnique P1FallOfFaithAssignment = new() { Assignments = [2, 3, 0, 1, 4, 5, 6, 7] }; [PropertyDisplay("P1 Fall of Faith (cone tethers): odd groups go W (rather than N)")] public bool P1FallOfFaithEW = false; diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index ddad50c128..cdb7d38f6a 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -309,11 +309,11 @@ private void P2MirrorMirror(uint id, float delay) ComponentCondition(id + 0x20, 9.3f, comp => comp.NumCasts > 0) .ActivateOnEnter() .DeactivateOnExit(); - ComponentCondition(id + 0x21, 0.6f, comp => comp.NumCasts > 1, "Mirror 2") + ComponentCondition(id + 0x21, 0.6f, comp => comp.NumCasts > 8, "Mirror 2") + .ActivateOnEnter() // activate a bit early, so that it can read state gathered by house of light component .DeactivateOnExit(); - ActorCastMulti(id + 0x100, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 0.5f, 5, true) - .ActivateOnEnter(); + ActorCastMulti(id + 0x100, _module.BossP2, [AID.BanishStack, AID.BanishSpread], 0.5f, 5, true); ComponentCondition(id + 0x102, 0.1f, comp => !comp.Active, "Spread/Stack") .DeactivateOnExit(); } @@ -523,6 +523,7 @@ private void P4AkhRhai(uint id, float delay) { ActorTargetable(id, _module.BossP4Usurper, true, delay, "Usurper appears") .ActivateOnEnter() + .ActivateOnEnter() .DeactivateOnExit() .SetHint(StateMachine.StateHint.DowntimeEnd); ActorCastStart(id + 0x10, _module.BossP4Usurper, AID.Materialization, 5.1f, true) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs index 89d0c47a96..bfbbf77b6b 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P2MirrorMirror.cs @@ -1,12 +1,19 @@ namespace BossMod.Dawntrail.Ultimate.FRU; -class P2MirrorMirrorReflectedScytheKickBlue(BossModule module) : Components.GenericAOEs(module, ActionID.MakeSpell(AID.ReflectedScytheKickBlue)) +class P2MirrorMirrorReflectedScytheKickBlue : Components.GenericAOEs { private WDir _blueMirror; + private BitMask _rangedSpots; private AOEInstance? _aoe; private static readonly AOEShapeDonut _shape = new(4, 20); + public P2MirrorMirrorReflectedScytheKickBlue(BossModule module) : base(module, ActionID.MakeSpell(AID.ReflectedScytheKickBlue)) + { + foreach (var (slot, group) in Service.Config.Get().P2MirrorMirror1SpreadSpots.Resolve(Raid)) + _rangedSpots[slot] = group >= 4; + } + public override IEnumerable ActiveAOEs(int slot, Actor actor) => Utils.ZeroOrOne(_aoe); public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) @@ -28,8 +35,9 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) Arena.Actor(Module.Center + 20 * _blueMirror, Angle.FromDirection(-_blueMirror), ArenaColor.Object); if (_aoe == null) { - // draw hint for melees - Arena.AddCircle(Module.Center - 11 * _blueMirror, 1, ArenaColor.Safe); + // draw preposition hint + var distance = _rangedSpots[pcSlot] ? 19 : -11; + Arena.AddCircle(Module.Center + distance * _blueMirror, 1, ArenaColor.Safe); } } } @@ -57,28 +65,34 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) class P2MirrorMirrorHouseOfLight(BossModule module) : Components.GenericBaitAway(module, ActionID.MakeSpell(AID.HouseOfLight)) { + public readonly record struct Source(Actor Actor, DateTime Activation); + + public bool RedRangedLeftOfMelee; + public readonly List FirstSources = []; // [boss, blue mirror] + public readonly List SecondSources = []; // [melee red mirror, ranged red mirror] private readonly FRUConfig _config = Service.Config.Get(); private Angle? _blueMirror; - private int _numRedMirrors; - private readonly List<(Actor source, DateTime activation)> _sources = []; + + private List CurrentSources => NumCasts == 0 ? FirstSources : SecondSources; private static readonly AOEShapeCone _shape = new(60, 15.Degrees()); public override void Update() { CurrentBaits.Clear(); - foreach (var s in _sources.Take(2)) - foreach (var p in Raid.WithoutSlot().SortedByRange(s.source.Position).Take(4)) - CurrentBaits.Add(new(s.source, p, _shape, s.activation)); + foreach (var s in CurrentSources) + foreach (var p in Raid.WithoutSlot().SortedByRange(s.Actor.Position).Take(4)) + CurrentBaits.Add(new(s.Actor, p, _shape, s.Activation)); } public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { var group = (NumCasts == 0 ? _config.P2MirrorMirror1SpreadSpots : _config.P2MirrorMirror2SpreadSpots)[assignment]; - if (_sources.Count < 2 || _blueMirror == null || group < 0) + var sources = CurrentSources; + if (sources.Count < 2 || _blueMirror == null || group < 0) return; // inactive or no valid assignments - var origin = _sources[group < 4 ? 0 : 1]; + var origin = sources[group < 4 ? 0 : 1]; Angle dir; if (NumCasts == 0) { @@ -93,31 +107,20 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme } else { - var offset = origin.source.Position - Module.Center; - var altSourceToTheRight = offset.OrthoL().Dot(_sources[group < 4 ? 1 : 0].source.Position - Module.Center) < 0; - dir = Angle.FromDirection(offset) + group switch + dir = Angle.FromDirection(origin.Actor.Position - Module.Center) + group switch { - 0 => (altSourceToTheRight ? 90 : -90).Degrees(), - 1 => (altSourceToTheRight ? -90 : 90).Degrees(), + 0 => (RedRangedLeftOfMelee ? -90 : 90).Degrees(), + 1 => (RedRangedLeftOfMelee ? 90 : -90).Degrees(), 2 => 180.Degrees(), - 3 => (altSourceToTheRight ? 135 : -135).Degrees(), + 3 => (RedRangedLeftOfMelee ? -135 : 135).Degrees(), 4 => -90.Degrees(), 5 => 90.Degrees(), - 6 => (altSourceToTheRight ? 180 : -135).Degrees(), - 7 => (altSourceToTheRight ? 135 : 180).Degrees(), + 6 => (RedRangedLeftOfMelee ? 180 : -135).Degrees(), + 7 => (RedRangedLeftOfMelee ? 135 : 180).Degrees(), _ => default }; - - // special logic for current tank: if mechanic will take a while to resolve, and boss is far enough away from the destination, and normal destination is on the same side as the boss, drag towards other side first - // this guarantees uptime for OT - //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.5f && (origin.source.Position - 3 * dirVec - boss.Position).Length() > boss.HitboxRadius + 3.5f) - // dir += 180.Degrees(); - //} } - hints.AddForbiddenZone(ShapeDistance.InvertedCone(origin.source.Position, 4, dir, 15.Degrees()), origin.activation); + hints.AddForbiddenZone(ShapeDistance.InvertedCone(origin.Actor.Position, 4, dir, 15.Degrees()), origin.Activation); } public override void OnEventEnvControl(byte index, uint state) @@ -134,80 +137,86 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) { case AID.ScytheKick: var activation = Module.CastFinishAt(spell, 0.7f); - _sources.Add((caster, activation)); + FirstSources.Add(new(caster, activation)); var mirror = _blueMirror != null ? Module.Enemies(OID.FrozenMirror).Closest(Module.Center + 20 * _blueMirror.Value.ToDirection()) : null; if (mirror != null) - _sources.Add((mirror, activation)); + FirstSources.Add(new(mirror, activation)); break; case AID.ReflectedScytheKickRed: - _sources.Add((caster, Module.CastFinishAt(spell, 0.6f))); - if (++_numRedMirrors == 2 && _blueMirror != null && _sources.Count >= 2) + SecondSources.Add(new(caster, Module.CastFinishAt(spell, 0.6f))); + if (SecondSources.Count == 2 && _blueMirror != null) { // order two red mirrors so that first one is closer to boss and second one closer to blue mirror; if both are same distance, select CW ones (arbitrary) - var d1 = (Angle.FromDirection(_sources[^2].source.Position - Module.Center) - _blueMirror.Value).Normalized(); - var d2 = (Angle.FromDirection(_sources[^1].source.Position - Module.Center) - _blueMirror.Value).Normalized(); + var d1 = (Angle.FromDirection(SecondSources[0].Actor.Position - Module.Center) - _blueMirror.Value).Normalized(); + var d2 = (Angle.FromDirection(SecondSources[1].Actor.Position - Module.Center) - _blueMirror.Value).Normalized(); var d1abs = d1.Abs(); var d2abs = d2.Abs(); var swap = d2abs.AlmostEqual(d1abs, 0.1f) ? d2.Rad > 0 // swap if currently second one is CCW from blue mirror : d2abs.Rad > d1abs.Rad; // swap if currently second one is further from the blue mirror if (swap) - (_sources[^1], _sources[^2]) = (_sources[^2], _sources[^1]); + (SecondSources[1], SecondSources[0]) = (SecondSources[0], SecondSources[1]); + + RedRangedLeftOfMelee = (SecondSources[0].Actor.Position - Module.Center).OrthoL().Dot(SecondSources[1].Actor.Position - Module.Center) > 0; } break; } } +} + +class P2MirrorMirrorBanish : P2Banish +{ + private WPos _anchorMelee; + private WPos _anchorRanged; + private BitMask _aroundRanged; + private BitMask _closerToCenter; + private BitMask _leftSide; - public override void OnEventCast(Actor caster, ActorCastEvent spell) + public P2MirrorMirrorBanish(BossModule module) : base(module) { - if (spell.Action == WatchedAction) + var proteans = module.FindComponent(); + if (proteans != null && proteans.FirstSources.Count == 2 && proteans.SecondSources.Count == 2) { - var deadline = WorldState.FutureTime(2); - var firstInBunch = _sources.RemoveAll(s => s.activation < deadline) > 0; - if (firstInBunch) - ++NumCasts; + _anchorMelee = proteans.FirstSources[0].Actor.Position; + _anchorRanged = module.Center + 0.5f * (proteans.SecondSources[1].Actor.Position - module.Center); + foreach (var (slot, group) in Service.Config.Get().P2MirrorMirror2SpreadSpots.Resolve(Raid)) + { + _aroundRanged[slot] = group >= 4; + _closerToCenter[slot] = (group & 2) != 0; + _leftSide[slot] = group switch + { + 0 => !proteans.RedRangedLeftOfMelee, + 1 => proteans.RedRangedLeftOfMelee, + 2 => proteans.RedRangedLeftOfMelee, + 3 => !proteans.RedRangedLeftOfMelee, + 4 => false, + 5 => true, + 6 => false, + 7 => true, + _ => false + }; + } } } -} -class P2MirrorMirrorBanish(BossModule module) : P2Banish(module) -{ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignment assignment, AIHints hints) { - var prepos = PrepositionLocation(assignment); + var prepos = PrepositionLocation(slot, 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) + private WPos? PrepositionLocation(int slot, PartyRolesConfig.Assignment assignment) + => Stacks.Count > 0 && Stacks[0].Activation > WorldState.FutureTime(2.5f) ? CalculatePrepositionLocation(_aroundRanged[slot], _leftSide[slot], 90.Degrees()) + : Spreads.Count > 0 && Spreads[0].Activation > WorldState.FutureTime(2.5f) ? CalculatePrepositionLocation(_aroundRanged[slot], _leftSide[slot], (_closerToCenter[slot] ? 135 : 45).Degrees()) + : null; + + private WPos CalculatePrepositionLocation(bool aroundRanged, bool leftSide, Angle angle) { - // 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; + var anchor = aroundRanged ? _anchorRanged : _anchorMelee; + var offset = Angle.FromDirection(anchor - Module.Center) + (leftSide ? angle : -angle); + return anchor + 6 * offset.ToDirection(); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs index cafc916384..5a220b3910 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P3UltimateRelativity.cs @@ -342,16 +342,26 @@ public override void AddAIHints(int slot, Actor actor, PartyRolesConfig.Assignme public override void DrawArenaForeground(int pcSlot, Actor pc) { - base.DrawArenaForeground(pcSlot, pc); - - foreach (ref var b in CurrentBaits.AsSpan()) + foreach (var bait in ActiveBaitsOn(pc)) { - if (b.Target == pc && b.Source.Position.AlmostEqual(AssignedHourglass(pcSlot), 1) && _rel != null) + if (bait.Source.Position.AlmostEqual(AssignedHourglass(pcSlot), 1) && _rel != null) { // draw extra rotation hints for correctly baited hourglass - var rot = _rel.LaserRotationAt(b.Source.Position); - for (int i = 1; i < 10; ++i) - _shape.Outline(Arena, b.Source.Position, b.Rotation + i * rot); + // note: we don't want to draw 'short' edges of the rectangle (farther one is far outside arena bounds anyway, and closer one messes visualization up too much + var rot = _rel.LaserRotationAt(bait.Source.Position); + for (int i = 0; i < 10; ++i) + { + var dir = (bait.Rotation + i * rot).ToDirection(); + var side = _shape.HalfWidth * dir.OrthoR(); + var end = bait.Source.Position + _shape.LengthFront * dir; + Arena.AddLine(bait.Source.Position + side, end + side, ArenaColor.Danger); + Arena.AddLine(bait.Source.Position - side, end - side, ArenaColor.Danger); + } + } + else + { + // just draw default hint + bait.Shape.Outline(Arena, bait.Source.Position, bait.Rotation); } } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs index ca5c8ee062..aeff488d05 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs @@ -630,7 +630,7 @@ private WDir AssignedPositionOffset(Actor actor, PartyRolesConfig.Assignment ass var isLeft = assignment is PartyRolesConfig.Assignment.MT or PartyRolesConfig.Assignment.H1 or PartyRolesConfig.Assignment.M1 or PartyRolesConfig.Assignment.R1; var offDir = (Angle.FromDirection(_exalines.StartingOffsetSum) + (isLeft ? 45 : -45).Degrees()).ToDirection(); var normDir = isLeft ? offDir.OrthoL() : offDir.OrthoR(); - var (offX, offY) = actor.Role == Role.Tank ? (4, 1) : (2, 3); + var (offX, offY) = actor.Role == Role.Tank ? (4, 1) : (1, 2); return offX * offDir + offY * normDir; } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4Preposition.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4Preposition.cs index 4f0af9f135..389a5b75c9 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4Preposition.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4Preposition.cs @@ -17,3 +17,25 @@ public override void DrawArenaForeground(int pcSlot, Actor pc) Arena.AddCircle(b.Position, 1, ArenaColor.Safe); } } + +// utility to draw hitbox around crystal, so that it's easier not to clip +class P4FragmentOfFate(BossModule module) : BossComponent(module) +{ + private readonly IReadOnlyList _fragment = module.Enemies(OID.FragmentOfFate); + + public override PlayerPriority CalcPriority(int pcSlot, Actor pc, int playerSlot, Actor player, ref uint customColor) + { + if (playerSlot >= PartyState.MaxPartySize) + { + customColor = ArenaColor.Object; + return PlayerPriority.Danger; + } + return PlayerPriority.Irrelevant; + } + + public override void DrawArenaForeground(int pcSlot, Actor pc) + { + foreach (var f in _fragment) + Arena.AddCircle(f.Position, f.HitboxRadius, ArenaColor.Object); + } +} diff --git a/TODO b/TODO index 3da413175b..c0012c20c9 100644 --- a/TODO +++ b/TODO @@ -1,15 +1,6 @@ immediate plans - circle vs rect/cone intersection -- fru --- banish 2 ai --- proper circle bounds -> revise everything --- p1 fall of faith presets for hhttmmrr / hrmttmrh --- mirror 1 hints for ranged --- ur laser circle hint on top of baits --- apoc revise default prio for healers --- p4 crystal hitbox --- ct revise visual pos hints for eruptions --- ct revise visual pos hints for rewind +- proper circle bounds -> revise everything in fru - ai refactoring -- high-level ai modules --- ordered before standard rotation modules @@ -35,6 +26,8 @@ immediate plans - ishape general: +- pending effects +-- there's no effectresult for attract if it won't move (within mindistance) - horizontal timeline / cooldown planner - cdplanner should use real cds - assignments per ui order rather than per player + class-specific assignments diff --git a/UIDev/UIDev.csproj b/UIDev/UIDev.csproj index aec60ee2af..eeac72bcbc 100644 --- a/UIDev/UIDev.csproj +++ b/UIDev/UIDev.csproj @@ -69,6 +69,6 @@ - +