Add quest battle difficulty selection; UI tweaks

This commit is contained in:
Liza 2025-02-21 12:21:01 +01:00
parent 31eb121cf0
commit 71e0b01dbc
Signed by: liza
GPG Key ID: 2C41B84815CF6445
5 changed files with 118 additions and 15 deletions

View File

@ -28,7 +28,11 @@
"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)"
]
}
]
},

View File

@ -46,6 +46,10 @@
},
"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",

View 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; } = [];
}

View 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);

View 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);
}