diff --git a/BossMod/Autorotation/RotationModule.cs b/BossMod/Autorotation/RotationModule.cs index a5771f72a7..f0ae56acb3 100644 --- a/BossMod/Autorotation/RotationModule.cs +++ b/BossMod/Autorotation/RotationModule.cs @@ -143,7 +143,7 @@ public int FindDutyActionSlot(ActionID action, ActionID other) protected (float Left, float In) EstimateRaidBuffTimings(Actor? primaryTarget) { - if (primaryTarget?.OID != 0x385) + if (primaryTarget == null || !primaryTarget.IsStrikingDummy) return (Bossmods.RaidCooldowns.DamageBuffLeft(Player), Bossmods.RaidCooldowns.NextDamageBuffIn()); // hack for a dummy: expect that raidbuffs appear at 7.8s and then every 120s diff --git a/BossMod/Data/Actor.cs b/BossMod/Data/Actor.cs index e412d5bae6..6e13ea4919 100644 --- a/BossMod/Data/Actor.cs +++ b/BossMod/Data/Actor.cs @@ -73,7 +73,8 @@ public record struct ActorModelState(byte ModelState, byte AnimState1, byte Anim public record struct PendingEffect(uint GlobalSequence, int TargetIndex, ulong SourceInstanceId, DateTime Expiration); public record struct PendingEffectDelta(PendingEffect Effect, int Value); -public record struct PendingEffectStatus(PendingEffect Effect, uint StatusId, byte ExtraLo); +public record struct PendingEffectStatus(PendingEffect Effect, uint StatusId); +public record struct PendingEffectStatusExtra(PendingEffect Effect, uint StatusId, byte ExtraLo); public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string name, uint nameID, ActorType type, Class classID, int level, Vector4 posRot, float hitboxRadius = 1, ActorHPMP hpmp = default, bool targetable = true, bool ally = false, ulong ownerID = 0, uint fateID = 0) { @@ -108,7 +109,8 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam // all pending lists are sorted by expiration time public List PendingHPDifferences = []; // damage and heal effects applied to the target that were not confirmed yet public List PendingMPDifferences = []; - public List PendingStatuses = []; + public List PendingStatuses = []; + public List PendingDispels = []; public List PendingKnockbacks = []; public Role Role => Class.GetRole(); @@ -120,13 +122,14 @@ public sealed class Actor(ulong instanceID, uint oid, int spawnIndex, string nam public bool Omnidirectional => Utils.CharacterIsOmnidirectional(OID); public bool IsDeadOrDestroyed => IsDead || IsDestroyed; public bool IsFriendlyNPC => Type == ActorType.Enemy && IsAlly && IsTargetable; + public bool IsStrikingDummy => NameID == 541; // this is a hack, but striking dummies are special in some ways public int CharacterSpawnIndex => SpawnIndex < 200 && (SpawnIndex & 1) == 0 ? (SpawnIndex >> 1) : -1; // [0,100) for 'real' characters, -1 otherwise public int PendingHPDiffence => PendingHPDifferences.Sum(p => p.Value); public int PendingMPDiffence => PendingMPDifferences.Sum(p => p.Value); public int PredictedHPRaw => (int)HPMP.CurHP + PendingHPDiffence; public int PredictedMPRaw => (int)HPMP.CurMP + PendingMPDiffence; public int PredictedHPClamped => Math.Clamp(PredictedHPRaw, 0, (int)HPMP.MaxHP); - public bool PredictedDead => PredictedHPRaw <= 1; + public bool PredictedDead => PredictedHPRaw <= 1 && !IsStrikingDummy; // if expirationForPredicted is not null, search pending first, and return one if found; in that case only low byte of extra will be set public ActorStatus? FindStatus(uint sid, DateTime? expirationForPending = null) diff --git a/BossMod/Data/ActorState.cs b/BossMod/Data/ActorState.cs index 4201ceb8d6..03599d603f 100644 --- a/BossMod/Data/ActorState.cs +++ b/BossMod/Data/ActorState.cs @@ -95,6 +95,11 @@ private void AddPendingEffects(Actor source, ActorCastEvent ev, DateTime timesta // note: effect reapplication (eg kardia) or some 'instant' effects (eg ast draw/earthly star) won't get confirmations effTarget.PendingStatuses.Add(new(header, eff.Value, eff.Param2)); break; + case ActionEffectType.RecoveredFromStatusEffect: + case ActionEffectType.LoseStatusEffectTarget: + case ActionEffectType.LoseStatusEffectSource: + effTarget.PendingDispels.Add(new(header, eff.Value)); + break; case ActionEffectType.Knockback: case ActionEffectType.Attract1: case ActionEffectType.Attract2: @@ -114,6 +119,7 @@ private void RemovePendingEffects(Actor target, RemovePendingEffectPredicate pre target.PendingHPDifferences.RemoveAll(e => predicate(e.Effect)); target.PendingMPDifferences.RemoveAll(e => predicate(e.Effect)); target.PendingStatuses.RemoveAll(e => predicate(e.Effect)); + target.PendingDispels.RemoveAll(e => predicate(e.Effect)); target.PendingKnockbacks.RemoveAll(e => predicate(e)); } diff --git a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs index da834e76e7..87f8ec4f2e 100644 --- a/BossMod/Replay/Visualization/ReplayDetailsWindow.cs +++ b/BossMod/Replay/Visualization/ReplayDetailsWindow.cs @@ -301,24 +301,28 @@ private void DrawCommonColumns(Actor actor) var numRealStatuses = actor.Statuses.Count(s => s.ID != 0); var mouseOffset = ImGui.GetMousePos() - ImGui.GetWindowPos() - ImGui.GetCursorPos(); var mouseInColumn = mouseOffset.X >= 0 && mouseOffset.Y >= 0 && mouseOffset.X < ImGui.GetColumnWidth() && mouseOffset.Y < ImGui.GetFontSize() + 2 * ImGui.GetStyle().FramePadding.Y; - ImGui.TextUnformatted($"{(actor.PendingKnockbacks.Count > 0 ? "Knockbacks pending, " : "")}{(actor.MountId != 0 ? $"Mounted ({actor.MountId}), " : "")}{numRealStatuses} + {actor.PendingStatuses.Count} statuses"); - if (mouseInColumn && numRealStatuses + actor.PendingStatuses.Count > 0) + ImGui.TextUnformatted($"{(actor.PendingKnockbacks.Count > 0 ? "Knockbacks pending, " : "")}{(actor.MountId != 0 ? $"Mounted ({actor.MountId}), " : "")}{numRealStatuses} + {actor.PendingStatuses.Count} statuses, {actor.PendingDispels.Count} dispels"); + if (mouseInColumn && numRealStatuses + actor.PendingStatuses.Count + actor.PendingDispels.Count > 0) { using var tooltip = ImRaii.Tooltip(); if (tooltip) { - string fromString(ulong instanceId) => instanceId == 0 ? "" : $", from {_player.WorldState.Actors.Find(instanceId)?.ToString() ?? instanceId.ToString("X")}"; + string fromString(string prefix, ulong instanceId) => instanceId == 0 ? "" : $", {prefix} {_player.WorldState.Actors.Find(instanceId)?.ToString() ?? instanceId.ToString("X")}"; for (int i = 0; i < actor.Statuses.Length; ++i) { ref var s = ref actor.Statuses[i]; if (s.ID != 0) { - ImGui.TextUnformatted($"[{i}] {Utils.StatusString(s.ID)} ({s.Extra}): {Utils.StatusTimeString(s.ExpireAt, _player.WorldState.CurrentTime)}{fromString(s.SourceID)}"); + ImGui.TextUnformatted($"[{i}] {Utils.StatusString(s.ID)} ({s.Extra}): {Utils.StatusTimeString(s.ExpireAt, _player.WorldState.CurrentTime)}{fromString("from", s.SourceID)}"); } } foreach (ref var s in actor.PendingStatuses.AsSpan()) { - ImGui.TextUnformatted($"[pending] {Utils.StatusString(s.StatusId)} ({s.ExtraLo}){fromString(s.Effect.SourceInstanceId)}"); + ImGui.TextUnformatted($"[pending] {Utils.StatusString(s.StatusId)} ({s.ExtraLo}){fromString("from", s.Effect.SourceInstanceId)}"); + } + foreach (ref var s in actor.PendingDispels.AsSpan()) + { + ImGui.TextUnformatted($"[dispel] {Utils.StatusString(s.StatusId)}{fromString("by", s.Effect.SourceInstanceId)}"); } } }