diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs index a8817cf68a..65dd13703b 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUEnums.cs @@ -227,7 +227,7 @@ public enum AID : uint AkhMornAOEOracle = 40303, // Helper->players, no cast, range 4 circle, 4-hit 4-man stack MornAfahUsurper = 40249, // UsurperOfFrostP4->self, 6.0s cast, single-target, visual (full raid stack, lethal if hp difference is large) MornAfahOracle = 40304, // OracleOfDarknessP4->self, 6.0s cast, single-target, visual (full raid stack, lethal if hp difference is large) - MornAfahAOE = 40250, // Helper->players, no cast, range 4 circle, wipe if hp difference check fails ? + MornAfahAOE = 40250, // Helper->players, no cast, range 4 circle, 8-man stack on usurper target, wipe if hp difference check fails CrystallizeTimeUsurper = 40240, // UsurperOfFrostP4->self, 10.0s cast, single-target, visual CrystallizeTimeOracle = 40298, // OracleOfDarknessP4->self, 10.0s cast, range 100 circle, raidwide @@ -240,8 +240,9 @@ public enum AID : uint LongingOfTheLost = 40241, // Helper->location, no cast, range 12 circle, aoe when head is touched DrachenWandererDisappear = 40244, // DrachenWanderer->self, no cast, single-target, visual (disappear) JoylessDragonsong = 40242, // Helper->self, no cast, range 40 circle, wipe if ??? - CrystallizeTimeHallowedWings = 40229, // UsurperOfFrostP4->self, 4.7+1.3s cast, single-target, visual (??? knockbacks?) - //_Weaponskill_HallowedWings = 40332, // UsurperOfFrostP4->self, 0.5s cast, range 40 width 50 rect + CrystallizeTimeHallowedWings1 = 40229, // UsurperOfFrostP4->self, 4.7+1.3s cast, single-target, visual (first knockback) + CrystallizeTimeHallowedWings2 = 40230, // UsurperOfFrostP4->self, 0.5+1.3s cast, single-target, visual (second knockback) + CrystallizeTimeHallowedWingsAOE = 40332, // UsurperOfFrostP4->self, 0.5s cast, range 40 width 50 rect, knockback 20, heavy damage on first target, vuln on first 4 targets } public enum SID : uint diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs index d84c82a0ba..46eb451fcf 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/FRUStates.cs @@ -54,6 +54,7 @@ private void Phase34(uint id) P4DarklitDragonsong(id + 0x110000, 1.9f); P4AkhMornMornAfah(id + 0x120000, 5.8f); P4CrystallizeTime(id + 0x130000, 4.6f); + P4AkhMornMornAfah(id + 0x140000, 0.1f); SimpleState(id + 0xFF0000, 100, "???"); } @@ -595,18 +596,23 @@ private void P4CrystallizeTime(uint id, float delay) .ActivateOnEnter() .DeactivateOnExit() .SetHint(StateMachine.StateHint.Raidwide); - ComponentCondition(id + 0x80, 1.9f, comp => comp.Done, "Rewind") - .DeactivateOnExit() + ComponentCondition(id + 0x80, 1.9f, comp => comp.RewindDone, "Rewind place") .DeactivateOnExit() .DeactivateOnExit() .DeactivateOnExit(); ActorCastStart(id + 0x90, _module.BossP4Oracle, AID.SpiritTaker, 0.4f); - ActorCastStart(id + 0x91, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings, 2.2f) + ActorCastStart(id + 0x91, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings1, 2.2f) .ActivateOnEnter(); ActorCastEnd(id + 0x92, _module.BossP4Oracle, 0.8f); ComponentCondition(id + 0x93, 0.3f, comp => comp.Spreads.Count == 0, "Jump") .DeactivateOnExit(); - ActorCastEnd(id + 0x94, _module.BossP4Usurper, 3.6f); - // TODO: knockbacks resolve, downtime end + ComponentCondition(id + 0x94, 3.3f, comp => comp.ReturnDone, "Rewind return") + .DeactivateOnExit(); + ActorCastEnd(id + 0x95, _module.BossP4Usurper, 0.3f); + ActorCast(id + 0xA0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWingsAOE, 1.4f, 0.5f, true); + ActorCast(id + 0xB0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWings2, 2.1f, 0.5f, true); + ActorCast(id + 0xC0, _module.BossP4Usurper, AID.CrystallizeTimeHallowedWingsAOE, 1.4f, 0.5f, true); + ActorTargetable(id + 0xD0, _module.BossP4Usurper, true, 5.3f, "Bosses reappear") + .SetHint(StateMachine.StateHint.DowntimeEnd); } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs index feea449556..f776b9110d 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4AkhMorn.cs @@ -1,13 +1,14 @@ namespace BossMod.Dawntrail.Ultimate.FRU; +// TODO: can target change if boss is provoked mid cast? class P4AkhMorn(BossModule module) : Components.UniformStackSpread(module, 4, 0, 4) { public int NumCasts; public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID == AID.AkhMornOracle) - AddStacks(Raid.WithoutSlot(true).Where(p => p.Role == Role.Tank), Module.CastFinishAt(spell, 0.9f)); + if ((AID)spell.Action.ID is AID.AkhMornOracle or AID.AkhMornUsurper && WorldState.Actors.Find(caster.TargetID) is var target && target != null) + AddStack(target, Module.CastFinishAt(spell, 0.9f)); } public override void OnEventCast(Actor caster, ActorCastEvent spell) diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs index 7719e73016..0fb686f6a8 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4CrystallizeTime.cs @@ -424,21 +424,77 @@ private WDir SafeOffsetFangOther(int numHourglassesDone, float northSlowX) private WDir SafeOffsetFinalNonAir(float northSlowX) => 6 * (northSlowX > 0 ? -150 : 150).Degrees().ToDirection(); } +// TODO: better positioning hints class P4CrystallizeTimeRewind(BossModule module) : BossComponent(module) { - public bool Done; + public bool RewindDone; + public bool ReturnDone; private readonly P4CrystallizeTime? _ct = module.FindComponent(); private readonly P4CrystallizeTimeTidalLight? _exalines = module.FindComponent(); + public override void AddHints(int slot, Actor actor, TextHints hints) + { + if (!RewindDone && _ct != null && _exalines != null && _ct.Cleansed[slot]) + { + var players = Raid.WithoutSlot(excludeNPCs: true).ToList(); + players.SortBy(p => p.Position.X); + var xOrder = players.IndexOf(actor); + players.SortBy(p => p.Position.Z); + var zOrder = players.IndexOf(actor); + if (xOrder >= 0 && zOrder >= 0) + { + if (_exalines.StartingOffset.X > 0) + xOrder = players.Count - 1 - xOrder; + if (_exalines.StartingOffset.Z > 0) + zOrder = players.Count - 1 - zOrder; + + var isFirst = xOrder == 0 || zOrder == 0; + var isTank = actor.Role == Role.Tank; + if (isFirst != isTank) + hints.Add(isTank ? "Stay in front of the group!" : "Hide behind tank!"); + var isFirstX = xOrder < 4; + var isFirstZ = zOrder < 4; + if (isFirstX == isFirstZ) + hints.Add("Position in group properly!"); + } + + if (KnockbackSpots(actor.Position).Any(p => !Module.Bounds.Contains(p - Module.Center))) + hints.Add("About to be knocked into wall!"); + } + } + public override void DrawArenaForeground(int pcSlot, Actor pc) { - if (_ct != null && _exalines != null && _ct.Cleansed[pcSlot]) - Arena.AddCircle(Arena.Center + 0.5f * _exalines.StartingOffset, 1, Colors.Safe); // TODO: better hints... + if (!RewindDone && _ct != null && _exalines != null && _ct.Cleansed[pcSlot]) + { + var vertices = KnockbackSpots(pc.Position).ToList(); + Arena.AddQuad(pc.Position, vertices[0], vertices[2], vertices[1], Colors.Danger); + Arena.AddCircle(Arena.Center + 0.5f * _exalines.StartingOffset, 1, Colors.Safe); + } } public override void OnStatusGain(Actor actor, ActorStatus status) { - if ((SID)status.ID == SID.Return) - Done = true; + switch ((SID)status.ID) + { + case SID.Return: + RewindDone = true; + break; + case SID.Stun: + ReturnDone = true; + break; + } + } + + private IEnumerable KnockbackSpots(WPos starting) + { + if (_exalines != null) + { + var dx = _exalines.StartingOffset.X > 0 ? -20 : +20; + var dz = _exalines.StartingOffset.Z > 0 ? -20 : +20; + yield return starting + new WDir(dx, 0); + yield return starting + new WDir(0, dz); + yield return starting + new WDir(dx, dz); + } } } diff --git a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs index 1ec3b55c60..0a46ad3bf1 100644 --- a/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs +++ b/BossMod/Modules/Dawntrail/Ultimate/FRU/P4MornAfah.cs @@ -4,9 +4,8 @@ class P4MornAfah(BossModule module) : Components.UniformStackSpread(module, 4, 0 { public override void OnCastStarted(Actor caster, ActorCastInfo spell) { - if ((AID)spell.Action.ID == AID.MornAfahOracle) + if ((AID)spell.Action.ID == AID.MornAfahUsurper) { - // note: target is random?.. var target = WorldState.Actors.Find(caster.TargetID); if (target != null) AddStack(target, Module.CastFinishAt(spell, 0.9f)); @@ -15,7 +14,7 @@ public override void OnCastStarted(Actor caster, ActorCastInfo spell) public override void OnEventCast(Actor caster, ActorCastEvent spell) { - if ((AID)spell.Action.ID == AID.MornAfahAOE) // TODO: proper spell... + if ((AID)spell.Action.ID == AID.MornAfahAOE) Stacks.Clear(); } } diff --git a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs index e178f02f50..fd2769f8dc 100644 --- a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs +++ b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs @@ -1,5 +1,6 @@ using BossMod.Autorotation; using ImGuiNET; +using System.IO; namespace BossMod.ReplayVisualization; @@ -211,6 +212,9 @@ private void DrawControlRow() ImGui.Checkbox("Show config", ref _showConfig); ImGui.SameLine(); ImGui.Checkbox("Show debug", ref _showDebug); + ImGui.SameLine(); + if (ImGui.Button("Split")) + SplitLog(); if (_showConfig) _config.Draw(); @@ -503,4 +507,17 @@ private void ResetPF() { _pfVisu = null; } + + private void SplitLog() + { + if (_player.Replay.Ops.Count == 0) + return; + + var player = new ReplayPlayer(_player.Replay); + player.WorldState.Frame.Timestamp = _player.Replay.Ops[0].Timestamp; // so that we get correct name etc. + using (var relogger = new ReplayRecorder(player.WorldState, ReplayLogFormat.BinaryCompressed, false, new FileInfo(_player.Replay.Path).Directory!, "Before")) + player.AdvanceTo(_curTime, () => { }); + using (var relogger = new ReplayRecorder(player.WorldState, ReplayLogFormat.BinaryCompressed, true, new FileInfo(_player.Replay.Path).Directory!, "After")) + player.AdvanceTo(DateTime.MaxValue, () => { }); + } }