732 lines
23 KiB
C#
732 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using Dalamud.Game.ClientState.Conditions;
|
|
using Dalamud.Game.ClientState.Objects.Enums;
|
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
using Dalamud.Game.ClientState.Objects.Types;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
|
using FFXIVClientStructs.FFXIV.Common.Component.BGCollision;
|
|
using Lumina.Excel.Sheets;
|
|
using Microsoft.Extensions.Logging;
|
|
using Questionable.Controller.CombatModules;
|
|
using Questionable.Controller.Steps;
|
|
using Questionable.Controller.Utils;
|
|
using Questionable.External;
|
|
using Questionable.Functions;
|
|
using Questionable.Model;
|
|
using Questionable.Model.Questing;
|
|
|
|
namespace Questionable.Controller;
|
|
|
|
internal sealed class CombatController : IDisposable
|
|
{
|
|
private sealed class CurrentFight
|
|
{
|
|
public required ICombatModule Module { get; init; }
|
|
|
|
public required CombatData Data { get; init; }
|
|
|
|
public required long LastDistanceCheck { get; set; }
|
|
|
|
public bool HasAttemptedFateSync { get; set; }
|
|
|
|
public Dictionary<int, int> KillCountsByComplexDataIndex { get; } = new Dictionary<int, int>();
|
|
|
|
public int TotalKillCount { get; set; }
|
|
|
|
public HashSet<ulong> CountedEnemies { get; } = new HashSet<ulong>();
|
|
}
|
|
|
|
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; }
|
|
|
|
public required CombatItemUse? CombatItemUse { get; init; }
|
|
|
|
public HashSet<int> CompletedComplexDatas { get; } = new HashSet<int>();
|
|
}
|
|
|
|
public enum EStatus
|
|
{
|
|
NotStarted,
|
|
InCombat,
|
|
Moving,
|
|
Complete
|
|
}
|
|
|
|
private const float MaxNameplateRange = 50f;
|
|
|
|
private const float MaxSelfDefenseRange = 30f;
|
|
|
|
private readonly List<ICombatModule> _combatModules;
|
|
|
|
private readonly MovementController _movementController;
|
|
|
|
private readonly GameFunctions _gameFunctions;
|
|
|
|
private readonly BossModIpc _bossModIpc;
|
|
|
|
private readonly ITargetManager _targetManager;
|
|
|
|
private readonly IObjectTable _objectTable;
|
|
|
|
private readonly ICondition _condition;
|
|
|
|
private readonly IClientState _clientState;
|
|
|
|
private readonly QuestFunctions _questFunctions;
|
|
|
|
private readonly IToastGui _toastGui;
|
|
|
|
private readonly ILogger<CombatController> _logger;
|
|
|
|
private readonly string? _cannotSeeTargetText;
|
|
|
|
private readonly Dictionary<ulong, long> _losIgnoreList = new Dictionary<ulong, long>();
|
|
|
|
private CurrentFight? _currentFight;
|
|
|
|
private bool _wasInCombat;
|
|
|
|
private bool _passiveBossModEnabled;
|
|
|
|
private ulong? _lastTargetId;
|
|
|
|
private List<byte>? _previousQuestVariables;
|
|
|
|
private ulong _lastLoSFailTargetId;
|
|
|
|
private int _losFailCount;
|
|
|
|
private long _lastLoSFailTimeMs;
|
|
|
|
public bool IsRunning => _currentFight != null;
|
|
|
|
public CombatController(IEnumerable<ICombatModule> combatModules, MovementController movementController, GameFunctions gameFunctions, BossModIpc bossModIpc, ITargetManager targetManager, IObjectTable objectTable, ICondition condition, IClientState clientState, QuestFunctions questFunctions, IToastGui toastGui, IDataManager dataManager, ILogger<CombatController> logger)
|
|
{
|
|
_combatModules = combatModules.ToList();
|
|
_movementController = movementController;
|
|
_gameFunctions = gameFunctions;
|
|
_bossModIpc = bossModIpc;
|
|
_targetManager = targetManager;
|
|
_objectTable = objectTable;
|
|
_condition = condition;
|
|
_clientState = clientState;
|
|
_questFunctions = questFunctions;
|
|
_toastGui = toastGui;
|
|
_logger = logger;
|
|
_clientState.TerritoryChanged += TerritoryChanged;
|
|
toastGui.ErrorToast += OnErrorToast;
|
|
try
|
|
{
|
|
_cannotSeeTargetText = dataManager.GetExcelSheet<LogMessage>()?.GetRowOrDefault(562u)?.Text.ExtractText();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogDebug(exception, "Failed to load LoS failure text from game data");
|
|
_cannotSeeTargetText = null;
|
|
}
|
|
}
|
|
|
|
public bool Start(CombatData combatData)
|
|
{
|
|
Stop("Starting combat");
|
|
ICombatModule combatModule = _combatModules.FirstOrDefault((ICombatModule x) => x.CanHandleFight(combatData));
|
|
if (combatModule == null)
|
|
{
|
|
return false;
|
|
}
|
|
if (!combatModule.Start(combatData))
|
|
{
|
|
return false;
|
|
}
|
|
_currentFight = new CurrentFight
|
|
{
|
|
Module = combatModule,
|
|
Data = combatData,
|
|
LastDistanceCheck = Environment.TickCount64
|
|
};
|
|
EEnemySpawnType spawnType = combatData.SpawnType;
|
|
bool wasInCombat = (uint)(spawnType - 8) <= 1u;
|
|
_wasInCombat = wasInCombat;
|
|
UpdateLastTargetAndQuestVariables(null);
|
|
_logger.LogDebug("Combat started: Module={Module}, SpawnType={SpawnType}, BossModVariant={Variant}, BossModSupported={Supported}", combatModule.GetType().Name, combatData.SpawnType, _bossModIpc.Variant, _bossModIpc.IsSupported());
|
|
if (!(combatModule is BossModModule) && (!(combatModule is ItemUseModule itemUseModule) || !(itemUseModule.Delegate is BossModModule)) && _bossModIpc.IsSupported() && combatData.SpawnType != EEnemySpawnType.QuestInterruption)
|
|
{
|
|
_bossModIpc.EnableAi(BossModIpc.EPreset.Passive);
|
|
_passiveBossModEnabled = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public EStatus Update()
|
|
{
|
|
if (_currentFight == null)
|
|
{
|
|
return EStatus.Complete;
|
|
}
|
|
if (_movementController.IsPathfinding || _movementController.IsPathRunning || !_movementController.HasBeenMovingForAtLeast(1000))
|
|
{
|
|
return EStatus.Moving;
|
|
}
|
|
if (_currentFight.Data.SpawnType == EEnemySpawnType.OverworldEnemies)
|
|
{
|
|
if (_targetManager.Target != null)
|
|
{
|
|
_lastTargetId = _targetManager.Target.GameObjectId;
|
|
}
|
|
else if (_lastTargetId.HasValue)
|
|
{
|
|
IGameObject gameObject = _objectTable.FirstOrDefault((IGameObject x) => x.GameObjectId == _lastTargetId);
|
|
if (gameObject != null)
|
|
{
|
|
if (gameObject.IsDead)
|
|
{
|
|
TrackKillIfNeeded(gameObject);
|
|
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))
|
|
{
|
|
return EStatus.InCombat;
|
|
}
|
|
if (questProgressInfo == null || questProgressInfo.Sequence != _currentFight.Data.Sequence || _previousQuestVariables == null || questProgressInfo.Variables.SequenceEqual(_previousQuestVariables))
|
|
{
|
|
return EStatus.InCombat;
|
|
}
|
|
UpdateLastTargetAndQuestVariables(null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_lastTargetId = null;
|
|
}
|
|
}
|
|
}
|
|
IGameObject target = _targetManager.Target;
|
|
if (target != null)
|
|
{
|
|
int item = GetKillPriority(target).Priority;
|
|
IGameObject gameObject2 = FindNextTarget();
|
|
int num = ((gameObject2 != null) ? GetKillPriority(gameObject2).Priority : 0);
|
|
if (gameObject2 != null && gameObject2.Equals(target))
|
|
{
|
|
if (!IsMovingOrShouldMove(target))
|
|
{
|
|
try
|
|
{
|
|
_currentFight.Module.Update(target);
|
|
}
|
|
catch (TaskException)
|
|
{
|
|
Stop("Combat module failure");
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
else if (gameObject2 != null)
|
|
{
|
|
if (num > item || item == 0)
|
|
{
|
|
SetTarget(gameObject2);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SetTarget(null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
IGameObject gameObject3 = FindNextTarget();
|
|
if (gameObject3 != null && !gameObject3.IsDead)
|
|
{
|
|
SetTarget(gameObject3);
|
|
}
|
|
}
|
|
if (_condition[ConditionFlag.InCombat])
|
|
{
|
|
_wasInCombat = true;
|
|
return EStatus.InCombat;
|
|
}
|
|
if (_wasInCombat)
|
|
{
|
|
return EStatus.Complete;
|
|
}
|
|
return EStatus.InCombat;
|
|
}
|
|
|
|
private void TrackKillIfNeeded(IGameObject deadEnemy)
|
|
{
|
|
if (_currentFight == null || !(deadEnemy is IBattleNpc battleNpc) || !_currentFight.CountedEnemies.Add(deadEnemy.GameObjectId))
|
|
{
|
|
return;
|
|
}
|
|
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
|
|
for (int i = 0; i < complexCombatDatas.Count; i++)
|
|
{
|
|
ComplexCombatData complexCombatData = complexCombatDatas[i];
|
|
if (complexCombatData.DataId == battleNpc.BaseId && (!complexCombatData.NameId.HasValue || complexCombatData.NameId == battleNpc.NameId) && complexCombatData.MinimumKillCount.HasValue)
|
|
{
|
|
int num = (_currentFight.KillCountsByComplexDataIndex[i] = _currentFight.KillCountsByComplexDataIndex.GetValueOrDefault(i) + 1);
|
|
int num3 = num;
|
|
_logger.LogDebug("Tracked kill for ComplexCombatData[{Index}] ({DataId}): {Count}/{Required}", i, complexCombatData.DataId, num3, complexCombatData.MinimumKillCount.Value);
|
|
return;
|
|
}
|
|
}
|
|
if (complexCombatDatas.Count == 0 && _currentFight.Data.KillEnemyDataIds.Contains(battleNpc.BaseId))
|
|
{
|
|
_currentFight.TotalKillCount++;
|
|
_logger.LogDebug("Tracked kill for KillEnemyDataIds ({DataId}): Total count = {Count}", battleNpc.BaseId, _currentFight.TotalKillCount);
|
|
}
|
|
}
|
|
|
|
private unsafe IGameObject? FindNextTarget()
|
|
{
|
|
if (_currentFight == null)
|
|
{
|
|
return null;
|
|
}
|
|
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
|
|
if (complexCombatDatas.Count > 0)
|
|
{
|
|
for (int i = 0; i < complexCombatDatas.Count; i++)
|
|
{
|
|
if (_currentFight.Data.CompletedComplexDatas.Contains(i))
|
|
{
|
|
continue;
|
|
}
|
|
ComplexCombatData complexCombatData = complexCombatDatas[i];
|
|
bool hasValue = complexCombatData.MinimumKillCount.HasValue;
|
|
bool flag = complexCombatData.RewardItemId.HasValue && complexCombatData.RewardItemCount.HasValue;
|
|
bool flag2 = QuestWorkUtils.HasCompletionFlags(complexCombatData.CompletionQuestVariablesFlags);
|
|
if (hasValue || flag)
|
|
{
|
|
bool flag3 = true;
|
|
bool flag4 = true;
|
|
if (hasValue)
|
|
{
|
|
int valueOrDefault = _currentFight.KillCountsByComplexDataIndex.GetValueOrDefault(i, 0);
|
|
flag3 = valueOrDefault >= complexCombatData.MinimumKillCount.Value;
|
|
if (flag3)
|
|
{
|
|
_logger.LogDebug("Kill count condition met for ComplexCombatData[{Index}]: {Count}/{Required}", i, valueOrDefault, complexCombatData.MinimumKillCount.Value);
|
|
}
|
|
}
|
|
if (flag)
|
|
{
|
|
int inventoryItemCount = InventoryManager.Instance()->GetInventoryItemCount(complexCombatData.RewardItemId.Value, isHq: false, checkEquipped: true, checkArmory: true, 0);
|
|
flag4 = inventoryItemCount >= complexCombatData.RewardItemCount.Value;
|
|
if (flag4)
|
|
{
|
|
_logger.LogDebug("Item count condition met for ComplexCombatData[{Index}]: {ItemId} = {Count}/{Required}", i, complexCombatData.RewardItemId.Value, inventoryItemCount, complexCombatData.RewardItemCount.Value);
|
|
}
|
|
}
|
|
if (flag3 && flag4)
|
|
{
|
|
_logger.LogDebug("Complex combat condition fulfilled for ComplexCombatData[{Index}] ({DataId}): KillCount={KillCountMet}, Items={ItemMet}", i, complexCombatData.DataId, flag3, flag4);
|
|
_currentFight.Data.CompletedComplexDatas.Add(i);
|
|
}
|
|
}
|
|
else if (flag2 && _currentFight.Data.ElementId is QuestId elementId)
|
|
{
|
|
QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(elementId);
|
|
if (questProgressInfo != null && QuestWorkUtils.MatchesQuestWork(complexCombatData.CompletionQuestVariablesFlags, questProgressInfo))
|
|
{
|
|
_logger.LogDebug("Complex combat condition fulfilled for ComplexCombatData[{Index}] ({DataId}): QuestWork (fallback)", i, complexCombatData.DataId);
|
|
_currentFight.Data.CompletedComplexDatas.Add(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
IPlayerCharacter localPlayer = _objectTable.LocalPlayer;
|
|
Vector3 value = localPlayer?.Position ?? Vector3.Zero;
|
|
IGameObject gameObject = null;
|
|
int num = 0;
|
|
float num2 = float.MaxValue;
|
|
bool flag5 = _logger.IsEnabled(LogLevel.Debug);
|
|
foreach (IGameObject item in _objectTable)
|
|
{
|
|
var (num3, text) = GetKillPriority(item, localPlayer);
|
|
if (num3 <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
if (_losIgnoreList.TryGetValue(item.GameObjectId, out var value2))
|
|
{
|
|
if (Environment.TickCount64 < value2)
|
|
{
|
|
if (flag5)
|
|
{
|
|
_logger.LogDebug("Target {Name} ({Id:X8}) skipped - LoS ignore list ({Remaining}ms left)", item.Name, item.GameObjectId, value2 - Environment.TickCount64);
|
|
}
|
|
continue;
|
|
}
|
|
_losIgnoreList.Remove(item.GameObjectId);
|
|
if (flag5)
|
|
{
|
|
_logger.LogDebug("Target {Name} ({Id:X8}) - LoS ignore expired", item.Name, item.GameObjectId);
|
|
}
|
|
}
|
|
float num4 = Vector3.Distance(item.Position, value);
|
|
if (flag5)
|
|
{
|
|
_logger.LogDebug("Target candidate: {Name} ({Id:X8}) BaseId={BaseId} Priority={Priority} ({Reason}) Distance={Distance:N1}", item.Name, item.GameObjectId, item.BaseId, num3, text, num4);
|
|
}
|
|
if (num3 > num || (num3 == num && num4 < num2))
|
|
{
|
|
gameObject = item;
|
|
num = num3;
|
|
num2 = num4;
|
|
}
|
|
}
|
|
ulong? num5 = gameObject?.GameObjectId;
|
|
if (num5 != _lastTargetId)
|
|
{
|
|
if (gameObject != null)
|
|
{
|
|
_logger.LogDebug("Selected target: {Name} ({Id:X8}) BaseId={BaseId} Priority={Priority} Distance={Distance:N1}", gameObject.Name, gameObject.GameObjectId, gameObject.BaseId, num, num2);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("No valid target found");
|
|
}
|
|
_lastTargetId = num5;
|
|
}
|
|
if (gameObject != null && num <= 15 && !_condition[ConditionFlag.InCombat])
|
|
{
|
|
return null;
|
|
}
|
|
if (gameObject != null && _currentFight.Data.SpawnType == EEnemySpawnType.FateEnemies && !_currentFight.HasAttemptedFateSync)
|
|
{
|
|
ushort currentFateId = _gameFunctions.GetCurrentFateId();
|
|
if (currentFateId != 0)
|
|
{
|
|
_logger.LogDebug("Checking FATE sync for FATE {FateId}", currentFateId);
|
|
_gameFunctions.SyncToFate(currentFateId);
|
|
_currentFight.HasAttemptedFateSync = true;
|
|
}
|
|
}
|
|
return gameObject;
|
|
}
|
|
|
|
public (int Priority, string Reason) GetKillPriority(IGameObject gameObject)
|
|
{
|
|
return GetKillPriority(gameObject, _objectTable.LocalPlayer);
|
|
}
|
|
|
|
private unsafe (int Priority, string Reason) GetKillPriority(IGameObject gameObject, IGameObject? localPlayer)
|
|
{
|
|
var (num, text) = GetRawKillPriority(gameObject, localPlayer);
|
|
if (!num.HasValue)
|
|
{
|
|
if (gameObject is IBattleNpc { IsDead: false, IsTargetable: not false } battleNpc && battleNpc.StatusFlags.HasFlag(StatusFlags.InCombat) && localPlayer != null && Vector3.Distance(localPlayer.Position, gameObject.Position) <= 30f)
|
|
{
|
|
if (gameObject.TargetObjectId == localPlayer.GameObjectId)
|
|
{
|
|
return (Priority: 15, Reason: text + "/SelfDefense-Targeted");
|
|
}
|
|
Hater hater = UIState.Instance()->Hater;
|
|
for (int i = 0; i < hater.HaterCount; i++)
|
|
{
|
|
if (hater.Haters[i].EntityId == gameObject.GameObjectId)
|
|
{
|
|
return (Priority: 10, Reason: text + "/SelfDefense-Enmity");
|
|
}
|
|
}
|
|
}
|
|
return (Priority: 0, Reason: text);
|
|
}
|
|
if (gameObject is IBattleNpc battleNpc2 && battleNpc2.StatusFlags.HasFlag(StatusFlags.InCombat))
|
|
{
|
|
if (gameObject.TargetObjectId == localPlayer?.GameObjectId)
|
|
{
|
|
return (Priority: num.Value + 150, Reason: text + "/Targeted");
|
|
}
|
|
Hater hater2 = UIState.Instance()->Hater;
|
|
for (int j = 0; j < hater2.HaterCount; j++)
|
|
{
|
|
if (hater2.Haters[j].EntityId == gameObject.GameObjectId)
|
|
{
|
|
return (Priority: num.Value + 125, Reason: text + "/Enmity");
|
|
}
|
|
}
|
|
}
|
|
return (Priority: num.Value, Reason: text);
|
|
}
|
|
|
|
private unsafe (int? Priority, string Reason) GetRawKillPriority(IGameObject gameObject, IGameObject? localPlayer)
|
|
{
|
|
if (_currentFight == null)
|
|
{
|
|
return (Priority: null, Reason: "Not Fighting");
|
|
}
|
|
if (!(gameObject is IBattleNpc battleNpc))
|
|
{
|
|
return (Priority: null, Reason: "Not BattleNpc");
|
|
}
|
|
if (!_currentFight.Module.CanAttack(battleNpc))
|
|
{
|
|
return (Priority: null, Reason: "Can't attack");
|
|
}
|
|
if (battleNpc.IsDead)
|
|
{
|
|
return (Priority: null, Reason: "Dead");
|
|
}
|
|
if (!battleNpc.IsTargetable)
|
|
{
|
|
return (Priority: null, Reason: "Untargetable");
|
|
}
|
|
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
|
|
GameObject* address = (GameObject*)gameObject.Address;
|
|
if (address->FateId != 0 && _currentFight.Data.SpawnType != EEnemySpawnType.FateEnemies && gameObject.TargetObjectId != localPlayer?.GameObjectId)
|
|
{
|
|
return (Priority: null, Reason: "FATE mob");
|
|
}
|
|
Vector3 value = localPlayer?.Position ?? Vector3.Zero;
|
|
bool flag;
|
|
switch (_currentFight.Data.SpawnType)
|
|
{
|
|
case EEnemySpawnType.FinishCombatIfAny:
|
|
flag = false;
|
|
break;
|
|
case EEnemySpawnType.FateEnemies:
|
|
flag = false;
|
|
break;
|
|
case EEnemySpawnType.OverworldEnemies:
|
|
if (Vector3.Distance(value, battleNpc.Position) >= 50f)
|
|
{
|
|
flag = false;
|
|
break;
|
|
}
|
|
goto default;
|
|
default:
|
|
flag = true;
|
|
break;
|
|
}
|
|
bool flag2 = flag;
|
|
if (complexCombatDatas.Count > 0)
|
|
{
|
|
for (int i = 0; i < complexCombatDatas.Count; i++)
|
|
{
|
|
if (!_currentFight.Data.CompletedComplexDatas.Contains(i) && (!flag2 || complexCombatDatas[i].IgnoreQuestMarker || address->NamePlateIconId != 0) && complexCombatDatas[i].DataId == battleNpc.BaseId && (!complexCombatDatas[i].NameId.HasValue || complexCombatDatas[i].NameId == battleNpc.NameId))
|
|
{
|
|
return (Priority: 100, Reason: "CCD");
|
|
}
|
|
}
|
|
}
|
|
else if ((!flag2 || address->NamePlateIconId != 0) && _currentFight.Data.KillEnemyDataIds.Contains(battleNpc.BaseId))
|
|
{
|
|
return (Priority: 90, Reason: "KED");
|
|
}
|
|
Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind battleNpcKind = battleNpc.BattleNpcKind;
|
|
if ((battleNpcKind == Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind.BNpcPart || battleNpcKind == Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind.Combatant) ? true : false)
|
|
{
|
|
uint namePlateIconId = address->NamePlateIconId;
|
|
if ((namePlateIconId == 60093 || namePlateIconId == 60732) ? true : false)
|
|
{
|
|
return (Priority: null, Reason: "FATE NPC");
|
|
}
|
|
return (Priority: null, Reason: "Not part of quest");
|
|
}
|
|
return (Priority: null, Reason: "Wrong BattleNpcKind");
|
|
}
|
|
|
|
private void SetTarget(IGameObject? target)
|
|
{
|
|
if (target == null)
|
|
{
|
|
if (_targetManager.Target != null)
|
|
{
|
|
_logger.LogDebug("Clearing target");
|
|
_targetManager.Target = null;
|
|
}
|
|
return;
|
|
}
|
|
IPlayerCharacter localPlayer = _objectTable.LocalPlayer;
|
|
float num = ((localPlayer != null) ? Vector3.Distance(localPlayer.Position, target.Position) : 0f);
|
|
bool flag = _currentFight?.Module is BossModModule;
|
|
_logger.LogDebug("Setting target to {TargetName} ({TargetId:X8}), distance: {Distance:N2}", target.Name.ToString(), target.GameObjectId, num);
|
|
_targetManager.Target = target;
|
|
if (!flag)
|
|
{
|
|
MoveToTarget(target);
|
|
}
|
|
}
|
|
|
|
private bool IsMovingOrShouldMove(IGameObject gameObject)
|
|
{
|
|
if (_currentFight?.Module is BossModModule)
|
|
{
|
|
return false;
|
|
}
|
|
if (_movementController.IsPathfinding || _movementController.IsPathRunning)
|
|
{
|
|
return true;
|
|
}
|
|
if (Environment.TickCount64 > _currentFight.LastDistanceCheck + 10000)
|
|
{
|
|
MoveToTarget(gameObject);
|
|
_currentFight.LastDistanceCheck = Environment.TickCount64;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void MoveToTarget(IGameObject gameObject)
|
|
{
|
|
IPlayerCharacter localPlayer = _objectTable.LocalPlayer;
|
|
if (localPlayer == null)
|
|
{
|
|
return;
|
|
}
|
|
float num = localPlayer.HitboxRadius + gameObject.HitboxRadius;
|
|
float num2 = Vector3.Distance(localPlayer.Position, gameObject.Position);
|
|
byte? b = localPlayer.ClassJob.ValueNullable?.Role;
|
|
bool flag;
|
|
if (b.HasValue)
|
|
{
|
|
byte valueOrDefault = b.GetValueOrDefault();
|
|
if ((uint)(valueOrDefault - 3) <= 1u)
|
|
{
|
|
flag = true;
|
|
goto IL_008e;
|
|
}
|
|
}
|
|
flag = false;
|
|
goto IL_008e;
|
|
IL_008e:
|
|
float num3 = (flag ? 20f : 2.9f);
|
|
bool flag2 = num2 - num >= num3;
|
|
bool flag3 = IsInLineOfSight(gameObject);
|
|
if (flag2 || !flag3)
|
|
{
|
|
bool flag4 = num2 - num > 5f;
|
|
if (!flag2 && !flag3)
|
|
{
|
|
num3 = Math.Min(num3, num2) / 2f;
|
|
flag4 = true;
|
|
}
|
|
if (!flag4)
|
|
{
|
|
_logger.LogDebug("Moving to {TargetName} ({BaseId}) to attack", gameObject.Name, gameObject.BaseId);
|
|
MovementController movementController = _movementController;
|
|
int num4 = 1;
|
|
List<Vector3> list = new List<Vector3>(num4);
|
|
CollectionsMarshal.SetCount(list, num4);
|
|
CollectionsMarshal.AsSpan(list)[0] = gameObject.Position;
|
|
movementController.NavigateTo(EMovementType.Combat, null, list, fly: false, sprint: false, num3 + num - 0.25f, float.MaxValue);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("Moving to {TargetName} ({BaseId}) to attack (with navmesh)", gameObject.Name, gameObject.BaseId);
|
|
_movementController.NavigateTo(EMovementType.Combat, null, gameObject.Position, fly: false, sprint: false, num3 + num - 0.25f, float.MaxValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal unsafe bool IsInLineOfSight(IGameObject target)
|
|
{
|
|
IPlayerCharacter localPlayer = _objectTable.LocalPlayer;
|
|
if (localPlayer == null)
|
|
{
|
|
return false;
|
|
}
|
|
Vector3 position = localPlayer.Position;
|
|
position.Y += 2f;
|
|
Vector3 position2 = target.Position;
|
|
position2.Y += 2f;
|
|
Vector3 vector = Vector3.Normalize(position2 - position);
|
|
float maxDistance = Vector3.Distance(position, position2);
|
|
int* flags = stackalloc int[4] { 16384, 0, 16384, 0 };
|
|
RaycastHit raycastHit = default(RaycastHit);
|
|
return !Framework.Instance()->BGCollisionModule->RaycastMaterialFilter(&raycastHit, &position, &vector, maxDistance, 1, flags);
|
|
}
|
|
|
|
private void OnErrorToast(ref SeString message, ref bool isHandled)
|
|
{
|
|
if (_cannotSeeTargetText == null || _currentFight == null || !message.TextValue.Contains(_cannotSeeTargetText, StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
IGameObject target = _targetManager.Target;
|
|
if (target == null)
|
|
{
|
|
return;
|
|
}
|
|
long tickCount = Environment.TickCount64;
|
|
if (_lastLoSFailTargetId == target.GameObjectId && tickCount - _lastLoSFailTimeMs < 2000)
|
|
{
|
|
_losFailCount++;
|
|
if (_losFailCount >= 2)
|
|
{
|
|
_losIgnoreList[target.GameObjectId] = tickCount + 10000;
|
|
_logger.LogDebug("Target {TargetId:X8} added to LoS ignore list for 10s", target.GameObjectId);
|
|
_losFailCount = 0;
|
|
_targetManager.Target = null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_lastLoSFailTargetId = target.GameObjectId;
|
|
_losFailCount = 1;
|
|
}
|
|
_lastLoSFailTimeMs = tickCount;
|
|
}
|
|
|
|
private void UpdateLastTargetAndQuestVariables(IGameObject? target)
|
|
{
|
|
_lastTargetId = target?.GameObjectId;
|
|
_previousQuestVariables = ((!(_currentFight.Data.ElementId != null)) ? null : _questFunctions.GetQuestProgressInfo(_currentFight.Data.ElementId)?.Variables);
|
|
}
|
|
|
|
public void Stop(string label)
|
|
{
|
|
using (_logger.BeginScope(label))
|
|
{
|
|
if (_currentFight != null)
|
|
{
|
|
_logger.LogDebug("Stopping current fight");
|
|
_currentFight.Module.Stop();
|
|
}
|
|
if (_passiveBossModEnabled)
|
|
{
|
|
_bossModIpc.DisableAi();
|
|
_passiveBossModEnabled = false;
|
|
}
|
|
_losIgnoreList.Clear();
|
|
_losFailCount = 0;
|
|
_currentFight = null;
|
|
_wasInCombat = false;
|
|
}
|
|
}
|
|
|
|
private void TerritoryChanged(uint territoryId)
|
|
{
|
|
Stop("TerritoryChanged");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_toastGui.ErrorToast -= OnErrorToast;
|
|
_clientState.TerritoryChanged -= TerritoryChanged;
|
|
Stop("Dispose");
|
|
}
|
|
}
|