diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index 1891e5b1b..358c30d93 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -91,7 +91,7 @@ internal static class UseItem step.CompletionQuestVariablesFlags); } - return [unmount, task]; + return [unmount, new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(0.5)), task]; } else if (step.DataId != null) { diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index 7ba7f3cc8..2be98c4db 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -45,12 +45,21 @@ internal sealed class QuestData .Where(x => x.RowId > 0 && x.Quest.Row > 0) .ToDictionary(x => x.Quest.Row, x => x.Redo); + Dictionary startingCities = new(); + for (byte redoChapter = 1; redoChapter <= 3; ++redoChapter) + { + var questRedo = dataManager.GetExcelSheet()!.GetRow(redoChapter)!; + foreach (var quest in questRedo.Quest.Where(x => x.Row > 0)) + startingCities[quest.Row] = redoChapter; + } + List quests = [ ..dataManager.GetExcelSheet()! .Where(x => x.RowId > 0) .Where(x => x.IssuerLocation.Row > 0) - .Select(x => new QuestInfo(x, questChapters.GetValueOrDefault(x.RowId))), + .Select(x => new QuestInfo(x, questChapters.GetValueOrDefault(x.RowId), + startingCities.GetValueOrDefault(x.RowId))), ..dataManager.GetExcelSheet()! .Where(x => x.RowId > 0) .Select(x => new SatisfactionSupplyInfo(x)), @@ -150,6 +159,14 @@ internal sealed class QuestData AddPreviousQuest(new QuestId(3821), new QuestId(spearfishing)); AddPreviousQuest(new QuestId(3833), new QuestId(spearfishing)); */ + + // initial city quests are side quests + ((QuestInfo)_quests[new QuestId(107)]).StartingCity = 1; + ((QuestInfo)_quests[new QuestId(39)]).StartingCity = 2; + ((QuestInfo)_quests[new QuestId(594)]).StartingCity = 3; + + // follow-up quests to picking a GC + AddGcFollowUpQuests(); } private void AddPreviousQuest(QuestId questToUpdate, QuestId requiredQuestId) @@ -158,6 +175,16 @@ internal sealed class QuestData quest.AddPreviousQuest(new QuestInfo.PreviousQuestInfo(requiredQuestId)); } + private void AddGcFollowUpQuests() + { + QuestId[] questIds = [new(683), new(684), new(685)]; + foreach (QuestId questId in questIds) + { + QuestInfo quest = (QuestInfo)_quests[questId]; + quest.AddQuestLocks(QuestInfo.QuestJoin.AtLeastOne, questIds.Where(x => x != questId).ToArray()); + } + } + public IQuestInfo GetQuestInfo(ElementId elementId) { return _quests[elementId] ?? throw new ArgumentOutOfRangeException(nameof(elementId)); diff --git a/Questionable/Functions/QuestFunctions.cs b/Questionable/Functions/QuestFunctions.cs index 3f48db397..091150a86 100644 --- a/Questionable/Functions/QuestFunctions.cs +++ b/Questionable/Functions/QuestFunctions.cs @@ -453,6 +453,14 @@ internal sealed unsafe class QuestFunctions return !HasCompletedPreviousQuests(questInfo, extraCompletedQuest) || !HasCompletedPreviousInstances(questInfo); } + public bool IsQuestUnobtainable(ElementId elementId, ElementId? extraCompletedQuest = null) + { + if (elementId is QuestId questId) + return IsQuestUnobtainable(questId, extraCompletedQuest); + else + return false; + } + public bool IsQuestUnobtainable(QuestId questId, ElementId? extraCompletedQuest = null) { var questInfo = (QuestInfo)_questData.GetQuestInfo(questId); @@ -468,6 +476,36 @@ internal sealed unsafe class QuestFunctions if (_questData.GetLockedClassQuests().Contains(questId)) return true; + unsafe + { + var startingCity = PlayerState.Instance()->StartTown; + if (questInfo.StartingCity > 0 && questInfo.StartingCity != startingCity) + return true; + + if (questId.Value == 674 && startingCity == 3) + return true; + if (questId.Value == 673 && startingCity != 3) + return true; + + Dictionary closeToHomeQuests = new() + { + { 108, EClassJob.Marauder }, + { 109, EClassJob.Arcanist }, + { 85, EClassJob.Lancer }, + { 123, EClassJob.Archer }, + { 124, EClassJob.Conjurer }, + { 568, EClassJob.Gladiator }, + { 569, EClassJob.Pugilist }, + { 570, EClassJob.Thaumaturge } + }; + if (closeToHomeQuests.TryGetValue(questId.Value, out EClassJob neededStartingClass)) + { + EClassJob actualStartingClass = (EClassJob)PlayerState.Instance()->FirstClass; + if (actualStartingClass != neededStartingClass) + return true; + } + } + return false; } diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index 9b1214b98..aeda70c33 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -11,7 +12,7 @@ namespace Questionable.Model; internal sealed class QuestInfo : IQuestInfo { - public QuestInfo(ExcelQuest quest, ushort newGamePlusChapter) + public QuestInfo(ExcelQuest quest, ushort newGamePlusChapter, byte startingCity) { QuestId = new QuestId((ushort)(quest.RowId & 0xFFFF)); @@ -60,6 +61,7 @@ internal sealed class QuestInfo : IQuestInfo ClassJobs = QuestInfoUtils.AsList(quest.ClassJobCategory0.Value!); IsSeasonalEvent = quest.Festival.Row != 0; NewGamePlusChapter = newGamePlusChapter; + StartingCity = startingCity; Expansion = (EExpansionVersion)quest.Expansion.Row; } @@ -69,10 +71,10 @@ internal sealed class QuestInfo : IQuestInfo public ushort Level { get; } public uint IssuerDataId { get; } public bool IsRepeatable { get; } - public ImmutableList PreviousQuests { get; set; } + public ImmutableList PreviousQuests { get; private set; } public QuestJoin PreviousQuestJoin { get; } - public ImmutableList QuestLocks { get; } - public QuestJoin QuestLockJoin { get; } + public ImmutableList QuestLocks { get; private set; } + public QuestJoin QuestLockJoin { get; private set; } public List PreviousInstanceContent { get; } public QuestJoin PreviousInstanceContentJoin { get; } public uint? JournalGenre { get; } @@ -84,6 +86,7 @@ internal sealed class QuestInfo : IQuestInfo public IReadOnlyList ClassJobs { get; } public bool IsSeasonalEvent { get; } public ushort NewGamePlusChapter { get; } + public byte StartingCity { get; set; } public EExpansionVersion Expansion { get; } [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] @@ -99,5 +102,14 @@ internal sealed class QuestInfo : IQuestInfo PreviousQuests = [..PreviousQuests, questId]; } + public void AddQuestLocks(QuestJoin questJoin, params QuestId[] questId) + { + if (QuestLocks.Count > 0 && QuestLockJoin != questJoin) + throw new InvalidOperationException(); + + QuestLockJoin = questJoin; + QuestLocks = [..QuestLocks, ..questId]; + } + public sealed record PreviousQuestInfo(QuestId QuestId, byte Sequence = 0); } diff --git a/Questionable/Windows/JournalComponents/QuestJournalComponent.cs b/Questionable/Windows/JournalComponents/QuestJournalComponent.cs index 9aa04a7e6..715c045fc 100644 --- a/Questionable/Windows/JournalComponents/QuestJournalComponent.cs +++ b/Questionable/Windows/JournalComponents/QuestJournalComponent.cs @@ -19,9 +19,13 @@ namespace Questionable.Windows.JournalComponents; internal sealed class QuestJournalComponent { - private readonly Dictionary _genreCounts = new(); - private readonly Dictionary _categoryCounts = new(); - private readonly Dictionary _sectionCounts = new(); + private readonly Dictionary _genreCounts = []; + + private readonly Dictionary _categoryCounts = + []; + + private readonly Dictionary _sectionCounts = + []; private readonly JournalData _journalData; private readonly QuestRegistry _questRegistry; @@ -95,7 +99,7 @@ internal sealed class QuestJournalComponent if (filter.Section.QuestCount == 0) return; - (int supported, int completed) = _sectionCounts.GetValueOrDefault(filter.Section); + (int available, int obtainable, int completed) = _sectionCounts.GetValueOrDefault(filter.Section); ImGui.TableNextRow(); ImGui.TableNextColumn(); @@ -103,9 +107,9 @@ internal sealed class QuestJournalComponent bool open = ImGui.TreeNodeEx(filter.Section.Name, ImGuiTreeNodeFlags.SpanFullWidth); ImGui.TableNextColumn(); - DrawCount(supported, filter.Section.QuestCount); + DrawCount(available, filter.Section.QuestCount); ImGui.TableNextColumn(); - DrawCount(completed, filter.Section.QuestCount); + DrawCount(completed, obtainable); if (open) { @@ -121,7 +125,7 @@ internal sealed class QuestJournalComponent if (filter.Category.QuestCount == 0) return; - (int supported, int completed) = _categoryCounts.GetValueOrDefault(filter.Category); + (int available, int obtainable, int completed) = _categoryCounts.GetValueOrDefault(filter.Category); ImGui.TableNextRow(); ImGui.TableNextColumn(); @@ -129,9 +133,9 @@ internal sealed class QuestJournalComponent bool open = ImGui.TreeNodeEx(filter.Category.Name, ImGuiTreeNodeFlags.SpanFullWidth); ImGui.TableNextColumn(); - DrawCount(supported, filter.Category.QuestCount); + DrawCount(available, filter.Category.QuestCount); ImGui.TableNextColumn(); - DrawCount(completed, filter.Category.QuestCount); + DrawCount(completed, obtainable); if (open) { @@ -147,7 +151,7 @@ internal sealed class QuestJournalComponent if (filter.Genre.QuestCount == 0) return; - (int supported, int completed) = _genreCounts.GetValueOrDefault(filter.Genre); + (int supported, int obtainable, int completed) = _genreCounts.GetValueOrDefault(filter.Genre); ImGui.TableNextRow(); ImGui.TableNextColumn(); @@ -157,7 +161,7 @@ internal sealed class QuestJournalComponent ImGui.TableNextColumn(); DrawCount(supported, filter.Genre.QuestCount); ImGui.TableNextColumn(); - DrawCount(completed, filter.Genre.QuestCount); + DrawCount(completed, obtainable); if (open) { @@ -192,7 +196,8 @@ internal sealed class QuestJournalComponent bool openInQuestMap = _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo); if (ImGui.MenuItem("View in Quest Map", questInfo.QuestId is QuestId && openInQuestMap)) { - _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, commandInfo!); + _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, + commandInfo!); } ImGui.EndPopup(); @@ -317,8 +322,9 @@ internal sealed class QuestJournalComponent { int available = genre.Quests.Count(x => _questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled); + int obtainable = genre.Quests.Count(x => !_questFunctions.IsQuestUnobtainable(x.QuestId)); int completed = genre.Quests.Count(x => _questFunctions.IsQuestComplete(x.QuestId)); - _genreCounts[genre] = (available, completed); + _genreCounts[genre] = (available, obtainable, completed); } foreach (var category in _journalData.Categories) @@ -328,8 +334,9 @@ internal sealed class QuestJournalComponent .Select(x => x.Value) .ToList(); int available = counts.Sum(x => x.Available); + int obtainable = counts.Sum(x => x.Obtainable); int completed = counts.Sum(x => x.Completed); - _categoryCounts[category] = (available, completed); + _categoryCounts[category] = (available, obtainable, completed); } foreach (var section in _journalData.Sections) @@ -339,21 +346,22 @@ internal sealed class QuestJournalComponent .Select(x => x.Value) .ToList(); int available = counts.Sum(x => x.Available); + int obtainable = counts.Sum(x => x.Obtainable); int completed = counts.Sum(x => x.Completed); - _sectionCounts[section] = (available, completed); + _sectionCounts[section] = (available, obtainable, completed); } } internal void ClearCounts() { foreach (var genreCount in _genreCounts.ToList()) - _genreCounts[genreCount.Key] = (genreCount.Value.Available, 0); + _genreCounts[genreCount.Key] = (genreCount.Value.Available, genreCount.Value.Available, 0); foreach (var categoryCount in _categoryCounts.ToList()) - _categoryCounts[categoryCount.Key] = (categoryCount.Value.Available, 0); + _categoryCounts[categoryCount.Key] = (categoryCount.Value.Available, categoryCount.Value.Available, 0); foreach (var sectionCount in _sectionCounts.ToList()) - _sectionCounts[sectionCount.Key] = (sectionCount.Value.Available, 0); + _sectionCounts[sectionCount.Key] = (sectionCount.Value.Available, sectionCount.Value.Available, 0); } private sealed record FilteredSection(JournalData.Section Section, List Categories); diff --git a/Questionable/Windows/UiUtils.cs b/Questionable/Windows/UiUtils.cs index e0155816c..270a76d90 100644 --- a/Questionable/Windows/UiUtils.cs +++ b/Questionable/Windows/UiUtils.cs @@ -26,6 +26,8 @@ internal sealed class UiUtils return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Active"); else if (_questFunctions.IsQuestAcceptedOrComplete(elementId)) return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete"); + else if (_questFunctions.IsQuestUnobtainable(elementId)) + return (ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus, "Unobtainable"); else if (_questFunctions.IsQuestLocked(elementId)) return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked"); else