Experimentally handle item use in 'Strange Bedfellows'

This commit is contained in:
Liza 2024-10-12 22:11:54 +02:00
parent 4f5721e67b
commit 7a3bab3d51
Signed by: liza
GPG Key ID: 7199F8D727D55F67
14 changed files with 224 additions and 29 deletions

View File

@ -188,14 +188,15 @@
"Z": 94.77368 "Z": 94.77368
}, },
"TerritoryId": 958, "TerritoryId": 958,
"InteractionType": "Instruction", "InteractionType": "Combat",
"EnemySpawnType": "AfterInteraction", "EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [ "KillEnemyDataIds": [
14079 14079
], ],
"Comment": "TODO Needs item use?", "CombatItemUse": {
"ItemId": 2003231, "ItemId": 2003231,
"ItemUseHealthMaxPercent": 10, "Condition": "Incapacitated"
},
"CompletionQuestVariablesFlags": [ "CompletionQuestVariablesFlags": [
null, null,
null, null,
@ -279,14 +280,15 @@
"Z": 396.96338 "Z": 396.96338
}, },
"TerritoryId": 958, "TerritoryId": 958,
"InteractionType": "Instruction", "InteractionType": "Combat",
"EnemySpawnType": "AfterInteraction", "EnemySpawnType": "AfterInteraction",
"KillEnemyDataIds": [ "KillEnemyDataIds": [
14080 14080
], ],
"Comment": "TODO Needs item use?", "CombatItemUse": {
"ItemId": 2003231, "ItemId": 2003231,
"ItemUseHealthMaxPercent": 10, "Condition": "Incapacitated"
},
"DisableNavmesh": true, "DisableNavmesh": true,
"CompletionQuestVariablesFlags": [ "CompletionQuestVariablesFlags": [
null, null,

View File

@ -617,6 +617,25 @@
] ]
} }
}, },
"CombatItemUse": {
"description": "Unlike the 'AfterItemUse' condition that is used for spawning an enemy in the first place, interacting with an item at a certain stage of combat is required",
"type": "object",
"properties": {
"ItemId": {
"type": "integer"
},
"Condition": {
"type": "string",
"enum": [
"Incapacitated"
]
}
},
"required": [
"ItemId",
"Condition"
]
},
"CombatDelaySecondsAtStart": { "CombatDelaySecondsAtStart": {
"type": "number" "type": "number"
} }

View File

@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
public sealed class CombatItemUse
{
public uint ItemId { get; set; }
[JsonConverter(typeof(CombatItemUseConditionConverter))]
public ECombatItemUseCondition Condition { get; set; }
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class CombatItemUseConditionConverter() : EnumConverter<ECombatItemUseCondition>(Values)
{
private static readonly Dictionary<ECombatItemUseCondition, string> Values = new()
{
{ ECombatItemUseCondition.Incapacitated, "Incapacitated" },
};
}

View File

@ -0,0 +1,7 @@
namespace Questionable.Model.Questing;
public enum ECombatItemUseCondition
{
None,
Incapacitated,
}

View File

@ -67,6 +67,7 @@ public sealed class QuestStep
public EEnemySpawnType? EnemySpawnType { get; set; } public EEnemySpawnType? EnemySpawnType { get; set; }
public List<uint> KillEnemyDataIds { get; set; } = []; public List<uint> KillEnemyDataIds { get; set; } = [];
public List<ComplexCombatData> ComplexCombatData { get; set; } = []; public List<ComplexCombatData> ComplexCombatData { get; set; } = [];
public CombatItemUse? CombatItemUse { get; set; }
public float? CombatDelaySecondsAtStart { get; set; } public float? CombatDelaySecondsAtStart { get; set; }
public JumpDestination? JumpDestination { get; set; } public JumpDestination? JumpDestination { get; set; }

View File

@ -65,11 +65,11 @@ internal sealed class CombatController : IDisposable
{ {
Stop("Starting combat"); Stop("Starting combat");
var combatModule = _combatModules.FirstOrDefault(x => x.IsLoaded); var combatModule = _combatModules.FirstOrDefault(x => x.CanHandleFight(combatData));
if (combatModule == null) if (combatModule == null)
return false; return false;
if (combatModule.Start()) if (combatModule.Start(combatData))
{ {
_currentFight = new CurrentFight _currentFight = new CurrentFight
{ {
@ -364,6 +364,7 @@ internal sealed class CombatController : IDisposable
public required EEnemySpawnType SpawnType { get; init; } public required EEnemySpawnType SpawnType { get; init; }
public required List<uint> KillEnemyDataIds { get; init; } public required List<uint> KillEnemyDataIds { get; init; }
public required List<ComplexCombatData> ComplexCombatDatas { get; init; } public required List<ComplexCombatData> ComplexCombatDatas { get; init; }
public required CombatItemUse? CombatItemUse { get; init; }
public HashSet<int> CompletedComplexDatas { get; } = new(); public HashSet<int> CompletedComplexDatas { get; } = new();
} }

View File

@ -4,9 +4,9 @@ namespace Questionable.Controller.CombatModules;
internal interface ICombatModule internal interface ICombatModule
{ {
bool IsLoaded { get; } bool CanHandleFight(CombatController.CombatData combatData);
bool Start(); bool Start(CombatController.CombatData combatData);
bool Stop(); bool Stop();

View File

@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model.Questing;
namespace Questionable.Controller.CombatModules;
internal sealed class ItemUseModule : ICombatModule
{
private readonly IServiceProvider _serviceProvider;
private readonly GameFunctions _gameFunctions;
private readonly ICondition _condition;
private readonly ILogger<ItemUseModule> _logger;
private ICombatModule? _delegate;
private CombatController.CombatData? _combatData;
private bool _isDoingRotation;
public ItemUseModule(IServiceProvider serviceProvider, GameFunctions gameFunctions, ICondition condition,
ILogger<ItemUseModule> logger)
{
_serviceProvider = serviceProvider;
_gameFunctions = gameFunctions;
_condition = condition;
_logger = logger;
}
public bool CanHandleFight(CombatController.CombatData combatData)
{
if (combatData.CombatItemUse == null)
return false;
_delegate = _serviceProvider.GetRequiredService<IEnumerable<ICombatModule>>()
.Where(x => x is not ItemUseModule)
.FirstOrDefault(x => x.CanHandleFight(combatData));
_logger.LogInformation("ItemUse delegate: {Delegate}", _delegate?.GetType().Name);
return _delegate != null;
}
public bool Start(CombatController.CombatData combatData)
{
if (_delegate!.Start(combatData))
{
_combatData = combatData;
_isDoingRotation = true;
return true;
}
return false;
}
public bool Stop()
{
if (_isDoingRotation)
{
_delegate!.Stop();
_isDoingRotation = false;
_combatData = null;
_delegate = null;
}
return true;
}
public void Update(IGameObject nextTarget)
{
if (_delegate == null)
return;
if (_combatData?.CombatItemUse == null)
{
_delegate.Update(nextTarget);
return;
}
if (_combatData.KillEnemyDataIds.Contains(nextTarget.DataId) ||
_combatData.ComplexCombatDatas.Any(x => x.DataId == nextTarget.DataId))
{
if (_isDoingRotation)
{
unsafe
{
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager->GetInventoryItemCount(_combatData.CombatItemUse.ItemId) == 0)
{
_isDoingRotation = false;
_delegate.Stop();
}
}
if (ShouldUseItem(nextTarget))
{
_isDoingRotation = false;
_delegate.Stop();
_gameFunctions.UseItem(nextTarget.DataId, _combatData.CombatItemUse.ItemId);
}
else
_delegate.Update(nextTarget);
}
else if (_condition[ConditionFlag.Casting])
{
// do nothing
}
else
{
_isDoingRotation = true;
_delegate.Start(_combatData);
}
}
else if (_isDoingRotation)
{
_delegate.Update(nextTarget);
}
}
private unsafe bool ShouldUseItem(IGameObject gameObject)
{
if (_combatData?.CombatItemUse == null)
return false;
if (gameObject is IBattleChara)
{
BattleChara* battleChara = (BattleChara*)gameObject.Address;
if (_combatData.CombatItemUse.Condition == ECombatItemUseCondition.Incapacitated)
return (battleChara->Flags2 & 128u) != 0;
}
return false;
}
public void MoveToTarget(IGameObject nextTarget) => _delegate!.MoveToTarget(nextTarget);
public bool CanAttack(IBattleNpc target) => _delegate!.CanAttack(target);
}

View File

@ -25,9 +25,9 @@ internal sealed class Mount128Module : ICombatModule
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
} }
public bool IsLoaded => _gameFunctions.GetMountId() == MountId; public bool CanHandleFight(CombatController.CombatData combatData) => _gameFunctions.GetMountId() == MountId;
public bool Start() => true; public bool Start(CombatController.CombatData combatData) => true;
public bool Stop() => true; public bool Stop() => true;

View File

@ -32,9 +32,7 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
pluginInterface.GetIpcSubscriber<StateCommandType, object>("RotationSolverReborn.ChangeOperatingMode"); pluginInterface.GetIpcSubscriber<StateCommandType, object>("RotationSolverReborn.ChangeOperatingMode");
} }
public bool IsLoaded public bool CanHandleFight(CombatController.CombatData combatData)
{
get
{ {
try try
{ {
@ -46,9 +44,8 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
return false; return false;
} }
} }
}
public bool Start() public bool Start(CombatController.CombatData combatData)
{ {
try try
{ {

View File

@ -169,7 +169,7 @@ internal abstract class MiniTaskController<T>
if (_condition[ConditionFlag.Mounted]) if (_condition[ConditionFlag.Mounted])
tasks.Add(new Mount.UnmountTask()); tasks.Add(new Mount.UnmountTask());
tasks.Add(Combat.Factory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], [])); tasks.Add(Combat.Factory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], [], null));
tasks.Add(new WaitAtEnd.WaitDelay()); tasks.Add(new WaitAtEnd.WaitDelay());
_taskQueue.InterruptWith(tasks); _taskQueue.InterruptWith(tasks);
} }

View File

@ -97,12 +97,12 @@ internal static class Combat
bool isLastStep = sequence.Steps.Last() == step; bool isLastStep = sequence.Steps.Last() == step;
return CreateTask(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds, return CreateTask(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
step.CompletionQuestVariablesFlags, step.ComplexCombatData); step.CompletionQuestVariablesFlags, step.ComplexCombatData, step.CombatItemUse);
} }
internal static Task CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType, internal static Task CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType,
IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags, IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags,
IList<ComplexCombatData> complexCombatData) IList<ComplexCombatData> complexCombatData, CombatItemUse? combatItemUse)
{ {
return new Task(new CombatController.CombatData return new Task(new CombatController.CombatData
{ {
@ -110,6 +110,7 @@ internal static class Combat
SpawnType = enemySpawnType, SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(), KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(), ComplexCombatDatas = complexCombatData.ToList(),
CombatItemUse = combatItemUse,
}, completionQuestVariablesFlags, isLastStep); }, completionQuestVariablesFlags, isLastStep);
} }
} }

View File

@ -234,6 +234,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<LeveUiController>(); serviceCollection.AddSingleton<LeveUiController>();
serviceCollection.AddSingleton<ICombatModule, Mount128Module>(); serviceCollection.AddSingleton<ICombatModule, Mount128Module>();
serviceCollection.AddSingleton<ICombatModule, ItemUseModule>();
serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>(); serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
} }