Questionable/Questionable/Controller/CombatController.cs

353 lines
12 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
2024-07-16 08:54:47 +00:00
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
2024-07-27 15:37:08 +00:00
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Common.Math;
using Microsoft.Extensions.Logging;
using Questionable.Controller.CombatModules;
using Questionable.Controller.Utils;
using Questionable.Functions;
2024-08-02 16:30:21 +00:00
using Questionable.Model.Questing;
namespace Questionable.Controller;
internal sealed class CombatController : IDisposable
{
private const float MaxTargetRange = 55f;
private const float MaxNameplateRange = 50f;
private readonly List<ICombatModule> _combatModules;
private readonly MovementController _movementController;
private readonly ITargetManager _targetManager;
private readonly IObjectTable _objectTable;
private readonly ICondition _condition;
private readonly IClientState _clientState;
private readonly QuestFunctions _questFunctions;
private readonly ILogger<CombatController> _logger;
private CurrentFight? _currentFight;
private bool _wasInCombat;
public CombatController(
IEnumerable<ICombatModule> combatModules,
MovementController movementController,
ITargetManager targetManager,
IObjectTable objectTable,
ICondition condition,
IClientState clientState,
QuestFunctions questFunctions,
ILogger<CombatController> logger)
{
_combatModules = combatModules.ToList();
_movementController = movementController;
_targetManager = targetManager;
_objectTable = objectTable;
_condition = condition;
_clientState = clientState;
_questFunctions = questFunctions;
_logger = logger;
_clientState.TerritoryChanged += TerritoryChanged;
}
public bool IsRunning => _currentFight != null;
public bool Start(CombatData combatData)
{
Stop("Starting combat");
var combatModule = _combatModules.FirstOrDefault(x => x.IsLoaded);
if (combatModule == null)
return false;
if (combatModule.Start())
{
_currentFight = new CurrentFight
{
Module = combatModule,
Data = combatData,
};
return true;
}
else
return false;
}
public EStatus Update()
{
if (_currentFight == null)
return EStatus.Complete;
if (_movementController.IsPathfinding || _movementController.IsPathRunning || _movementController.MovementStartedAt > DateTime.Now.AddSeconds(-1))
return EStatus.Moving;
var target = _targetManager.Target;
if (target != null)
{
2024-07-27 09:19:18 +00:00
int currentTargetPriority = GetKillPriority(target);
var nextTarget = FindNextTarget();
2024-07-27 09:19:18 +00:00
int nextTargetPriority = GetKillPriority(target);
if (nextTarget != null && nextTarget.Equals(target))
{
_currentFight.Module.Update(target);
}
else if (nextTarget != null)
{
2024-07-27 09:19:18 +00:00
if (nextTargetPriority > currentTargetPriority)
SetTarget(nextTarget);
}
else
SetTarget(null);
}
else
{
var nextTarget = FindNextTarget();
if (nextTarget is { IsDead: false })
SetTarget(nextTarget);
}
if (_condition[ConditionFlag.InCombat])
{
_wasInCombat = true;
return EStatus.InCombat;
}
else if (_wasInCombat)
return EStatus.Complete;
else
return EStatus.InCombat;
}
2024-07-16 08:54:47 +00:00
[SuppressMessage("ReSharper", "RedundantJumpStatement")]
private IGameObject? FindNextTarget()
{
if (_currentFight == null)
return null;
// check if any complex combat conditions are fulfilled
var complexCombatData = _currentFight.Data.ComplexCombatDatas;
if (complexCombatData.Count > 0)
{
for (int i = 0; i < complexCombatData.Count; ++i)
{
if (_currentFight.Data.CompletedComplexDatas.Contains(i))
continue;
var condition = complexCombatData[i];
if (condition.RewardItemId != null && condition.RewardItemCount != null)
{
unsafe
{
var inventoryManager = InventoryManager.Instance();
if (inventoryManager->GetInventoryItemCount(condition.RewardItemId.Value) >=
condition.RewardItemCount.Value)
{
_logger.LogInformation(
"Complex combat condition fulfilled: itemCount({ItemId}) >= {ItemCount}",
condition.RewardItemId, condition.RewardItemCount);
_currentFight.Data.CompletedComplexDatas.Add(i);
continue;
}
}
}
if (QuestWorkUtils.HasCompletionFlags(condition.CompletionQuestVariablesFlags) &&
_currentFight.Data.ElementId is QuestId questId)
{
2024-08-07 23:49:14 +00:00
var questWork = _questFunctions.GetQuestProgressInfo(questId);
if (questWork != null &&
QuestWorkUtils.MatchesQuestWork(condition.CompletionQuestVariablesFlags, questWork))
{
_logger.LogInformation("Complex combat condition fulfilled: QuestWork matches");
_currentFight.Data.CompletedComplexDatas.Add(i);
continue;
}
}
}
}
return _objectTable.Select(x => new
{
GameObject = x,
Priority = GetKillPriority(x),
Distance = Vector3.Distance(x.Position, _clientState.LocalPlayer!.Position),
})
2024-07-27 09:19:18 +00:00
.Where(x => x.Priority > 0)
.OrderByDescending(x => x.Priority)
.ThenBy(x => x.Distance)
.Select(x => x.GameObject)
.FirstOrDefault();
}
public unsafe int GetKillPriority(IGameObject gameObject)
{
2024-07-19 19:16:40 +00:00
if (gameObject is IBattleNpc battleNpc)
{
if (_currentFight != null && !_currentFight.Module.CanAttack(battleNpc))
return 0;
// TODO this works as somewhat of a delay between killing enemies if certain items/flags are checked
// but also delays killing the next enemy a little
2024-09-11 21:17:39 +00:00
if (_currentFight == null ||
_currentFight.Data.SpawnType == EEnemySpawnType.OverworldEnemies ||
2024-09-12 17:11:32 +00:00
_currentFight.Data.SpawnType == EEnemySpawnType.FateEnemies ||
_currentFight.Data.KillEnemyDataIds.Count > 0)
{
2024-07-19 19:16:40 +00:00
if (battleNpc.IsDead)
2024-07-27 09:19:18 +00:00
return 0;
}
2024-07-19 19:16:40 +00:00
if (!battleNpc.IsTargetable)
2024-07-27 09:19:18 +00:00
return 0;
if (_currentFight != null)
{
var complexCombatData = _currentFight.Data.ComplexCombatDatas;
if (complexCombatData.Count >= 0)
{
for (int i = 0; i < complexCombatData.Count; ++i)
{
if (_currentFight.Data.CompletedComplexDatas.Contains(i))
continue;
2024-07-19 19:16:40 +00:00
if (complexCombatData[i].DataId == battleNpc.DataId)
return 100;
}
}
else
{
2024-07-19 19:16:40 +00:00
if (_currentFight.Data.KillEnemyDataIds.Contains(battleNpc.DataId))
return 90;
}
}
// enemies that we have aggro on
2024-07-19 19:16:40 +00:00
if (battleNpc.BattleNpcKind is BattleNpcSubKind.BattleNpcPart or BattleNpcSubKind.Enemy)
{
var gameObjectStruct = (GameObject*)gameObject.Address;
2024-07-27 15:37:08 +00:00
// npc that starts a fate or does turn-ins; not sure why they're marked as hostile
if (gameObjectStruct->NamePlateIconId is 60093 or 60732)
2024-07-27 09:19:18 +00:00
return 0;
2024-07-27 15:37:08 +00:00
var enemyData =
_currentFight?.Data.ComplexCombatDatas.FirstOrDefault(x => x.DataId == battleNpc.DataId);
if (enemyData is { IgnoreQuestMarker: true })
2024-07-27 15:37:08 +00:00
{
if (battleNpc.StatusFlags.HasFlag(StatusFlags.InCombat))
return 20;
}
else if (enemyData != null)
2024-07-27 15:37:08 +00:00
{
if (gameObjectStruct->NamePlateIconId != 0)
return 30;
// for enemies that are very far away, their nameplate doesn't render but they're in the object table
if (_currentFight?.Data.SpawnType == EEnemySpawnType.OverworldEnemies &&
Vector3.Distance(_clientState.LocalPlayer?.Position ?? Vector3.Zero, battleNpc.Position) > MaxNameplateRange)
return 25;
2024-07-27 15:37:08 +00:00
}
else
{
// as part of KillEnemyDataIds, not ComplexCombatData
// TODO maybe remove KillEnemyDataIds, rename ComplexCombatData to CombatData
if (gameObjectStruct->NamePlateIconId != 0)
return 29;
}
}
// stuff trying to kill us
if (battleNpc.TargetObjectId == _clientState.LocalPlayer?.GameObjectId)
2024-07-27 09:19:18 +00:00
return 10;
2024-07-27 15:37:08 +00:00
// stuff on our enmity list that's not necessarily targeting us
var haters = UIState.Instance()->Hater;
for (int i = 0; i < haters.HaterCount; ++i)
{
var hater = haters.Haters[i];
if (hater.EntityId == battleNpc.GameObjectId)
return 5;
}
2024-07-27 15:37:08 +00:00
return 0;
}
else
return 0;
}
private void SetTarget(IGameObject? target)
{
if (target == null)
{
if (_targetManager.Target != null)
{
_logger.LogInformation("Clearing target");
_targetManager.Target = null;
}
}
else if (Vector3.Distance(_clientState.LocalPlayer!.Position, target.Position) > MaxTargetRange)
{
_logger.LogInformation("Moving to target, distance: {Distance:N2}", Vector3.Distance(_clientState.LocalPlayer!.Position, target.Position));
_currentFight!.Module.MoveToTarget(target);
}
else
{
_logger.LogInformation("Setting target to {TargetName} ({TargetId:X8})", target.Name.ToString(), target.GameObjectId);
_targetManager.Target = target;
_currentFight!.Module.MoveToTarget(target);
}
}
public void Stop(string label)
{
using var scope = _logger.BeginScope(label);
if (_currentFight != null)
{
_logger.LogInformation("Stopping current fight");
_currentFight.Module.Stop();
}
_currentFight = null;
_wasInCombat = false;
}
private void TerritoryChanged(ushort territoryId) => Stop("TerritoryChanged");
public void Dispose()
{
_clientState.TerritoryChanged -= TerritoryChanged;
Stop("Dispose");
}
private sealed class CurrentFight
{
public required ICombatModule Module { get; init; }
public required CombatData Data { get; init; }
}
public sealed class CombatData
{
public required ElementId ElementId { get; init; }
public required EEnemySpawnType SpawnType { get; init; }
public required List<uint> KillEnemyDataIds { get; init; }
public required List<ComplexCombatData> ComplexCombatDatas { get; init; }
public HashSet<int> CompletedComplexDatas { get; } = new();
}
public enum EStatus
{
InCombat,
Moving,
Complete,
}
}