Add quest battle notes

This commit is contained in:
Liza 2025-02-21 03:22:47 +01:00
parent a75286e927
commit 31eb121cf0
Signed by: liza
GPG Key ID: 2C41B84815CF6445
8 changed files with 110 additions and 30 deletions

View File

@ -126,6 +126,9 @@ internal static class QuestStepExtensions
Assignment(nameof(QuestStep.BossModEnabled), Assignment(nameof(QuestStep.BossModEnabled),
step.BossModEnabled, emptyStep.BossModEnabled) step.BossModEnabled, emptyStep.BossModEnabled)
.AsSyntaxNodeOrToken(), .AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.BossModNotes),
step.BossModNotes, emptyStep.BossModNotes)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.SinglePlayerDutyIndex), Assignment(nameof(QuestStep.SinglePlayerDutyIndex),
step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex) step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex)
.AsSyntaxNodeOrToken(), .AsSyntaxNodeOrToken(),

View File

@ -103,7 +103,11 @@
"Z": 479.9724 "Z": 479.9724
}, },
"TerritoryId": 1053, "TerritoryId": 1053,
"InteractionType": "SinglePlayerDuty" "InteractionType": "SinglePlayerDuty",
"BossModEnabled": false,
"BossModNotes": [
"Doesn't handle death properly"
]
} }
] ]
}, },

View File

@ -61,7 +61,19 @@
"TerritoryId": 156, "TerritoryId": 156,
"InteractionType": "Interact", "InteractionType": "Interact",
"AetheryteShortcut": "Mor Dhona", "AetheryteShortcut": "Mor Dhona",
"TargetTerritoryId": 351 "TargetTerritoryId": 351,
"SkipConditions": {
"AetheryteShortcutIf": {
"InTerritory": [
351
]
},
"StepIf": {
"InTerritory": [
351
]
}
}
}, },
{ {
"DataId": 1032081, "DataId": 1032081,
@ -73,13 +85,14 @@
"TerritoryId": 351, "TerritoryId": 351,
"InteractionType": "SinglePlayerDuty", "InteractionType": "SinglePlayerDuty",
"Comment": "Estinien vs. Arch Ultima", "Comment": "Estinien vs. Arch Ultima",
"DialogueChoices": [ "BossModEnabled": false,
{ "BossModNotes": [
"Type": "YesNo", "AI doesn't move automatically for the first boss",
"Prompt": "TEXT_LUCKMG110_03682_Q1_100_125", "AI doesn't move automatically for the dialogue with gaius on the bridge",
"Yes": true "After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)",
} "After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking"
] ],
"$.1": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one"
} }
] ]
}, },

View File

@ -1270,6 +1270,12 @@
"BossModEnabled": { "BossModEnabled": {
"type": "boolean" "type": "boolean"
}, },
"BossModNotes": {
"type": "array",
"items": {
"type": "string"
}
},
"SinglePlayerDutyIndex": { "SinglePlayerDutyIndex": {
"type": "integer", "type": "integer",
"minimum": 0, "minimum": 0,

View File

@ -76,6 +76,7 @@ public sealed class QuestStep
public uint? ContentFinderConditionId { get; set; } public uint? ContentFinderConditionId { get; set; }
public bool AutoDutyEnabled { get; set; } public bool AutoDutyEnabled { get; set; }
public bool BossModEnabled { get; set; } public bool BossModEnabled { get; set; }
public List<string> BossModNotes { get; set; } = [];
public byte SinglePlayerDutyIndex { get; set; } public byte SinglePlayerDutyIndex { get; set; }
public SkipConditions? SkipConditions { get; set; } public SkipConditions? SkipConditions { get; set; }

View File

@ -19,6 +19,7 @@ using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Interactions;
using Questionable.Data; using Questionable.Data;
using Questionable.External;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Gathering; using Questionable.Model.Gathering;
@ -45,6 +46,7 @@ internal sealed class InteractionUiController : IDisposable
private readonly ITargetManager _targetManager; private readonly ITargetManager _targetManager;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly ShopController _shopController; private readonly ShopController _shopController;
private readonly BossModIpc _bossModIpc;
private readonly ILogger<InteractionUiController> _logger; private readonly ILogger<InteractionUiController> _logger;
private readonly Regex _returnRegex; private readonly Regex _returnRegex;
private readonly Regex _purchaseItemRegex; private readonly Regex _purchaseItemRegex;
@ -68,6 +70,7 @@ internal sealed class InteractionUiController : IDisposable
IPluginLog pluginLog, IPluginLog pluginLog,
IClientState clientState, IClientState clientState,
ShopController shopController, ShopController shopController,
BossModIpc bossModIpc,
ILogger<InteractionUiController> logger) ILogger<InteractionUiController> logger)
{ {
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
@ -85,6 +88,7 @@ internal sealed class InteractionUiController : IDisposable
_targetManager = targetManager; _targetManager = targetManager;
_clientState = clientState; _clientState = clientState;
_shopController = shopController; _shopController = shopController;
_bossModIpc = bossModIpc;
_logger = logger; _logger = logger;
_returnRegex = _dataManager.GetExcelSheet<Addon>().GetRow(196).GetRegex(addon => addon.Text, pluginLog)!; _returnRegex = _dataManager.GetExcelSheet<Addon>().GetRow(196).GetRegex(addon => addon.Text, pluginLog)!;
@ -176,7 +180,10 @@ internal sealed class InteractionUiController : IDisposable
int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt); int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt);
if (answer != null) if (answer != null)
{
_logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", answer, actualPrompt);
addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value); addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value);
}
} }
private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args) private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
@ -224,6 +231,7 @@ internal sealed class InteractionUiController : IDisposable
int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps); int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps);
if (answer != null) if (answer != null)
{ {
_logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", answer, actualPrompt);
addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value); addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value);
return; return;
} }
@ -266,6 +274,7 @@ internal sealed class InteractionUiController : IDisposable
int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x)); int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x));
if (questSelection >= 0) if (questSelection >= 0)
{ {
_logger.LogInformation("Selecting quest {QuestName}", questName);
addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection); addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection);
return true; return true;
} }
@ -655,13 +664,21 @@ internal sealed class InteractionUiController : IDisposable
continue; continue;
} }
_logger.LogInformation("Returning {YesNo} for '{Prompt}'", dialogueChoice.Yes ? "Yes" : "No", actualPrompt);
addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1); addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1);
return true; return true;
} }
if (step is { InteractionType: EInteractionType.SinglePlayerDuty, BossModEnabled: true }) if (step is { InteractionType: EInteractionType.SinglePlayerDuty } &&
_bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled))
{ {
_logger.LogTrace("DefaultYesNo: probably Single Player Duty"); // Most of these are yes/no dialogs "Duty calls, ...".
//
// For 'Vows of Virtue, Deeds of Cruelty', there's no such dialog, and it just puts you into the instance
// 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); addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true; return true;
} }

View File

@ -84,6 +84,9 @@ internal sealed class BossModIpc
public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault) public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault)
{ {
if (!IsSupported())
return false;
if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod) if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod)
return false; return false;

View File

@ -40,12 +40,22 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
(EClassJob.BlackMage, "Magical Ranged Role Quests"), (EClassJob.BlackMage, "Magical Ranged Role Quests"),
]; ];
private ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>> _startingCityBattles = ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>>.Empty; private ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>> _startingCityBattles =
private ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>> _mainScenarioBattles = ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>>.Empty; ImmutableDictionary<EAetheryteLocation, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _jobQuestBattles = ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _roleQuestBattles = ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty; private ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>> _mainScenarioBattles =
ImmutableDictionary<EExpansionVersion, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _jobQuestBattles =
ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>> _roleQuestBattles =
ImmutableDictionary<EClassJob, List<SinglePlayerDutyInfo>>.Empty;
private ImmutableList<SinglePlayerDutyInfo> _otherRoleQuestBattles = ImmutableList<SinglePlayerDutyInfo>.Empty; private ImmutableList<SinglePlayerDutyInfo> _otherRoleQuestBattles = ImmutableList<SinglePlayerDutyInfo>.Empty;
private ImmutableList<(string Label, List<SinglePlayerDutyInfo>)> _otherQuestBattles = ImmutableList<(string Label, List<SinglePlayerDutyInfo>)>.Empty;
private ImmutableList<(string Label, List<SinglePlayerDutyInfo>)> _otherQuestBattles =
ImmutableList<(string Label, List<SinglePlayerDutyInfo>)>.Empty;
public SinglePlayerDutyConfigComponent( public SinglePlayerDutyConfigComponent(
IDalamudPluginInterface pluginInterface, IDalamudPluginInterface pluginInterface,
@ -103,10 +113,10 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
{ {
IQuestInfo questInfo = _questData.GetQuestInfo(questId); IQuestInfo questInfo = _questData.GetQuestInfo(questId);
QuestStep questStep = new QuestStep QuestStep questStep = new QuestStep
{ {
SinglePlayerDutyIndex = 0, SinglePlayerDutyIndex = 0,
BossModEnabled = false, BossModEnabled = false,
}; };
bool enabled; bool enabled;
if (_questRegistry.TryGetQuest(questId, out var quest)) if (_questRegistry.TryGetQuest(questId, out var quest))
{ {
@ -122,7 +132,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
x.Step.SinglePlayerDutyIndex == index); x.Step.SinglePlayerDutyIndex == index);
if (foundStep == default) if (foundStep == default)
{ {
_logger.LogWarning("Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, index); _logger.LogWarning(
"Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId,
index);
enabled = false; enabled = false;
} }
else else
@ -156,7 +168,8 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
questInfo.SortKey, questInfo.SortKey,
questStep.SinglePlayerDutyIndex, questStep.SinglePlayerDutyIndex,
enabled, enabled,
questStep.BossModEnabled); questStep.BossModEnabled,
questStep.BossModNotes);
if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334) if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334)
startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo); startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo);
@ -343,7 +356,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
} }
} }
if(ImGui.CollapsingHeader("General Role Quests")) if (ImGui.CollapsingHeader("General Role Quests"))
DrawQuestTable("RoleQuestsGeneral", _otherRoleQuestBattles); DrawQuestTable("RoleQuestsGeneral", _otherRoleQuestBattles);
} }
@ -380,9 +393,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
? SupportedCfcOptions ? SupportedCfcOptions
: UnsupportedCfcOptions; : UnsupportedCfcOptions;
int value = 0; int value = 0;
if (Configuration.Duties.WhitelistedDutyCfcIds.Contains(dutyInfo.CfcId)) if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId))
value = 1; value = 1;
if (Configuration.Duties.BlacklistedDutyCfcIds.Contains(dutyInfo.CfcId)) if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId))
value = 2; value = 2;
if (ImGui.TableNextColumn()) if (ImGui.TableNextColumn())
@ -407,6 +420,25 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
ImGuiComponents.HelpMarker("Questionable doesn't include support for this quest.", ImGuiComponents.HelpMarker("Questionable doesn't include support for this quest.",
FontAwesomeIcon.Times, ImGuiColors.DalamudRed); FontAwesomeIcon.Times, ImGuiColors.DalamudRed);
} }
else if (dutyInfo.Notes.Count > 0)
{
using var color = new ImRaii.Color();
color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow);
ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString());
}
if (ImGui.IsItemHovered())
{
using var _ = ImRaii.Tooltip();
ImGui.TextColored(ImGuiColors.DalamudYellow, "While testing, the following issues have been found:");
foreach (string note in dutyInfo.Notes)
ImGui.BulletText(note);
}
}
} }
if (ImGui.TableNextColumn()) if (ImGui.TableNextColumn())
@ -417,13 +449,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
ImGui.SetNextItemWidth(200); ImGui.SetNextItemWidth(200);
if (ImGui.Combo(string.Empty, ref value, labels, labels.Length)) if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
{ {
Configuration.Duties.WhitelistedDutyCfcIds.Remove(dutyInfo.CfcId); Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId);
Configuration.Duties.BlacklistedDutyCfcIds.Remove(dutyInfo.CfcId); Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId);
if (value == 1) if (value == 1)
Configuration.Duties.WhitelistedDutyCfcIds.Add(dutyInfo.CfcId); Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId);
else if (value == 2) else if (value == 2)
Configuration.Duties.BlacklistedDutyCfcIds.Add(dutyInfo.CfcId); Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId);
Save(); Save();
} }
@ -460,5 +492,6 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
ushort SortKey, ushort SortKey,
byte Index, byte Index,
bool Enabled, bool Enabled,
bool BossModEnabledByDefault); bool BossModEnabledByDefault,
List<string> Notes);
} }