From 7f348207d3019860f1fed47abe2e620a425ff8d4 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 1 Sep 2024 15:14:28 +0200 Subject: [PATCH] Add experimental combat module for Magiteknical Failure (aether current quest) --- .../2639_Magiteknical Failure.json | 6 +- Questionable.Model/Questing/EAction.cs | 2 + Questionable/Controller/CombatController.cs | 3 + .../Controller/CombatModules/ICombatModule.cs | 2 + .../CombatModules/Mount128Module.cs | 51 +++++++++++++++++ .../RotationSolverRebornModule.cs | 2 + Questionable/Controller/CommandHandler.cs | 56 ++++++++++++++----- .../Controller/Steps/Interactions/Combat.cs | 10 ++-- Questionable/Functions/GameFunctions.cs | 18 ++++-- Questionable/QuestionablePlugin.cs | 1 + 10 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 Questionable/Controller/CombatModules/Mount128Module.cs diff --git a/QuestPaths/4.x - Stormblood/Aether Currents/The Fringes/2639_Magiteknical Failure.json b/QuestPaths/4.x - Stormblood/Aether Currents/The Fringes/2639_Magiteknical Failure.json index 7f1657a26..ce26036ab 100644 --- a/QuestPaths/4.x - Stormblood/Aether Currents/The Fringes/2639_Magiteknical Failure.json +++ b/QuestPaths/4.x - Stormblood/Aether Currents/The Fringes/2639_Magiteknical Failure.json @@ -49,8 +49,7 @@ }, "StopDistance": 0.5, "TerritoryId": 612, - "InteractionType": "Instruction", - "Comment": "Manual combat", + "InteractionType": "Combat", "EnemySpawnType": "AutoOnEnterArea", "KillEnemyDataIds": [ 7504 @@ -69,8 +68,7 @@ }, "StopDistance": 2, "TerritoryId": 612, - "InteractionType": "Instruction", - "Comment": "Manual combat", + "InteractionType": "Combat", "EnemySpawnType": "AutoOnEnterArea", "KillEnemyDataIds": [ 7505 diff --git a/Questionable.Model/Questing/EAction.cs b/Questionable.Model/Questing/EAction.cs index 5ca20daf6..89605e877 100644 --- a/Questionable.Model/Questing/EAction.cs +++ b/Questionable.Model/Questing/EAction.cs @@ -15,6 +15,8 @@ public enum EAction BuffetSanuwa = 4931, BuffetGriffin = 4583, Fumigate = 5872, + MagitekPulse = 8624, + MagitekThunder = 8625, SiphonSnout = 18187, Cannonfire = 20121, RedGulal = 29382, diff --git a/Questionable/Controller/CombatController.cs b/Questionable/Controller/CombatController.cs index 2a7c08d94..d1ff66bea 100644 --- a/Questionable/Controller/CombatController.cs +++ b/Questionable/Controller/CombatController.cs @@ -191,6 +191,9 @@ internal sealed class CombatController : IDisposable { 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 if (_currentFight == null || _currentFight.Data.SpawnType != EEnemySpawnType.OverworldEnemies || diff --git a/Questionable/Controller/CombatModules/ICombatModule.cs b/Questionable/Controller/CombatModules/ICombatModule.cs index 1aec57cc8..542e2d6f9 100644 --- a/Questionable/Controller/CombatModules/ICombatModule.cs +++ b/Questionable/Controller/CombatModules/ICombatModule.cs @@ -13,4 +13,6 @@ internal interface ICombatModule void Update(IGameObject nextTarget); void MoveToTarget(IGameObject nextTarget); + + bool CanAttack(IBattleNpc target); } diff --git a/Questionable/Controller/CombatModules/Mount128Module.cs b/Questionable/Controller/CombatModules/Mount128Module.cs new file mode 100644 index 000000000..39ef13cc3 --- /dev/null +++ b/Questionable/Controller/CombatModules/Mount128Module.cs @@ -0,0 +1,51 @@ +using System; +using System.Numerics; +using Dalamud.Game.ClientState.Objects.Types; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Controller.CombatModules; + +/// +/// Commandeered Magitek Armor; used in 'Magiteknical Failure' quest. +/// +internal sealed class Mount128Module : ICombatModule +{ + public const ushort MountId = 128; + private readonly EAction[] _actions = [EAction.MagitekThunder, EAction.MagitekPulse]; + + private readonly MovementController _movementController; + private readonly GameFunctions _gameFunctions; + + + public Mount128Module(MovementController movementController, GameFunctions gameFunctions) + { + _movementController = movementController; + _gameFunctions = gameFunctions; + } + + public bool IsLoaded => _gameFunctions.GetMountId() == MountId; + + public bool Start() => true; + + public bool Stop() => true; + + public void Update(IGameObject gameObject) + { + if (_movementController.IsPathfinding || _movementController.IsPathRunning) + return; + + foreach (EAction action in _actions) + { + if (_gameFunctions.UseAction(gameObject, action, checkCanUse: false)) + return; + } + } + + public void MoveToTarget(IGameObject gameObject) + { + } + + public bool CanAttack(IBattleNpc target) => target.DataId is 7504 or 7505; +} diff --git a/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs b/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs index 8d0484d6a..72e13ae70 100644 --- a/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs +++ b/Questionable/Controller/CombatModules/RotationSolverRebornModule.cs @@ -119,6 +119,8 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable } } + public bool CanAttack(IBattleNpc target) => true; + public void Dispose() => Stop(); [PublicAPI] diff --git a/Questionable/Controller/CommandHandler.cs b/Questionable/Controller/CommandHandler.cs index dbf8ade5e..776f615af 100644 --- a/Questionable/Controller/CommandHandler.cs +++ b/Questionable/Controller/CommandHandler.cs @@ -3,16 +3,20 @@ using System.Linq; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Plugin.Services; +using Lumina.Excel.GeneratedSheets; using Questionable.Functions; -using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows; using Questionable.Windows.QuestComponents; +using Quest = Questionable.Model.Quest; namespace Questionable.Controller; internal sealed class CommandHandler : IDisposable { + private const string MessageTag = "Questionable"; + private const ushort TagColor = 576; + private readonly ICommandManager _commandManager; private readonly IChatGui _chatGui; private readonly QuestController _questController; @@ -24,6 +28,8 @@ internal sealed class CommandHandler : IDisposable private readonly QuestSelectionWindow _questSelectionWindow; private readonly ITargetManager _targetManager; private readonly QuestFunctions _questFunctions; + private readonly GameFunctions _gameFunctions; + private readonly IDataManager _dataManager; public CommandHandler( ICommandManager commandManager, @@ -36,7 +42,9 @@ internal sealed class CommandHandler : IDisposable QuestWindow questWindow, QuestSelectionWindow questSelectionWindow, ITargetManager targetManager, - QuestFunctions questFunctions) + QuestFunctions questFunctions, + GameFunctions gameFunctions, + IDataManager dataManager) { _commandManager = commandManager; _chatGui = chatGui; @@ -49,6 +57,8 @@ internal sealed class CommandHandler : IDisposable _questSelectionWindow = questSelectionWindow; _targetManager = targetManager; _questFunctions = questFunctions; + _gameFunctions = gameFunctions; + _dataManager = dataManager; _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) { @@ -108,12 +118,16 @@ internal sealed class CommandHandler : IDisposable _questSelectionWindow.OpenForCurrentZone(); break; + case "mountid": + PrintMountId(); + break; + case "": _questWindow.Toggle(); break; default: - _chatGui.PrintError($"Unknown subcommand {parts[0]}", "Questionable"); + _chatGui.PrintError($"Unknown subcommand {parts[0]}", MessageTag, TagColor); break; } } @@ -122,7 +136,7 @@ internal sealed class CommandHandler : IDisposable { if (!_debugOverlay.DrawConditions()) { - _chatGui.PrintError("[Questionable] You don't have the debug overlay enabled."); + _chatGui.PrintError("You don't have the debug overlay enabled.", MessageTag, TagColor); return; } @@ -131,15 +145,15 @@ internal sealed class CommandHandler : IDisposable if (_questRegistry.TryGetQuest(questId, out Quest? quest)) { _debugOverlay.HighlightedQuest = quest.Id; - _chatGui.Print($"[Questionable] Set highlighted quest to {questId} ({quest.Info.Name})."); + _chatGui.Print($"Set highlighted quest to {questId} ({quest.Info.Name}).", MessageTag, TagColor); } else - _chatGui.PrintError($"[Questionable] Unknown quest {questId}."); + _chatGui.PrintError($"Unknown quest {questId}.", MessageTag, TagColor); } else { _debugOverlay.HighlightedQuest = null; - _chatGui.Print("[Questionable] Cleared highlighted quest."); + _chatGui.Print("Cleared highlighted quest.", MessageTag, TagColor); } } @@ -148,21 +162,21 @@ internal sealed class CommandHandler : IDisposable if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId? questId) && questId != null) { if (_questFunctions.IsQuestLocked(questId)) - _chatGui.PrintError($"[Questionable] Quest {questId} is locked."); + _chatGui.PrintError($"Quest {questId} is locked.", MessageTag, TagColor); else if (_questRegistry.TryGetQuest(questId, out Quest? quest)) { _questController.SetNextQuest(quest); - _chatGui.Print($"[Questionable] Set next quest to {questId} ({quest.Info.Name})."); + _chatGui.Print($"Set next quest to {questId} ({quest.Info.Name}).", MessageTag, TagColor); } else { - _chatGui.PrintError($"[Questionable] Unknown quest {questId}."); + _chatGui.PrintError($"Unknown quest {questId}.", MessageTag, TagColor); } } else { _questController.SetNextQuest(null); - _chatGui.Print("[Questionable] Cleared next quest."); + _chatGui.Print("Cleared next quest.", MessageTag, TagColor); } } @@ -173,18 +187,32 @@ internal sealed class CommandHandler : IDisposable if (_questRegistry.TryGetQuest(questId, out Quest? quest)) { _questController.SimulateQuest(quest); - _chatGui.Print($"[Questionable] Simulating quest {questId} ({quest.Info.Name})."); + _chatGui.Print($"Simulating quest {questId} ({quest.Info.Name}).", MessageTag, TagColor); } else - _chatGui.PrintError($"[Questionable] Unknown quest {questId}."); + _chatGui.PrintError($"Unknown quest {questId}.", MessageTag, TagColor); } else { _questController.SimulateQuest(null); - _chatGui.Print("[Questionable] Cleared simulated quest."); + _chatGui.Print("Cleared simulated quest.", MessageTag, TagColor); } } + private void PrintMountId() + { + ushort? mountId = _gameFunctions.GetMountId(); + if (mountId != null) + { + var row = _dataManager.GetExcelSheet()!.GetRow(mountId.Value); + _chatGui.Print( + $"Mount ID: {mountId}, Name: {row?.Singular}, Obtainable: {(row?.Order == -1 ? "No" : "Yes")}", + MessageTag, TagColor); + } + else + _chatGui.Print("You are not mounted.", MessageTag, TagColor); + } + public void Dispose() { _commandManager.RemoveHandler("/qst"); diff --git a/Questionable/Controller/Steps/Interactions/Combat.cs b/Questionable/Controller/Steps/Interactions/Combat.cs index 6657da338..eab500dde 100644 --- a/Questionable/Controller/Steps/Interactions/Combat.cs +++ b/Questionable/Controller/Steps/Interactions/Combat.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.DependencyInjection; +using Questionable.Controller.CombatModules; using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; @@ -19,7 +19,8 @@ internal static class Combat Mount.Factory mountFactory, UseItem.Factory useItemFactory, Action.Factory actionFactory, - QuestFunctions questFunctions) : ITaskFactory + QuestFunctions questFunctions, + GameFunctions gameFunctions) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { @@ -28,7 +29,8 @@ internal static class Combat ArgumentNullException.ThrowIfNull(step.EnemySpawnType); - yield return mountFactory.Unmount(); + if (gameFunctions.GetMountId() != Mount128Module.MountId) + yield return mountFactory.Unmount(); if (step.CombatDelaySecondsAtStart != null) { @@ -70,7 +72,7 @@ internal static class Combat yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1)); yield return CreateTask(quest, sequence, step); break; - } ; + } case EEnemySpawnType.AutoOnEnterArea: if (step.CombatDelaySecondsAtStart == null) diff --git a/Questionable/Functions/GameFunctions.cs b/Questionable/Functions/GameFunctions.cs index 517957055..90703500f 100644 --- a/Questionable/Functions/GameFunctions.cs +++ b/Questionable/Functions/GameFunctions.cs @@ -81,8 +81,9 @@ internal sealed unsafe class GameFunctions if (_questFunctions.IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted]) { - BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0); - if (battleChara != null && battleChara->Mount.MountId == 198) // special quest amaro, not the normal one + // special quest amaro, not the normal one + // TODO Check if this also applies to beast tribe mounts + if (GetMountId() == 198) return true; } @@ -92,6 +93,15 @@ internal sealed unsafe class GameFunctions playerState->IsAetherCurrentZoneComplete(aetherCurrentCompFlgSet); } + public ushort? GetMountId() + { + BattleChara* battleChara = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0); + if (battleChara != null && battleChara->Mount.MountId != 0) + return battleChara->Mount.MountId; + else + return null; + } + public bool IsFlyingUnlockedInCurrentZone() => IsFlyingUnlocked(_clientState.TerritoryType); public bool IsAetherCurrentUnlocked(uint aetherCurrentId) @@ -210,10 +220,10 @@ internal sealed unsafe class GameFunctions return false; } - public bool UseAction(IGameObject gameObject, EAction action) + public bool UseAction(IGameObject gameObject, EAction action, bool checkCanUse = true) { var actionRow = _dataManager.GetExcelSheet()!.GetRow((uint)action)!; - if (!ActionManager.CanUseActionOnTarget((uint)action, (GameObject*)gameObject.Address)) + if (checkCanUse && !ActionManager.CanUseActionOnTarget((uint)action, (GameObject*)gameObject.Address)) { _logger.LogWarning("Can not use action {Action} on target {Target}", action, gameObject); return false; diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index d1a5b543f..88ebc1b36 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -185,6 +185,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); }