forked from liza/Questionable
237 lines
8.2 KiB
C#
237 lines
8.2 KiB
C#
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
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;
|
|
using Microsoft.Extensions.Logging;
|
|
using Questionable.Controller.CombatModules;
|
|
using Questionable.Controller.Utils;
|
|
using Questionable.Model.V1;
|
|
|
|
namespace Questionable.Controller;
|
|
|
|
internal sealed class CombatController
|
|
{
|
|
private readonly List<ICombatModule> _combatModules;
|
|
private readonly ITargetManager _targetManager;
|
|
private readonly IObjectTable _objectTable;
|
|
private readonly ICondition _condition;
|
|
private readonly IClientState _clientState;
|
|
private readonly GameFunctions _gameFunctions;
|
|
private readonly ILogger<CombatController> _logger;
|
|
|
|
private CurrentFight? _currentFight;
|
|
|
|
public CombatController(IEnumerable<ICombatModule> combatModules, ITargetManager targetManager,
|
|
IObjectTable objectTable, ICondition condition, IClientState clientState, GameFunctions gameFunctions,
|
|
ILogger<CombatController> logger)
|
|
{
|
|
_combatModules = combatModules.ToList();
|
|
_targetManager = targetManager;
|
|
_objectTable = objectTable;
|
|
_condition = condition;
|
|
_clientState = clientState;
|
|
_gameFunctions = gameFunctions;
|
|
_logger = logger;
|
|
}
|
|
|
|
public bool IsRunning => _currentFight != null;
|
|
|
|
public bool Start(CombatData combatData)
|
|
{
|
|
Stop();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <returns>true if still in combat, false otherwise</returns>
|
|
public bool Update()
|
|
{
|
|
if (_currentFight == null)
|
|
return false;
|
|
|
|
var target = _targetManager.Target;
|
|
if (target != null)
|
|
{
|
|
if (IsEnemyToKill(target))
|
|
return true;
|
|
|
|
var nextTarget = FindNextTarget();
|
|
if (nextTarget != null)
|
|
{
|
|
_logger.LogInformation("Changing next target to {TargetName} ({TargetId:X8})",
|
|
nextTarget.Name.ToString(), nextTarget.GameObjectId);
|
|
_targetManager.Target = nextTarget;
|
|
_currentFight.Module.SetTarget(nextTarget);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation("Resetting next target");
|
|
_targetManager.Target = null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var nextTarget = FindNextTarget();
|
|
if (nextTarget != null)
|
|
{
|
|
_logger.LogInformation("Setting next target to {TargetName} ({TargetId:X8})",
|
|
nextTarget.Name.ToString(), nextTarget.GameObjectId);
|
|
_targetManager.Target = nextTarget;
|
|
_currentFight.Module.SetTarget(nextTarget);
|
|
}
|
|
}
|
|
|
|
return _condition[ConditionFlag.InCombat];
|
|
}
|
|
|
|
[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))
|
|
{
|
|
var questWork = _gameFunctions.GetQuestEx(_currentFight.Data.QuestId);
|
|
if (questWork != null && QuestWorkUtils.MatchesQuestWork(condition.CompletionQuestVariablesFlags,
|
|
questWork.Value, false))
|
|
{
|
|
_logger.LogInformation("Complex combat condition fulfilled: QuestWork matches");
|
|
_currentFight.Data.CompletedComplexDatas.Add(i);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return _objectTable.Where(IsEnemyToKill).MinBy(x => (x.Position - _clientState.LocalPlayer!.Position).Length());
|
|
}
|
|
|
|
private unsafe bool IsEnemyToKill(IGameObject gameObject)
|
|
{
|
|
if (gameObject is IBattleChara battleChara)
|
|
{
|
|
// 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
|
|
if (_currentFight == null || _currentFight.Data.SpawnType != EEnemySpawnType.OverworldEnemies ||
|
|
_currentFight.Data.ComplexCombatDatas.Count == 0)
|
|
{
|
|
if (battleChara.IsDead)
|
|
return false;
|
|
}
|
|
|
|
if (!battleChara.IsTargetable)
|
|
return false;
|
|
|
|
if (battleChara.TargetObjectId == _clientState.LocalPlayer?.GameObjectId)
|
|
return true;
|
|
|
|
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;
|
|
|
|
if (complexCombatData[i].DataId == battleChara.DataId)
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_currentFight.Data.KillEnemyDataIds.Contains(battleChara.DataId))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (battleChara.StatusFlags.HasFlag(StatusFlags.Hostile))
|
|
{
|
|
var gameObjectStruct = (GameObject*)gameObject.Address;
|
|
return gameObjectStruct->NamePlateIconId != 0;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
if (_currentFight != null)
|
|
{
|
|
_logger.LogInformation("Stopping current fight");
|
|
_currentFight.Module.Stop();
|
|
}
|
|
|
|
_currentFight = null;
|
|
}
|
|
|
|
private sealed class CurrentFight
|
|
{
|
|
public required ICombatModule Module { get; init; }
|
|
public required CombatData Data { get; init; }
|
|
}
|
|
|
|
public sealed class CombatData
|
|
{
|
|
public required ushort QuestId { 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();
|
|
}
|
|
}
|