Add Statistics/Progress window
authorLiza Carvelli <liza@carvel.li>
Mon, 29 Jul 2024 14:54:18 +0000 (16:54 +0200)
committerLiza Carvelli <liza@carvel.li>
Mon, 29 Jul 2024 14:54:18 +0000 (16:54 +0200)
Questionable/Controller/QuestRegistry.cs
Questionable/DalamudInitializer.cs
Questionable/Data/JournalData.cs [new file with mode: 0644]
Questionable/Data/QuestData.cs
Questionable/Model/QuestInfo.cs
Questionable/QuestionablePlugin.cs
Questionable/Windows/JournalProgressWindow.cs [new file with mode: 0644]
Questionable/Windows/QuestComponents/QuestTooltipComponent.cs [new file with mode: 0644]
Questionable/Windows/QuestComponents/QuickAccessButtonsComponent.cs
Questionable/Windows/QuestSelectionWindow.cs

index d0c02abd19d32b4e7100e609ab375c3628fffe99..71750a1c07836fbcd26f50ff45b984af194f36f8 100644 (file)
@@ -44,6 +44,8 @@ internal sealed class QuestRegistry
     public int ValidationIssueCount => _questValidator.IssueCount;
     public int ValidationErrorCount => _questValidator.ErrorCount;
 
+    public event EventHandler? Reloaded;
+
     public void Reload()
     {
         _questValidator.Reset();
@@ -63,6 +65,7 @@ internal sealed class QuestRegistry
         }
 
         ValidateQuests();
+        Reloaded?.Invoke(this, EventArgs.Empty);
         _logger.LogInformation("Loaded {Count} quests in total", _quests.Count);
     }
 
index d2cf7af44633fc11f0f9397ba9706671e38e1de0..76678de6802f044006573b76cf2a280be22363ad 100644 (file)
@@ -30,7 +30,8 @@ internal sealed class DalamudInitializer : IDisposable
         DebugOverlay debugOverlay,
         ConfigWindow configWindow,
         QuestSelectionWindow questSelectionWindow,
-        QuestValidationWindow questValidationWindow)
+        QuestValidationWindow questValidationWindow,
+        JournalProgressWindow journalProgressWindow)
     {
         _pluginInterface = pluginInterface;
         _framework = framework;
@@ -46,6 +47,7 @@ internal sealed class DalamudInitializer : IDisposable
         _windowSystem.AddWindow(debugOverlay);
         _windowSystem.AddWindow(questSelectionWindow);
         _windowSystem.AddWindow(questValidationWindow);
+        _windowSystem.AddWindow(journalProgressWindow);
 
         _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
         _pluginInterface.UiBuilder.OpenMainUi += _questWindow.Toggle;
diff --git a/Questionable/Data/JournalData.cs b/Questionable/Data/JournalData.cs
new file mode 100644 (file)
index 0000000..a7bf55e
--- /dev/null
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using System.Linq;
+using Dalamud.Plugin.Services;
+using Lumina.Excel.GeneratedSheets;
+using Questionable.Model;
+
+namespace Questionable.Data;
+
+internal sealed class JournalData
+{
+    public JournalData(IDataManager dataManager, QuestData questData)
+    {
+        var genres = dataManager.GetExcelSheet<JournalGenre>()!
+            .Where(x => x.RowId > 0 && x.Icon > 0)
+            .Select(x => new Genre(x, questData.GetAllByJournalGenre(x.RowId)))
+            .ToList();
+
+        var limsaStart = dataManager.GetExcelSheet<QuestRedo>()!.GetRow(1)!;
+        var gridaniaStart = dataManager.GetExcelSheet<QuestRedo>()!.GetRow(2)!;
+        var uldahStart = dataManager.GetExcelSheet<QuestRedo>()!.GetRow(3)!;
+        var genreLimsa = new Genre(uint.MaxValue - 3, "Starting in Limsa Lominsa", 1,
+            new uint[] { 108, 109 }.Concat(limsaStart.Quest.Select(x => x.Row))
+                .Where(x => x != 0)
+                .Select(x => questData.GetQuestInfo((ushort)(x & 0xFFFF))).ToList());
+        var genreGridania = new Genre(uint.MaxValue - 2, "Starting in Gridania", 1,
+            new uint[] { 85, 123, 124 }.Concat(gridaniaStart.Quest.Select(x => x.Row))
+                .Where(x => x != 0)
+                .Select(x => questData.GetQuestInfo((ushort)(x & 0xFFFF))).ToList());
+        var genreUldah = new Genre(uint.MaxValue - 1, "Starting in Ul'dah", 1,
+            new uint[] { 568, 569, 570 }.Concat(uldahStart.Quest.Select(x => x.Row))
+                .Where(x => x != 0)
+                .Select(x => questData.GetQuestInfo((ushort)(x & 0xFFFF)))
+                .ToList());
+        genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]);
+        genres.Single(x => x.Id == 1)
+            .Quests
+            .RemoveAll(x =>
+                genreLimsa.Quests.Contains(x) || genreGridania.Quests.Contains(x) || genreUldah.Quests.Contains(x));
+
+        Genres = genres.AsReadOnly();
+        Categories = dataManager.GetExcelSheet<JournalCategory>()!
+            .Where(x => x.RowId > 0)
+            .Select(x => new Category(x, Genres.Where(y => y.CategoryId == x.RowId).ToList()))
+            .ToList()
+            .AsReadOnly();
+        Sections = dataManager.GetExcelSheet<JournalSection>()!
+            .Select(x => new Section(x, Categories.Where(y => y.SectionId == x.RowId).ToList()))
+            .ToList();
+    }
+
+    public IReadOnlyList<Genre> Genres { get; }
+    public IReadOnlyList<Category> Categories { get; }
+    public List<Section> Sections { get; set; }
+
+    internal sealed class Genre
+    {
+        public Genre(JournalGenre journalGenre, List<QuestInfo> quests)
+        {
+            Id = journalGenre.RowId;
+            Name = journalGenre.Name.ToString();
+            CategoryId = journalGenre.JournalCategory.Row;
+            Quests = quests;
+        }
+
+        public Genre(uint id, string name, uint categoryId, List<QuestInfo> quests)
+        {
+            Id = id;
+            Name = name;
+            CategoryId = categoryId;
+            Quests = quests;
+        }
+
+        public uint Id { get; }
+        public string Name { get; }
+        public uint CategoryId { get; }
+        public List<QuestInfo> Quests { get; }
+        public int QuestCount => Quests.Count;
+    }
+
+    internal sealed class Category(JournalCategory journalCategory, IReadOnlyList<Genre> genres)
+    {
+        public uint Id { get; } = journalCategory.RowId;
+        public string Name { get; } = journalCategory.Name.ToString();
+        public uint SectionId { get; } = journalCategory.JournalSection.Row;
+        public IReadOnlyList<Genre> Genres { get; } = genres;
+        public int QuestCount => Genres.Sum(x => x.QuestCount);
+    }
+
+    internal sealed class Section(JournalSection journalSection, IReadOnlyList<Category> categories)
+    {
+        public uint Id { get; } = journalSection.RowId;
+        public string Name { get; } = journalSection.Name.ToString();
+        public IReadOnlyList<Category> Categories { get; } = categories;
+        public int QuestCount => Categories.Sum(x => x.QuestCount);
+    }
+}
index ac63dc7e23f7cd903fee900afa4f9294e00fd9a4..7b36aef5c81a55c170ddedc537930b02793829e2 100644 (file)
@@ -17,6 +17,7 @@ internal sealed class QuestData
         _quests = dataManager.GetExcelSheet<Quest>()!
             .Where(x => x.RowId > 0)
             .Where(x => x.IssuerLocation.Row > 0)
+            .Where(x => x.Festival.Row == 0)
             .Select(x => new QuestInfo(x))
             .ToImmutableDictionary(x => x.QuestId, x => x);
     }
@@ -34,4 +35,13 @@ internal sealed class QuestData
     }
 
     public bool IsIssuerOfAnyQuest(uint targetId) => _quests.Values.Any(x => x.IssuerDataId == targetId);
+
+    public List<QuestInfo> GetAllByJournalGenre(uint journalGenre)
+    {
+        return _quests.Values
+            .Where(x => x.JournalGenre == journalGenre)
+            .OrderBy(x => x.SortKey)
+            .ThenBy(x => x.QuestId)
+            .ToList();
+    }
 }
index 54fd6fad628eda849df761fe9686026e041515a5..bdfa0a7fe7f7bee2677c5a63e14e44bb45b18cf7 100644 (file)
@@ -14,7 +14,23 @@ internal sealed class QuestInfo
     public QuestInfo(ExcelQuest quest)
     {
         QuestId = (ushort)(quest.RowId & 0xFFFF);
-        Name = quest.Name.ToString();
+
+        string suffix = QuestId switch
+        {
+            85 => " (LNC)",
+            108 => " (MRD)",
+            109 => " (ACN)",
+            123 => " (ARC)",
+            124 => " (CNJ)",
+            568 => " (GLA)",
+            569 => " (PGL)",
+            570 => " (THM)",
+            673 => " (Ul'dah)",
+            674 => " (Limsa/Gridania)",
+            _ => "",
+        };
+
+        Name = $"{quest.Name}{suffix}";
         Level = quest.ClassJobLevel0;
         IssuerDataId = quest.IssuerStart;
         IsRepeatable = quest.IsRepeatable;
@@ -22,6 +38,8 @@ internal sealed class QuestInfo
         PreviousQuestJoin = (QuestJoin)quest.PreviousQuestJoin;
         QuestLocks = quest.QuestLock.Select(x => (ushort)(x.Row & 0xFFFFF)).Where(x => x != 0).ToImmutableList();
         QuestLockJoin = (QuestJoin)quest.QuestLockJoin;
+        JournalGenre = quest.JournalGenre?.Row;
+        SortKey = quest.SortKey;
         IsMainScenarioQuest = quest.JournalGenre?.Value?.JournalCategory?.Value?.JournalSection?.Row is 0 or 1;
         CompletesInstantly = quest.ToDoCompleteSeq[0] == 0;
         PreviousInstanceContent = quest.InstanceContent.Select(x => (ushort)x.Row).Where(x => x != 0).ToList();
@@ -42,6 +60,8 @@ internal sealed class QuestInfo
     public QuestJoin QuestLockJoin { get; }
     public List<ushort> PreviousInstanceContent { get; }
     public QuestJoin PreviousInstanceContentJoin { get; }
+    public uint? JournalGenre { get; }
+    public ushort SortKey { get; set; }
     public bool IsMainScenarioQuest { get; }
     public bool CompletesInstantly { get; }
     public GrandCompany GrandCompany { get; }
index f44b41d77e327535bb40aa4d5062bbeb194815dc..df100963831635951399030b13c0cbc1db5e7a37 100644 (file)
@@ -89,6 +89,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<ChatFunctions>();
         serviceCollection.AddSingleton<AetherCurrentData>();
         serviceCollection.AddSingleton<AetheryteData>();
+        serviceCollection.AddSingleton<JournalData>();
         serviceCollection.AddSingleton<QuestData>();
         serviceCollection.AddSingleton<TerritoryData>();
         serviceCollection.AddSingleton<NavmeshIpc>();
@@ -159,6 +160,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<ActiveQuestComponent>();
         serviceCollection.AddSingleton<ARealmRebornComponent>();
         serviceCollection.AddSingleton<CreationUtilsComponent>();
+        serviceCollection.AddSingleton<QuestTooltipComponent>();
         serviceCollection.AddSingleton<QuickAccessButtonsComponent>();
         serviceCollection.AddSingleton<RemainingTasksComponent>();
 
@@ -167,6 +169,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<DebugOverlay>();
         serviceCollection.AddSingleton<QuestSelectionWindow>();
         serviceCollection.AddSingleton<QuestValidationWindow>();
+        serviceCollection.AddSingleton<JournalProgressWindow>();
     }
 
     private static void AddQuestValidators(ServiceCollection serviceCollection)
diff --git a/Questionable/Windows/JournalProgressWindow.cs b/Questionable/Windows/JournalProgressWindow.cs
new file mode 100644 (file)
index 0000000..5dd82b3
--- /dev/null
@@ -0,0 +1,268 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Numerics;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin.Services;
+using ImGuiNET;
+using LLib.ImGui;
+using Questionable.Controller;
+using Questionable.Data;
+using Questionable.Model;
+using Questionable.Windows.QuestComponents;
+
+namespace Questionable.Windows;
+
+internal sealed class JournalProgressWindow : LWindow, IDisposable
+{
+    private readonly JournalData _journalData;
+    private readonly QuestRegistry _questRegistry;
+    private readonly GameFunctions _gameFunctions;
+    private readonly UiUtils _uiUtils;
+    private readonly QuestTooltipComponent _questTooltipComponent;
+    private readonly IClientState _clientState;
+    private readonly ICommandManager _commandManager;
+
+    private readonly Dictionary<JournalData.Genre, (int Available, int Completed)> _genreCounts = new();
+    private readonly Dictionary<JournalData.Category, (int Available, int Completed)> _categoryCounts = new();
+    private readonly Dictionary<JournalData.Section, (int Available, int Completed)> _sectionCounts = new();
+
+    public JournalProgressWindow(JournalData journalData,
+        QuestRegistry questRegistry,
+        GameFunctions gameFunctions,
+        UiUtils uiUtils,
+        QuestTooltipComponent questTooltipComponent,
+        IClientState clientState,
+        ICommandManager commandManager)
+        : base("Journal Progress###QuestionableJournalProgress")
+    {
+        _journalData = journalData;
+        _questRegistry = questRegistry;
+        _gameFunctions = gameFunctions;
+        _uiUtils = uiUtils;
+        _questTooltipComponent = questTooltipComponent;
+        _clientState = clientState;
+        _commandManager = commandManager;
+
+        _clientState.Login += RefreshCounts;
+        _clientState.Logout -= ClearCounts;
+        _questRegistry.Reloaded += OnQuestsReloaded;
+
+        SizeConstraints = new WindowSizeConstraints
+        {
+            MinimumSize = new Vector2(700, 500)
+        };
+    }
+
+    private void OnQuestsReloaded(object? sender, EventArgs e) => RefreshCounts();
+
+    public override void OnOpen() => RefreshCounts();
+
+    public override void Draw()
+    {
+        ImGui.Text("The list below contains all quests that appear in your journal.");
+        ImGui.BulletText("'Supported' lists quests that Questionable can do for you");
+        ImGui.BulletText("'Completed' lists quests your current character has completed.");
+        ImGui.BulletText(
+            "Not all quests can be completed even if they're listed as available, e.g. starting city quest chains.");
+
+        ImGui.Spacing();
+        ImGui.Separator();
+        ImGui.Spacing();
+
+        using var table = ImRaii.Table("Quests", 3, ImGuiTableFlags.NoSavedSettings);
+        if (!table)
+            return;
+
+        ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide);
+        ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
+        ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
+        ImGui.TableHeadersRow();
+
+        foreach (var section in _journalData.Sections)
+        {
+            DrawSection(section);
+        }
+    }
+
+    private void DrawSection(JournalData.Section section)
+    {
+        if (section.QuestCount == 0)
+            return;
+
+        (int supported, int completed) = _sectionCounts.GetValueOrDefault(section);
+
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx(section.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        DrawCount(supported, section.QuestCount);
+        ImGui.TableNextColumn();
+        DrawCount(completed, section.QuestCount);
+
+        if (open)
+        {
+            foreach (var category in section.Categories)
+                DrawCategory(category);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawCategory(JournalData.Category category)
+    {
+        if (category.QuestCount == 0)
+            return;
+
+        (int supported, int completed) = _categoryCounts.GetValueOrDefault(category);
+
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx(category.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        DrawCount(supported, category.QuestCount);
+        ImGui.TableNextColumn();
+        DrawCount(completed, category.QuestCount);
+
+        if (open)
+        {
+            foreach (var genre in category.Genres)
+                DrawGenre(genre);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawGenre(JournalData.Genre genre)
+    {
+        if (genre.QuestCount == 0)
+            return;
+
+        (int supported, int completed) = _genreCounts.GetValueOrDefault(genre);
+
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx(genre.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        DrawCount(supported, genre.QuestCount);
+        ImGui.TableNextColumn();
+        DrawCount(completed, genre.QuestCount);
+
+        if (open)
+        {
+            foreach (var quest in genre.Quests)
+                DrawQuest(quest);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawQuest(QuestInfo questInfo)
+    {
+        _questRegistry.TryGetQuest(questInfo.QuestId, out var quest);
+
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+        ImGui.TreeNodeEx(questInfo.Name,
+            ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth);
+
+
+        if (ImGui.IsItemClicked() && _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
+        {
+            _commandManager.DispatchCommand("/questinfo",
+                questInfo.QuestId.ToString(CultureInfo.InvariantCulture), commandInfo);
+        }
+
+        if (ImGui.IsItemHovered())
+            _questTooltipComponent.Draw(questInfo);
+
+        ImGui.TableNextColumn();
+        List<string> authors = quest?.Root.Author ?? [];
+        _uiUtils.ChecklistItem(authors.Count > 0 ? string.Join(", ", authors) : string.Empty,
+            quest is { Root.Disabled: false });
+
+        ImGui.TableNextColumn();
+        var (color, icon, text) = _uiUtils.GetQuestStyle(questInfo.QuestId);
+        _uiUtils.ChecklistItem(text, color, icon);
+    }
+
+    private static void DrawCount(int count, int total)
+    {
+        string len = 9999.ToString(CultureInfo.CurrentCulture);
+        ImGui.PushFont(UiBuilder.MonoFont);
+
+        string text =
+            $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)}";
+        if (count == total)
+            ImGui.TextColored(ImGuiColors.ParsedGreen, text);
+        else
+            ImGui.TextUnformatted(text);
+
+        ImGui.PopFont();
+    }
+
+    private void RefreshCounts()
+    {
+        _genreCounts.Clear();
+        _categoryCounts.Clear();
+        _sectionCounts.Clear();
+
+        foreach (var genre in _journalData.Genres)
+        {
+            int available = genre.Quests.Count(x =>
+                _questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled);
+            int completed = genre.Quests.Count(x => _gameFunctions.IsQuestComplete(x.QuestId));
+            _genreCounts[genre] = (available, completed);
+        }
+
+        foreach (var category in _journalData.Categories)
+        {
+            var counts = _genreCounts
+                .Where(x => category.Genres.Contains(x.Key))
+                .Select(x => x.Value)
+                .ToList();
+            int available = counts.Sum(x => x.Available);
+            int completed = counts.Sum(x => x.Completed);
+            _categoryCounts[category] = (available, completed);
+        }
+
+        foreach (var section in _journalData.Sections)
+        {
+            var counts = _categoryCounts
+                .Where(x => section.Categories.Contains(x.Key))
+                .Select(x => x.Value)
+                .ToList();
+            int available = counts.Sum(x => x.Available);
+            int completed = counts.Sum(x => x.Completed);
+            _sectionCounts[section] = (available, completed);
+        }
+    }
+
+    private void ClearCounts()
+    {
+        foreach (var genreCount in _genreCounts.ToList())
+            _genreCounts[genreCount.Key] = (genreCount.Value.Available, 0);
+
+        foreach (var categoryCount in _categoryCounts.ToList())
+            _categoryCounts[categoryCount.Key] = (categoryCount.Value.Available, 0);
+
+        foreach (var sectionCount in _sectionCounts.ToList())
+            _sectionCounts[sectionCount.Key] = (sectionCount.Value.Available, 0);
+    }
+
+    public void Dispose()
+    {
+        _questRegistry.Reloaded -= OnQuestsReloaded;
+        _clientState.Logout -= ClearCounts;
+        _clientState.Login -= RefreshCounts;
+    }
+}
diff --git a/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs b/Questionable/Windows/QuestComponents/QuestTooltipComponent.cs
new file mode 100644 (file)
index 0000000..e8f5b5f
--- /dev/null
@@ -0,0 +1,170 @@
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility.Raii;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using ImGuiNET;
+using Questionable.Controller;
+using Questionable.Data;
+using Questionable.Model;
+
+namespace Questionable.Windows.QuestComponents;
+
+internal sealed class QuestTooltipComponent
+{
+    private readonly QuestRegistry _questRegistry;
+    private readonly QuestData _questData;
+    private readonly TerritoryData _territoryData;
+    private readonly GameFunctions _gameFunctions;
+    private readonly UiUtils _uiUtils;
+
+    public QuestTooltipComponent(
+        QuestRegistry questRegistry,
+        QuestData questData,
+        TerritoryData territoryData,
+        GameFunctions gameFunctions,
+        UiUtils uiUtils)
+    {
+        _questRegistry = questRegistry;
+        _questData = questData;
+        _territoryData = territoryData;
+        _gameFunctions = gameFunctions;
+        _uiUtils = uiUtils;
+    }
+
+    public void Draw(QuestInfo quest)
+    {
+        using var tooltip = ImRaii.Tooltip();
+        if (tooltip)
+        {
+            var (color, _, tooltipText) = _uiUtils.GetQuestStyle(quest.QuestId);
+            ImGui.TextColored(color, tooltipText);
+            if (quest.IsRepeatable)
+            {
+                ImGui.SameLine();
+                ImGui.TextUnformatted("Repeatable");
+            }
+
+            if (quest.CompletesInstantly)
+            {
+                ImGui.SameLine();
+                ImGui.TextUnformatted("Instant");
+            }
+
+            if (!_questRegistry.IsKnownQuest(quest.QuestId))
+            {
+                ImGui.SameLine();
+                ImGui.TextUnformatted("NoQuestPath");
+            }
+
+            DrawQuestUnlocks(quest, 0);
+        }
+    }
+
+    private void DrawQuestUnlocks(QuestInfo quest, int counter)
+    {
+        if (counter >= 10)
+            return;
+
+        if (counter != 0 && quest.IsMainScenarioQuest)
+            return;
+
+        if (counter > 0)
+            ImGui.Indent();
+
+        if (quest.PreviousQuests.Count > 0)
+        {
+            if (counter == 0)
+                ImGui.Separator();
+
+            if (quest.PreviousQuests.Count > 1)
+            {
+                if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.All)
+                    ImGui.Text("Requires all:");
+                else if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne)
+                    ImGui.Text("Requires one:");
+            }
+
+            foreach (var q in quest.PreviousQuests)
+            {
+                var qInfo = _questData.GetQuestInfo(q);
+                var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
+                if (!_questRegistry.IsKnownQuest(qInfo.QuestId))
+                    iconColor = ImGuiColors.DalamudGrey;
+
+                _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
+
+                DrawQuestUnlocks(qInfo, counter + 1);
+            }
+        }
+
+        if (counter == 0 && quest.QuestLocks.Count > 0)
+        {
+            ImGui.Separator();
+            if (quest.QuestLocks.Count > 1)
+            {
+                if (quest.QuestLockJoin == QuestInfo.QuestJoin.All)
+                    ImGui.Text("Blocked by (if all completed):");
+                else if (quest.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne)
+                    ImGui.Text("Blocked by (if at least completed):");
+            }
+            else
+                ImGui.Text("Blocked by (if completed):");
+
+            foreach (var q in quest.QuestLocks)
+            {
+                var qInfo = _questData.GetQuestInfo(q);
+                var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
+                if (!_questRegistry.IsKnownQuest(qInfo.QuestId))
+                    iconColor = ImGuiColors.DalamudGrey;
+
+                _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
+            }
+        }
+
+        if (counter == 0 && quest.PreviousInstanceContent.Count > 0)
+        {
+            ImGui.Separator();
+            if (quest.PreviousInstanceContent.Count > 1)
+            {
+                if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.All)
+                    ImGui.Text("Requires all:");
+                else if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne)
+                    ImGui.Text("Requires one:");
+            }
+            else
+                ImGui.Text("Requires:");
+
+            foreach (var instanceId in quest.PreviousInstanceContent)
+            {
+                string instanceName = _territoryData.GetInstanceName(instanceId) ?? "?";
+                var (iconColor, icon) = UiUtils.GetInstanceStyle(instanceId);
+                _uiUtils.ChecklistItem(instanceName, iconColor, icon);
+            }
+        }
+
+        if (counter == 0 && quest.GrandCompany != GrandCompany.None)
+        {
+            ImGui.Separator();
+            string gcName = quest.GrandCompany switch
+            {
+                GrandCompany.Maelstrom => "Maelstrom",
+                GrandCompany.TwinAdder => "Twin Adder",
+                GrandCompany.ImmortalFlames => "Immortal Flames",
+                _ => "None",
+            };
+
+            GrandCompany currentGrandCompany = _gameFunctions.GetGrandCompany();
+            _uiUtils.ChecklistItem($"Grand Company: {gcName}", quest.GrandCompany == currentGrandCompany);
+        }
+
+        if (counter > 0)
+            ImGui.Unindent();
+    }
+
+    private static string FormatQuestUnlockName(QuestInfo questInfo)
+    {
+        if (questInfo.IsMainScenarioQuest)
+            return $"{questInfo.Name} ({questInfo.QuestId}, MSQ)";
+        else
+            return $"{questInfo.Name} ({questInfo.QuestId})";
+    }
+}
index 5a528a1e6a3db680c086305955f887792cb74be1..7cdfb388a6aaa4501321b5d9cff0bcdced812c78 100644 (file)
@@ -25,15 +25,25 @@ internal sealed class QuickAccessButtonsComponent
     private readonly QuestRegistry _questRegistry;
     private readonly NavmeshIpc _navmeshIpc;
     private readonly QuestValidationWindow _questValidationWindow;
+    private readonly JournalProgressWindow _journalProgressWindow;
     private readonly IClientState _clientState;
     private readonly ICondition _condition;
     private readonly IFramework _framework;
     private readonly ICommandManager _commandManager;
 
-    public QuickAccessButtonsComponent(QuestController questController, MovementController movementController,
-        GameUiController gameUiController, GameFunctions gameFunctions, ChatFunctions chatFunctions,
-        QuestRegistry questRegistry, NavmeshIpc navmeshIpc, QuestValidationWindow questValidationWindow,
-        IClientState clientState, ICondition condition, IFramework framework, ICommandManager commandManager)
+    public QuickAccessButtonsComponent(QuestController questController,
+        MovementController movementController,
+        GameUiController gameUiController,
+        GameFunctions gameFunctions,
+        ChatFunctions chatFunctions,
+        QuestRegistry questRegistry,
+        NavmeshIpc navmeshIpc,
+        QuestValidationWindow questValidationWindow,
+        JournalProgressWindow journalProgressWindow,
+        IClientState clientState,
+        ICondition condition,
+        IFramework framework,
+        ICommandManager commandManager)
     {
         _questController = questController;
         _movementController = movementController;
@@ -43,6 +53,7 @@ internal sealed class QuickAccessButtonsComponent
         _questRegistry = questRegistry;
         _navmeshIpc = navmeshIpc;
         _questValidationWindow = questValidationWindow;
+        _journalProgressWindow = journalProgressWindow;
         _clientState = clientState;
         _condition = condition;
         _framework = framework;
@@ -80,6 +91,11 @@ internal sealed class QuickAccessButtonsComponent
         if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.RedoAlt,"Reload Data"))
             Reload();
 
+        ImGui.SameLine();
+        if (ImGuiComponents.IconButton(FontAwesomeIcon.ChartColumn))
+            _journalProgressWindow.IsOpen = true;
+
+
         if (_questRegistry.ValidationIssueCount > 0)
         {
             ImGui.SameLine();
index d9e0f8709954805b57f4e2cd62a23fdcba16ce2d..6f4207860c5493131306d6dc8ace6af29fafc590 100644 (file)
@@ -20,6 +20,7 @@ using Questionable.Controller;
 using Questionable.Data;
 using Questionable.Model;
 using Questionable.Model.V1;
+using Questionable.Windows.QuestComponents;
 
 namespace Questionable.Windows;
 
@@ -36,6 +37,7 @@ internal sealed class QuestSelectionWindow : LWindow
     private readonly TerritoryData _territoryData;
     private readonly IClientState _clientState;
     private readonly UiUtils _uiUtils;
+    private readonly QuestTooltipComponent _questTooltipComponent;
 
     private List<QuestInfo> _quests = [];
     private List<QuestInfo> _offeredQuests = [];
@@ -43,7 +45,8 @@ internal sealed class QuestSelectionWindow : LWindow
 
     public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions,
         QuestController questController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface,
-        TerritoryData territoryData, IClientState clientState, UiUtils uiUtils)
+        TerritoryData territoryData, IClientState clientState, UiUtils uiUtils,
+        QuestTooltipComponent questTooltipComponent)
         : base($"Quest Selection{WindowId}")
     {
         _questData = questData;
@@ -56,6 +59,7 @@ internal sealed class QuestSelectionWindow : LWindow
         _territoryData = territoryData;
         _clientState = clientState;
         _uiUtils = uiUtils;
+        _questTooltipComponent = questTooltipComponent;
 
         Size = new Vector2(500, 200);
         SizeCondition = ImGuiCond.Once;
@@ -169,7 +173,7 @@ internal sealed class QuestSelectionWindow : LWindow
             if (ImGui.TableNextColumn())
             {
                 ImGui.AlignTextToFramePadding();
-                var (color, icon, tooltipText) = _uiUtils.GetQuestStyle(quest.QuestId);
+                var (color, icon, _) = _uiUtils.GetQuestStyle(quest.QuestId);
                 using (var _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
                 {
                     if (isKnownQuest)
@@ -179,32 +183,7 @@ internal sealed class QuestSelectionWindow : LWindow
                 }
 
                 if (ImGui.IsItemHovered())
-                {
-                    using var tooltip = ImRaii.Tooltip();
-                    if (tooltip)
-                    {
-                        ImGui.TextColored(color, tooltipText);
-                        if (quest.IsRepeatable)
-                        {
-                            ImGui.SameLine();
-                            ImGui.TextUnformatted("Repeatable");
-                        }
-
-                        if (quest.CompletesInstantly)
-                        {
-                            ImGui.SameLine();
-                            ImGui.TextUnformatted("Instant");
-                        }
-
-                        if (!isKnownQuest)
-                        {
-                            ImGui.SameLine();
-                            ImGui.TextUnformatted("NoQuestPath");
-                        }
-
-                        DrawQuestUnlocks(quest, 0);
-                    }
-                }
+                    _questTooltipComponent.Draw(quest);
             }
 
             if (ImGui.TableNextColumn())
@@ -272,113 +251,4 @@ internal sealed class QuestSelectionWindow : LWindow
         ImGui.SetClipboardText(fileName);
         _chatGui.Print($"Copied '{fileName}' to clipboard");
     }
-
-    private void DrawQuestUnlocks(QuestInfo quest, int counter)
-    {
-        if (counter >= 10)
-            return;
-
-        if (counter != 0 && quest.IsMainScenarioQuest)
-            return;
-
-        if (counter > 0)
-            ImGui.Indent();
-
-        if (quest.PreviousQuests.Count > 0)
-        {
-            if (counter == 0)
-                ImGui.Separator();
-
-            if (quest.PreviousQuests.Count > 1)
-            {
-                if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.All)
-                    ImGui.Text("Requires all:");
-                else if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne)
-                    ImGui.Text("Requires one:");
-            }
-
-            foreach (var q in quest.PreviousQuests)
-            {
-                var qInfo = _questData.GetQuestInfo(q);
-                var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
-                if (!_questRegistry.IsKnownQuest(qInfo.QuestId))
-                    iconColor = ImGuiColors.DalamudGrey;
-
-                _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
-
-                DrawQuestUnlocks(qInfo, counter + 1);
-            }
-        }
-
-        if (counter == 0 && quest.QuestLocks.Count > 0)
-        {
-            ImGui.Separator();
-            if (quest.QuestLocks.Count > 1)
-            {
-                if (quest.QuestLockJoin == QuestInfo.QuestJoin.All)
-                    ImGui.Text("Blocked by (if all completed):");
-                else if (quest.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne)
-                    ImGui.Text("Blocked by (if at least completed):");
-            }
-            else
-                ImGui.Text("Blocked by (if completed):");
-
-            foreach (var q in quest.QuestLocks)
-            {
-                var qInfo = _questData.GetQuestInfo(q);
-                var (iconColor, icon, _) = _uiUtils.GetQuestStyle(q);
-                if (!_questRegistry.IsKnownQuest(qInfo.QuestId))
-                    iconColor = ImGuiColors.DalamudGrey;
-
-                _uiUtils.ChecklistItem(FormatQuestUnlockName(qInfo), iconColor, icon);
-            }
-        }
-
-        if (counter == 0 && quest.PreviousInstanceContent.Count > 0)
-        {
-            ImGui.Separator();
-            if (quest.PreviousInstanceContent.Count > 1)
-            {
-                if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.All)
-                    ImGui.Text("Requires all:");
-                else if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne)
-                    ImGui.Text("Requires one:");
-            }
-            else
-                ImGui.Text("Requires:");
-
-            foreach (var instanceId in quest.PreviousInstanceContent)
-            {
-                string instanceName = _territoryData.GetInstanceName(instanceId) ?? "?";
-                var (iconColor, icon) = UiUtils.GetInstanceStyle(instanceId);
-                _uiUtils.ChecklistItem(instanceName, iconColor, icon);
-            }
-        }
-
-        if (counter == 0 && quest.GrandCompany != GrandCompany.None)
-        {
-            ImGui.Separator();
-            string gcName = quest.GrandCompany switch
-            {
-                GrandCompany.Maelstrom => "Maelstrom",
-                GrandCompany.TwinAdder => "Twin Adder",
-                GrandCompany.ImmortalFlames => "Immortal Flames",
-                _ => "None",
-            };
-
-            GrandCompany currentGrandCompany = _gameFunctions.GetGrandCompany();
-            _uiUtils.ChecklistItem($"Grand Company: {gcName}", quest.GrandCompany == currentGrandCompany);
-        }
-
-        if (counter > 0)
-            ImGui.Unindent();
-    }
-
-    private static string FormatQuestUnlockName(QuestInfo questInfo)
-    {
-        if (questInfo.IsMainScenarioQuest)
-            return $"{questInfo.Name} ({questInfo.QuestId}, MSQ)";
-        else
-            return $"{questInfo.Name} ({questInfo.QuestId})";
-    }
 }