Add quest battle difficulty selection; UI tweaks
authorLiza Carvelli <liza@carvel.li>
Fri, 21 Feb 2025 11:21:01 +0000 (12:21 +0100)
committerLiza Carvelli <liza@carvel.li>
Fri, 21 Feb 2025 11:21:01 +0000 (12:21 +0100)
QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json
QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json
Questionable/Configuration.cs
Questionable/Controller/GameUi/InteractionUiController.cs
Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs

index d59a446c92f440e75b4da035ee8c070a14175deb..bb4aa6554a4be23ff214b28c854ccf702dad01ec 100644 (file)
             "Z": -309.55975
           },
           "TerritoryId": 148,
-          "InteractionType": "SinglePlayerDuty"
+          "InteractionType": "SinglePlayerDuty",
+          "BossModEnabled": false,
+          "BossModNotes": [
+            "AI doesn't automatically target newly spawning adds and dies until after the boss died (tested on CNJ)"
+          ]
         }
       ]
     },
index 5cd92e470056a22ba8101be8a66c38b3d746d843..49a098916eecc5e4ca18d09a45d2ea3eeba2432d 100644 (file)
           },
           "TerritoryId": 817,
           "InteractionType": "SinglePlayerDuty",
+          "BossModEnabled": false,
+          "BossModNotes": [
+            "Doesn't walk to the teleporter to finish the duty"
+          ],
           "Fly": true,
           "Comment": "A Sleep Disturbed (Opo-Opo, Wolf, Serpent)",
           "$": "The dialogue choices and data ids here are recycled",
index a4126ed6f91508f84eb9b8dd3ed6cc2766fbd07c..07b20f4d023fc1a6e74e37425b1c1c486b48e0c6 100644 (file)
@@ -45,6 +45,7 @@ internal sealed class Configuration : IPluginConfiguration
     internal sealed class SinglePlayerDutyConfiguration
     {
         public bool RunSoloInstancesWithBossMod { get; set; }
+        public byte RetryDifficulty { get; set; } = 2;
         public HashSet<uint> WhitelistedSinglePlayerDutyCfcIds { get; set; } = [];
         public HashSet<uint> BlacklistedSinglePlayerDutyCfcIds { get; set; } = [];
     }
index 0c7e4d0d2ee5972e5ffc592b95a8ee833b04683a..1825b4f793f69d79a6803efa44096e5d4dff2d4c 100644 (file)
@@ -47,6 +47,7 @@ internal sealed class InteractionUiController : IDisposable
     private readonly IClientState _clientState;
     private readonly ShopController _shopController;
     private readonly BossModIpc _bossModIpc;
+    private readonly Configuration _configuration;
     private readonly ILogger<InteractionUiController> _logger;
     private readonly Regex _returnRegex;
     private readonly Regex _purchaseItemRegex;
@@ -71,6 +72,7 @@ internal sealed class InteractionUiController : IDisposable
         IClientState clientState,
         ShopController shopController,
         BossModIpc bossModIpc,
+        Configuration configuration,
         ILogger<InteractionUiController> logger)
     {
         _addonLifecycle = addonLifecycle;
@@ -89,6 +91,7 @@ internal sealed class InteractionUiController : IDisposable
         _clientState = clientState;
         _shopController = shopController;
         _bossModIpc = bossModIpc;
+        _configuration = configuration;
         _logger = logger;
 
         _returnRegex = _dataManager.GetExcelSheet<Addon>().GetRow(196).GetRegex(addon => addon.Text, pluginLog)!;
@@ -98,6 +101,7 @@ internal sealed class InteractionUiController : IDisposable
         _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
         _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
         _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
+        _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "DifficultySelectYesNo", DifficultySelectYesNoPostSetup);
         _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
         _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
 
@@ -144,6 +148,12 @@ internal sealed class InteractionUiController : IDisposable
                 SelectYesnoPostSetup(addonSelectYesno, true);
             }
 
+            if (_gameGui.TryGetAddonByName("DifficultySelectYesNo", out AtkUnitBase* addonDifficultySelectYesNo))
+            {
+                _logger.LogInformation("DifficultySelectYesNo window is open");
+                DifficultySelectYesNoPostSetup(addonDifficultySelectYesNo, true);
+            }
+
             if (_gameGui.TryGetAddonByName("PointMenu", out AtkUnitBase* addonPointMenu))
             {
                 _logger.LogInformation("PointMenu is open");
@@ -669,8 +679,19 @@ internal sealed class InteractionUiController : IDisposable
             return true;
         }
 
+        if (CheckSinglePlayerDutyYesNo(quest.Id, step))
+        {
+            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+            return true;
+        }
+
+        return false;
+    }
+
+    private bool CheckSinglePlayerDutyYesNo(ElementId questId, QuestStep? step)
+    {
         if (step is { InteractionType: EInteractionType.SinglePlayerDuty } &&
-            _bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled))
+            _bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyIndex, step.BossModEnabled))
         {
             // Most of these are yes/no dialogs "Duty calls, ...".
             //
@@ -678,8 +699,7 @@ internal sealed class InteractionUiController : IDisposable
             // after you confirm 'Wait for Krile?'. However, if you fail that duty, you'll get a DifficultySelectYesNo.
 
             // DifficultySelectYesNo → [0, 2] for very easy
-            _logger.LogInformation("DefaultYesNo: probably Single Player Duty");
-            addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
+            _logger.LogInformation("SinglePlayerDutyYesNo: probably Single Player Duty");
             return true;
         }
 
@@ -716,6 +736,44 @@ internal sealed class InteractionUiController : IDisposable
         return false;
     }
 
+
+    private unsafe void DifficultySelectYesNoPostSetup(AddonEvent type, AddonArgs args)
+    {
+        AtkUnitBase* addonDifficultySelectYesNo = (AtkUnitBase*)args.Addon;
+        DifficultySelectYesNoPostSetup(addonDifficultySelectYesNo, false);
+    }
+
+    private unsafe void DifficultySelectYesNoPostSetup(AtkUnitBase* addonDifficultySelectYesNo, bool checkAllSteps)
+    {
+        var currentQuest = _questController.StartedQuest;
+        if (currentQuest == null)
+            return;
+
+        var quest = currentQuest.Quest;
+        bool autoConfirm;
+        if (checkAllSteps)
+        {
+            var sequence = quest.FindSequence(currentQuest.Sequence);
+            autoConfirm = sequence != null && sequence.Steps.Any(step => CheckSinglePlayerDutyYesNo(quest.Id, step));
+        }
+        else
+        {
+            var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
+            autoConfirm = step != null && CheckSinglePlayerDutyYesNo(quest.Id, step);
+        }
+
+        if (autoConfirm)
+        {
+            _logger.LogInformation("Confirming difficulty ({Difficulty}) for quest battle", _configuration.SinglePlayerDuties.RetryDifficulty);
+            var selectChoice = stackalloc AtkValue[]
+            {
+                new() { Type = ValueType.Int, Int = 0 },
+                new() { Type = ValueType.Int, Int = _configuration.SinglePlayerDuties.RetryDifficulty }
+            };
+            addonDifficultySelectYesNo->FireCallback(2, selectChoice);
+        }
+    }
+
     private ushort? FindTargetTerritoryFromQuestStep(QuestController.QuestProgress currentQuest)
     {
         // this can be triggered either manually (in which case we should increase the step counter), or automatically
@@ -888,6 +946,7 @@ internal sealed class InteractionUiController : IDisposable
     {
         _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
         _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
+        _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "DifficultySelectYesNo", DifficultySelectYesNoPostSetup);
         _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
         _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
         _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
index 263a3b54d1a981c10e1c65d71830987f5a3e5193..07149debaba85cd1362d27de1ae7d39220267089 100644 (file)
@@ -25,12 +25,6 @@ namespace Questionable.Windows.ConfigComponents;
 
 internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
 {
-    private readonly TerritoryData _territoryData;
-    private readonly QuestRegistry _questRegistry;
-    private readonly QuestData _questData;
-    private readonly IDataManager _dataManager;
-    private readonly ILogger<SinglePlayerDutyConfigComponent> _logger;
-
     private static readonly List<(EClassJob ClassJob, string Name)> RoleQuestCategories =
     [
         (EClassJob.Paladin, "Tank Role Quests"),
@@ -40,6 +34,15 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
         (EClassJob.BlackMage, "Magical Ranged Role Quests"),
     ];
 
+    private readonly string[] _retryDifficulties = ["Normal", "Easy", "Very Easy"];
+
+    private readonly TerritoryData _territoryData;
+    private readonly QuestRegistry _questRegistry;
+    private readonly QuestData _questData;
+    private readonly IDataManager _dataManager;
+    private readonly ILogger<SinglePlayerDutyConfigComponent> _logger;
+    private readonly List<(EClassJob ClassJob, int Category)> _sortedClassJobs;
+
     private ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>> _startingCityBattles =
         ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>>.Empty;
 
@@ -72,6 +75,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
         _questData = questData;
         _dataManager = dataManager;
         _logger = logger;
+
+        _sortedClassJobs = dataManager.GetExcelSheet<ClassJob>()
+            .Where(x => x is { RowId: > 0, UIPriority: < 100 })
+            .Select(x => (ClassJob: (EClassJob)x.RowId, Priority: x.UIPriority))
+            .OrderBy(x => x.Priority)
+            .Select(x => (x.ClassJob, x.Priority / 10))
+            .ToList();
     }
 
     public void Reload()
@@ -256,8 +266,23 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
             Save();
         }
 
-        ImGui.TextColored(ImGuiColors.DalamudRed,
-            "Work in Progress: For now, this will always use BossMod for combat.");
+        using (ImRaii.PushIndent(ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X))
+        {
+            ImGui.AlignTextToFramePadding();
+            ImGui.TextColored(ImGuiColors.DalamudRed,
+                "Work in Progress: For now, this will always use BossMod for combat.");
+
+            using (ImRaii.Disabled(!runSoloInstancesWithBossMod))
+            {
+                int retryDifficulty = Configuration.SinglePlayerDuties.RetryDifficulty;
+                if (ImGui.Combo("Difficulty when retrying a quest battle", ref retryDifficulty, _retryDifficulties,
+                        _retryDifficulties.Length))
+                {
+                    Configuration.SinglePlayerDuties.RetryDifficulty = (byte)retryDifficulty;
+                    Save();
+                }
+            }
+        }
 
         ImGui.Separator();
 
@@ -286,7 +311,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
 
     private void DrawMainScenarioConfigTable()
     {
-        using var tab = ImRaii.TabItem("MSQ###MSQ");
+        using var tab = ImRaii.TabItem("Main Scenario Quests###MSQ");
         if (!tab)
             return;
 
@@ -323,10 +348,19 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
         if (!child)
             return;
 
-        foreach (EClassJob classJob in Enum.GetValues<EClassJob>())
+        int oldPriority = 0;
+        foreach (var (classJob, priority) in _sortedClassJobs)
         {
             if (_jobQuestBattles.TryGetValue(classJob, out var dutyInfos))
             {
+                if (priority != oldPriority)
+                {
+                    oldPriority = priority;
+                    ImGui.Spacing();
+                    ImGui.Separator();
+                    ImGui.Spacing();
+                }
+
                 string jobName = classJob.ToFriendlyString();
                 if (classJob.IsClass())
                     jobName += $" / {classJob.AsJob().ToFriendlyString()}";
@@ -434,7 +468,8 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
                         {
                             using var _ = ImRaii.Tooltip();
 
-                            ImGui.TextColored(ImGuiColors.DalamudYellow, "While testing, the following issues have been found:");
+                            ImGui.TextColored(ImGuiColors.DalamudYellow,
+                                "While testing, the following issues have been found:");
                             foreach (string note in dutyInfo.Notes)
                                 ImGui.BulletText(note);
                         }