Optimize combat for overworld enemies
authorLiza Carvelli <liza@carvel.li>
Sun, 12 Jan 2025 01:28:31 +0000 (02:28 +0100)
committerLiza Carvelli <liza@carvel.li>
Sun, 12 Jan 2025 01:28:31 +0000 (02:28 +0100)
Questionable/Controller/CombatController.cs
Questionable/Controller/MiniTaskController.cs
Questionable/Controller/Steps/Interactions/Combat.cs

index f44c4934ddc1c45576fbd64ab52754475ceddefa..73fe17dfc9e78421e50d0ec8f2ade2c4ab187eee 100644 (file)
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using System.Numerics;
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Game.ClientState.Objects;
 using Dalamud.Game.ClientState.Objects.Enums;
@@ -10,7 +11,8 @@ using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game;
 using FFXIVClientStructs.FFXIV.Client.Game.Object;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
-using FFXIVClientStructs.FFXIV.Common.Math;
+using FFXIVClientStructs.FFXIV.Client.System.Framework;
+using FFXIVClientStructs.FFXIV.Common.Component.BGCollision;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.CombatModules;
 using Questionable.Controller.Steps;
@@ -38,6 +40,7 @@ internal sealed class CombatController : IDisposable
     private CurrentFight? _currentFight;
     private bool _wasInCombat;
     private ulong? _lastTargetId;
+    private List<byte>? _previousQuestVariables;
 
     public CombatController(
         IEnumerable<ICombatModule> combatModules,
@@ -79,7 +82,9 @@ internal sealed class CombatController : IDisposable
                 Data = combatData,
                 LastDistanceCheck = DateTime.Now,
             };
-            _wasInCombat = combatData.SpawnType is EEnemySpawnType.QuestInterruption or EEnemySpawnType.FinishCombatIfAny;
+            _wasInCombat =
+                combatData.SpawnType is EEnemySpawnType.QuestInterruption or EEnemySpawnType.FinishCombatIfAny;
+            UpdateLastTargetAndQuestVariables(null);
             return true;
         }
         else
@@ -115,7 +120,31 @@ internal sealed class CombatController : IDisposable
                     {
                         // wait until the game cleans up the target
                         if (lastTarget.IsDead)
-                            return EStatus.InCombat;
+                        {
+                            ElementId? elementId = _currentFight.Data.ElementId;
+                            QuestProgressInfo? questProgressInfo = elementId != null
+                                ? _questFunctions.GetQuestProgressInfo(elementId)
+                                : null;
+
+                            if (questProgressInfo != null &&
+                                questProgressInfo.Sequence == _currentFight.Data.Sequence &&
+                                QuestWorkUtils.HasCompletionFlags(_currentFight.Data.CompletionQuestVariablesFlags) &&
+                                QuestWorkUtils.MatchesQuestWork(_currentFight.Data.CompletionQuestVariablesFlags,
+                                    questProgressInfo))
+                            {
+                                // would be the final enemy of the bunch
+                                return EStatus.InCombat;
+                            }
+                            else if (questProgressInfo != null &&
+                                     questProgressInfo.Sequence == _currentFight.Data.Sequence &&
+                                     _previousQuestVariables != null &&
+                                     !questProgressInfo.Variables.SequenceEqual(_previousQuestVariables))
+                            {
+                                UpdateLastTargetAndQuestVariables(null);
+                            }
+                            else
+                                return EStatus.InCombat;
+                        }
                     }
                     else
                         _lastTargetId = null;
@@ -372,9 +401,18 @@ internal sealed class CombatController : IDisposable
         float hitboxOffset = player.HitboxRadius + gameObject.HitboxRadius;
         float actualDistance = Vector3.Distance(player.Position, gameObject.Position);
         float maxDistance = player.ClassJob.ValueNullable?.Role is 3 or 4 ? 20f : 2.9f;
-        if (actualDistance - hitboxOffset >= maxDistance)
+        bool outOfRange = actualDistance - hitboxOffset >= maxDistance;
+        bool isInLineOfSight = IsInLineOfSight(gameObject);
+        if (outOfRange || !isInLineOfSight)
         {
-            if (actualDistance - hitboxOffset <= 5)
+            bool useNavmesh = actualDistance - hitboxOffset > 5f;
+            if (!outOfRange && !isInLineOfSight)
+            {
+                maxDistance = Math.Min(maxDistance, actualDistance) / 2;
+                useNavmesh = true;
+            }
+
+            if (!useNavmesh)
             {
                 _logger.LogInformation("Moving to {TargetName} ({DataId}) to attack", gameObject.Name,
                     gameObject.DataId);
@@ -391,6 +429,44 @@ internal sealed class CombatController : IDisposable
         }
     }
 
+    internal unsafe bool IsInLineOfSight(IGameObject target)
+    {
+        Vector3 sourcePos = _clientState.LocalPlayer!.Position;
+        sourcePos.Y += 2;
+
+        Vector3 targetPos = target.Position;
+        targetPos.Y += 2;
+
+        Vector3 direction = targetPos - sourcePos;
+        float distance = direction.Length();
+
+        direction = Vector3.Normalize(direction);
+
+        Vector3 originVect = new Vector3(sourcePos.X, sourcePos.Y, sourcePos.Z);
+        Vector3 directionVect = new Vector3(direction.X, direction.Y, direction.Z);
+
+        RaycastHit hit;
+        var flags = stackalloc int[] { 0x4000, 0, 0x4000, 0 };
+        var isLoSBlocked =
+            Framework.Instance()->BGCollisionModule->RaycastMaterialFilter(&hit, &originVect, &directionVect, distance,
+                1, flags);
+
+        return isLoSBlocked == false;
+    }
+
+    private void UpdateLastTargetAndQuestVariables(IGameObject? target)
+    {
+        _lastTargetId = target?.GameObjectId;
+        _previousQuestVariables = _currentFight!.Data.ElementId != null
+            ? _questFunctions.GetQuestProgressInfo(_currentFight.Data.ElementId)?.Variables
+            : null;
+        /*
+        _logger.LogTrace("UpdateTargetData: {TargetId}; {QuestVariables}",
+            target?.GameObjectId.ToString("X8", CultureInfo.InvariantCulture) ?? "null",
+            _previousQuestVariables != null ? string.Join(", ", _previousQuestVariables) : "null");
+        */
+    }
+
     public void Stop(string label)
     {
         using var scope = _logger.BeginScope(label);
@@ -422,6 +498,8 @@ internal sealed class CombatController : IDisposable
     public sealed class CombatData
     {
         public required ElementId? ElementId { get; init; }
+        public required int Sequence { get; init; }
+        public required IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; init; }
         public required EEnemySpawnType SpawnType { get; init; }
         public required List<uint> KillEnemyDataIds { get; init; }
         public required List<ComplexCombatData> ComplexCombatDatas { get; init; }
index 06e5d874d983afd014090667e19417c32757e1a8..6055a68c64bfb5102b8c033ee52682f675f8a72d 100644 (file)
@@ -173,7 +173,7 @@ internal abstract class MiniTaskController<T>
             if (_condition[ConditionFlag.Mounted])
                 tasks.Add(new Mount.UnmountTask());
 
-            tasks.Add(Combat.Factory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], [], null));
+            tasks.Add(Combat.Factory.CreateTask(null, -1, false, EEnemySpawnType.QuestInterruption, [], [], [], null));
             tasks.Add(new WaitAtEnd.WaitDelay());
             _taskQueue.InterruptWith(tasks);
         }
index a4d9594d08ed9d8dfdd52117647e1ff85aa67f65..463c32cec501bd17023a08f6cf32476b4e5f068d 100644 (file)
@@ -102,17 +102,30 @@ internal static class Combat
             ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
 
             bool isLastStep = sequence.Steps.Last() == step;
-            return CreateTask(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
-                step.CompletionQuestVariablesFlags, step.ComplexCombatData, step.CombatItemUse);
+            return CreateTask(quest.Id,
+                sequence.Sequence,
+                isLastStep,
+                step.EnemySpawnType.Value,
+                step.KillEnemyDataIds,
+                step.CompletionQuestVariablesFlags,
+                step.ComplexCombatData,
+                step.CombatItemUse);
         }
 
-        internal static Task CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType,
-            IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags,
-            IList<ComplexCombatData> complexCombatData, CombatItemUse? combatItemUse)
+        internal static Task CreateTask(ElementId? elementId,
+            int sequence,
+            bool isLastStep,
+            EEnemySpawnType enemySpawnType,
+            IList<uint> killEnemyDataIds,
+            IList<QuestWorkValue?> completionQuestVariablesFlags,
+            IList<ComplexCombatData> complexCombatData,
+            CombatItemUse? combatItemUse)
         {
             return new Task(new CombatController.CombatData
             {
                 ElementId = elementId,
+                Sequence = sequence,
+                CompletionQuestVariablesFlags = completionQuestVariablesFlags,
                 SpawnType = enemySpawnType,
                 KillEnemyDataIds = killEnemyDataIds.ToList(),
                 ComplexCombatDatas = complexCombatData.ToList(),