Add experimental combat module for Magiteknical Failure (aether current quest)
authorLiza Carvelli <liza@carvel.li>
Sun, 1 Sep 2024 13:14:28 +0000 (15:14 +0200)
committerLiza Carvelli <liza@carvel.li>
Sun, 1 Sep 2024 13:14:37 +0000 (15:14 +0200)
QuestPaths/4.x - Stormblood/Aether Currents/The Fringes/2639_Magiteknical Failure.json
Questionable.Model/Questing/EAction.cs
Questionable/Controller/CombatController.cs
Questionable/Controller/CombatModules/ICombatModule.cs
Questionable/Controller/CombatModules/Mount128Module.cs [new file with mode: 0644]
Questionable/Controller/CombatModules/RotationSolverRebornModule.cs
Questionable/Controller/CommandHandler.cs
Questionable/Controller/Steps/Interactions/Combat.cs
Questionable/Functions/GameFunctions.cs
Questionable/QuestionablePlugin.cs

index 7f1657a26431cfb1f752707b0c4a48f871283b79..ce26036abf7035d4a2e5d2fcfcaabcfad0984371 100644 (file)
@@ -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
index 5ca20daf6ad933e569e39096e3ed1a263f7afee1..89605e8776616f41932dd721dcac8d80715e52a3 100644 (file)
@@ -15,6 +15,8 @@ public enum EAction
     BuffetSanuwa = 4931,
     BuffetGriffin = 4583,
     Fumigate = 5872,
+    MagitekPulse = 8624,
+    MagitekThunder = 8625,
     SiphonSnout = 18187,
     Cannonfire = 20121,
     RedGulal = 29382,
index 2a7c08d94d33360613a0f2567f2780f49bd59d5f..d1ff66bea67cf34a1b92b0ecb98468953eb983bf 100644 (file)
@@ -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 ||
index 1aec57cc8e10619a04a7b7bbb764def32bdd92c0..542e2d6f9461e177a593626b984310b2ffb31289 100644 (file)
@@ -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 (file)
index 0000000..39ef13c
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Commandeered Magitek Armor; used in 'Magiteknical Failure' quest.
+/// </summary>
+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;
+}
index 8d0484d6adeeb8e92632ab5732057d191123f99e..72e13ae70fd78605e0e6824da75d280528471b31 100644 (file)
@@ -119,6 +119,8 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
         }
     }
 
+    public bool CanAttack(IBattleNpc target) => true;
+
     public void Dispose() => Stop();
 
     [PublicAPI]
index dbf8ade5ee5680053175493ceb9f5872068e8b58..776f615af2b7aac5e31632ce2c62bf1ac9fb76b5 100644 (file)
@@ -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<Mount>()!.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");
index 6657da338fbe39de533c4f9fbff5fae6f0f13543..eab500dde30de63a4df4cb37bd347633fc3cbd39 100644 (file)
@@ -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<ITask> 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)
index 517957055b2d22173d317569af813e2ee82ece86..90703500fa15f5f2ba7505fa8b74d9ac5a5680cc 100644 (file)
@@ -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<Action>()!.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;
index d1a5b543f5d06631f7a4942e48e886ac8071d19b..88ebc1b36a9e0b422b41a989f0ff16cca819fb12 100644 (file)
@@ -185,6 +185,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<InteractionUiController>();
         serviceCollection.AddSingleton<LeveUiController>();
 
+        serviceCollection.AddSingleton<ICombatModule, Mount128Module>();
         serviceCollection.AddSingleton<ICombatModule, RotationSolverRebornModule>();
     }