diff --git a/QuestPaths/6.x - Endwalker/MSQ/B-Garlemald/4393_Strange Bedfellows.json b/QuestPaths/6.x - Endwalker/MSQ/B-Garlemald/4393_Strange Bedfellows.json index 1ed061f7..36790c21 100644 --- a/QuestPaths/6.x - Endwalker/MSQ/B-Garlemald/4393_Strange Bedfellows.json +++ b/QuestPaths/6.x - Endwalker/MSQ/B-Garlemald/4393_Strange Bedfellows.json @@ -188,14 +188,15 @@ "Z": 94.77368 }, "TerritoryId": 958, - "InteractionType": "Instruction", + "InteractionType": "Combat", "EnemySpawnType": "AfterInteraction", "KillEnemyDataIds": [ 14079 ], - "Comment": "TODO Needs item use?", - "ItemId": 2003231, - "ItemUseHealthMaxPercent": 10, + "CombatItemUse": { + "ItemId": 2003231, + "Condition": "Incapacitated" + }, "CompletionQuestVariablesFlags": [ null, null, @@ -279,14 +280,15 @@ "Z": 396.96338 }, "TerritoryId": 958, - "InteractionType": "Instruction", + "InteractionType": "Combat", "EnemySpawnType": "AfterInteraction", "KillEnemyDataIds": [ 14080 ], - "Comment": "TODO Needs item use?", - "ItemId": 2003231, - "ItemUseHealthMaxPercent": 10, + "CombatItemUse": { + "ItemId": 2003231, + "Condition": "Incapacitated" + }, "DisableNavmesh": true, "CompletionQuestVariablesFlags": [ null, diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index aa74871e..50d39d05 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -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": { "type": "number" } diff --git a/Questionable.Model/Questing/CombatItemUse.cs b/Questionable.Model/Questing/CombatItemUse.cs new file mode 100644 index 00000000..bbcfd100 --- /dev/null +++ b/Questionable.Model/Questing/CombatItemUse.cs @@ -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; } +} diff --git a/Questionable.Model/Questing/Converter/CombatItemUseConditionConverter.cs b/Questionable.Model/Questing/Converter/CombatItemUseConditionConverter.cs new file mode 100644 index 00000000..d2bca66a --- /dev/null +++ b/Questionable.Model/Questing/Converter/CombatItemUseConditionConverter.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Questionable.Model.Common.Converter; + +namespace Questionable.Model.Questing.Converter; + +public sealed class CombatItemUseConditionConverter() : EnumConverter(Values) +{ + private static readonly Dictionary Values = new() + { + { ECombatItemUseCondition.Incapacitated, "Incapacitated" }, + }; +} diff --git a/Questionable.Model/Questing/ECombatItemUseCondition.cs b/Questionable.Model/Questing/ECombatItemUseCondition.cs new file mode 100644 index 00000000..dab191e6 --- /dev/null +++ b/Questionable.Model/Questing/ECombatItemUseCondition.cs @@ -0,0 +1,7 @@ +namespace Questionable.Model.Questing; + +public enum ECombatItemUseCondition +{ + None, + Incapacitated, +} diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index c5626a5d..dcb8042a 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -67,6 +67,7 @@ public sealed class QuestStep public EEnemySpawnType? EnemySpawnType { get; set; } public List KillEnemyDataIds { get; set; } = []; public List ComplexCombatData { get; set; } = []; + public CombatItemUse? CombatItemUse { get; set; } public float? CombatDelaySecondsAtStart { get; set; } public JumpDestination? JumpDestination { get; set; } diff --git a/Questionable/Controller/CombatController.cs b/Questionable/Controller/CombatController.cs index d4b8ae4e..9cdf3bcf 100644 --- a/Questionable/Controller/CombatController.cs +++ b/Questionable/Controller/CombatController.cs @@ -65,11 +65,11 @@ internal sealed class CombatController : IDisposable { Stop("Starting combat"); - var combatModule = _combatModules.FirstOrDefault(x => x.IsLoaded); + var combatModule = _combatModules.FirstOrDefault(x => x.CanHandleFight(combatData)); if (combatModule == null) return false; - if (combatModule.Start()) + if (combatModule.Start(combatData)) { _currentFight = new CurrentFight { @@ -364,6 +364,7 @@ internal sealed class CombatController : IDisposable public required EEnemySpawnType SpawnType { get; init; } public required List KillEnemyDataIds { get; init; } public required List ComplexCombatDatas { get; init; } + public required CombatItemUse? CombatItemUse { get; init; } public HashSet CompletedComplexDatas { get; } = new(); } diff --git a/Questionable/Controller/CombatModules/ICombatModule.cs b/Questionable/Controller/CombatModules/ICombatModule.cs index 542e2d6f..06fe4ae6 100644 --- a/Questionable/Controller/CombatModules/ICombatModule.cs +++ b/Questionable/Controller/CombatModules/ICombatModule.cs @@ -4,9 +4,9 @@ namespace Questionable.Controller.CombatModules; internal interface ICombatModule { - bool IsLoaded { get; } + bool CanHandleFight(CombatController.CombatData combatData); - bool Start(); + bool Start(CombatController.CombatData combatData); bool Stop(); diff --git a/Questionable/Controller/CombatModules/ItemUseModule.cs b/Questionable/Controller/CombatModules/ItemUseModule.cs new file mode 100644 index 00000000..cff43161 --- /dev/null +++ b/Questionable/Controller/CombatModules/ItemUseModule.cs @@ -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 _logger; + + private ICombatModule? _delegate; + private CombatController.CombatData? _combatData; + private bool _isDoingRotation; + + public ItemUseModule(IServiceProvider serviceProvider, GameFunctions gameFunctions, ICondition condition, + ILogger logger) + { + _serviceProvider = serviceProvider; + _gameFunctions = gameFunctions; + _condition = condition; + _logger = logger; + } + + public bool CanHandleFight(CombatController.CombatData combatData) + { + if (combatData.CombatItemUse == null) + return false; + + _delegate = _serviceProvider.GetRequiredService>() + .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); +} diff --git a/Questionable/Controller/CombatModules/Mount128Module.cs b/Questionable/Controller/CombatModules/Mount128Module.cs index c2f1a6f8..e665163a 100644 --- a/Questionable/Controller/CombatModules/Mount128Module.cs +++ b/Questionable/Controller/CombatModules/Mount128Module.cs @@ -25,9 +25,9 @@ internal sealed class Mount128Module : ICombatModule _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; diff --git a/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs b/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs index 72e13ae7..a0b43b2f 100644 --- a/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs +++ b/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs @@ -32,23 +32,20 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable pluginInterface.GetIpcSubscriber("RotationSolverReborn.ChangeOperatingMode"); } - public bool IsLoaded + public bool CanHandleFight(CombatController.CombatData combatData) { - get + try { - try - { - _test.InvokeAction("Validate RSR is callable from Questionable"); - return true; - } - catch (IpcError) - { - return false; - } + _test.InvokeAction("Validate RSR is callable from Questionable"); + return true; + } + catch (IpcError) + { + return false; } } - public bool Start() + public bool Start(CombatController.CombatData combatData) { try { diff --git a/Questionable/Controller/MiniTaskController.cs b/Questionable/Controller/MiniTaskController.cs index 4cf0828e..dfa0bfc9 100644 --- a/Questionable/Controller/MiniTaskController.cs +++ b/Questionable/Controller/MiniTaskController.cs @@ -169,7 +169,7 @@ internal abstract class MiniTaskController if (_condition[ConditionFlag.Mounted]) 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()); _taskQueue.InterruptWith(tasks); } diff --git a/Questionable/Controller/Steps/Interactions/Combat.cs b/Questionable/Controller/Steps/Interactions/Combat.cs index 12a41d60..c670e3bc 100644 --- a/Questionable/Controller/Steps/Interactions/Combat.cs +++ b/Questionable/Controller/Steps/Interactions/Combat.cs @@ -97,12 +97,12 @@ internal static class Combat bool isLastStep = sequence.Steps.Last() == step; 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, IList killEnemyDataIds, IList completionQuestVariablesFlags, - IList complexCombatData) + IList complexCombatData, CombatItemUse? combatItemUse) { return new Task(new CombatController.CombatData { @@ -110,6 +110,7 @@ internal static class Combat SpawnType = enemySpawnType, KillEnemyDataIds = killEnemyDataIds.ToList(), ComplexCombatDatas = complexCombatData.ToList(), + CombatItemUse = combatItemUse, }, completionQuestVariablesFlags, isLastStep); } } diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index b9a1c54e..bdb639f4 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -234,6 +234,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); }