439 lines
13 KiB
C#
439 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using Dalamud.Plugin;
|
|
using Microsoft.Extensions.Logging;
|
|
using Questionable.Data;
|
|
using Questionable.Functions;
|
|
using Questionable.Model;
|
|
using Questionable.Model.Questing;
|
|
|
|
namespace Questionable.Windows.JournalComponents;
|
|
|
|
internal sealed class DutyJournalComponent
|
|
{
|
|
private enum RowType : byte
|
|
{
|
|
Category,
|
|
Duty
|
|
}
|
|
|
|
private readonly record struct FlatRow(RowType Type, int GroupIndex, DutyInfo? Duty, string Label, DutyCounts Counts);
|
|
|
|
private sealed record DutyCategory(string Name, List<DutyInfo> Duties);
|
|
|
|
private sealed record DutyCounts(int Unlocked, int Completed, int CompletionTrackable, int Total)
|
|
{
|
|
public string FormattedUnlocked { get; } = FormatPair(Unlocked, Total);
|
|
|
|
public string FormattedCompleted { get; } = FormatPair(Completed, CompletionTrackable);
|
|
|
|
public bool IsUnlockedComplete
|
|
{
|
|
get
|
|
{
|
|
if (Unlocked == Total)
|
|
{
|
|
return Total > 0;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool IsCompletedComplete
|
|
{
|
|
get
|
|
{
|
|
if (Completed == CompletionTrackable)
|
|
{
|
|
return CompletionTrackable > 0;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static readonly int PadWidth = 9999.ToString(CultureInfo.CurrentCulture).Length;
|
|
|
|
public DutyCounts()
|
|
: this(0, 0, 0, 0)
|
|
{
|
|
}
|
|
|
|
private static string FormatPair(int n, int d)
|
|
{
|
|
if (d != 0)
|
|
{
|
|
return n.ToString(CultureInfo.CurrentCulture).PadLeft(PadWidth) + " / " + d.ToString(CultureInfo.CurrentCulture).PadLeft(PadWidth);
|
|
}
|
|
return "-".PadLeft(PadWidth) + " / " + "-".PadLeft(PadWidth);
|
|
}
|
|
|
|
[CompilerGenerated]
|
|
private DutyCounts(DutyCounts original)
|
|
{
|
|
Unlocked = original.Unlocked;
|
|
Completed = original.Completed;
|
|
CompletionTrackable = original.CompletionTrackable;
|
|
Total = original.Total;
|
|
FormattedUnlocked = original.FormattedUnlocked;
|
|
FormattedCompleted = original.FormattedCompleted;
|
|
}
|
|
}
|
|
|
|
private readonly DutyUnlockData _dutyUnlockData;
|
|
|
|
private readonly QuestData _questData;
|
|
|
|
private readonly QuestFunctions _questFunctions;
|
|
|
|
private readonly UiUtils _uiUtils;
|
|
|
|
private readonly IDalamudPluginInterface _pluginInterface;
|
|
|
|
private readonly ILogger<DutyJournalComponent> _logger;
|
|
|
|
private List<DutyCategory> _filteredCategories = new List<DutyCategory>();
|
|
|
|
private Dictionary<string, DutyCounts> _categoryCounts = new Dictionary<string, DutyCounts>();
|
|
|
|
private List<FlatRow> _flatRows = new List<FlatRow>();
|
|
|
|
private readonly HashSet<string> _expandedGroups = new HashSet<string>();
|
|
|
|
private string _searchText = string.Empty;
|
|
|
|
private bool _showLockedOnly;
|
|
|
|
private bool _showUnlockedOnly;
|
|
|
|
public DutyJournalComponent(DutyUnlockData dutyUnlockData, QuestData questData, QuestFunctions questFunctions, UiUtils uiUtils, IDalamudPluginInterface pluginInterface, ILogger<DutyJournalComponent> logger)
|
|
{
|
|
_dutyUnlockData = dutyUnlockData;
|
|
_questData = questData;
|
|
_questFunctions = questFunctions;
|
|
_uiUtils = uiUtils;
|
|
_pluginInterface = pluginInterface;
|
|
_logger = logger;
|
|
}
|
|
|
|
public unsafe void DrawDuties()
|
|
{
|
|
DrawFilterControls();
|
|
ImGui.Separator();
|
|
if (_flatRows.Count > 0)
|
|
{
|
|
float x;
|
|
using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
|
|
{
|
|
x = ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
|
|
}
|
|
float fontGlobalScale = ImGui.GetIO().FontGlobalScale;
|
|
using ImRaii.TableDisposable tableDisposable = UiThemeUtils.BeginThemedTable("Duties", 3, ImGuiTableFlags.NoSavedSettings | ImGuiTableFlags.ScrollY);
|
|
if (!tableDisposable)
|
|
{
|
|
return;
|
|
}
|
|
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide);
|
|
ImGui.TableSetupColumn("Unlocked", ImGuiTableColumnFlags.WidthFixed, 120f * fontGlobalScale);
|
|
ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120f * fontGlobalScale);
|
|
ImGui.TableHeadersRow();
|
|
float textLineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing();
|
|
bool flag = false;
|
|
ImGuiListClipper* intPtr = ImGuiNative.ImGuiListClipper();
|
|
ImGuiNative.Begin(intPtr, _flatRows.Count, textLineHeightWithSpacing);
|
|
ImGuiListClipperPtr imGuiListClipperPtr = intPtr;
|
|
try
|
|
{
|
|
while (imGuiListClipperPtr.Step())
|
|
{
|
|
for (int i = imGuiListClipperPtr.DisplayStart; i < imGuiListClipperPtr.DisplayEnd; i++)
|
|
{
|
|
if (DrawFlatRow(_flatRows[i], x))
|
|
{
|
|
flag = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
imGuiListClipperPtr.Destroy();
|
|
}
|
|
if (flag)
|
|
{
|
|
RebuildFlatRows();
|
|
}
|
|
return;
|
|
}
|
|
UiThemeUtils.EmptyState(FontAwesomeIcon.Dungeon, "No duties match your search.");
|
|
}
|
|
|
|
private bool DrawFlatRow(FlatRow row, float statusIconSpacing)
|
|
{
|
|
ImGui.TableNextRow();
|
|
ImGui.TableNextColumn();
|
|
bool result = false;
|
|
if (row.Type == RowType.Duty)
|
|
{
|
|
float indentSpacing = ImGui.GetStyle().IndentSpacing;
|
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + indentSpacing);
|
|
string text = $"Lv{row.Duty.Level} {row.Duty.Name}";
|
|
if (row.Duty.ItemLevel > 0)
|
|
{
|
|
text += $" (i{row.Duty.ItemLevel})";
|
|
}
|
|
ImGui.TreeNodeEx(text, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.SpanFullWidth);
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
DrawDutyTooltip(row.Duty);
|
|
}
|
|
ImGui.TableNextColumn();
|
|
DrawDutyStatus(row.Duty.IsUnlocked, statusIconSpacing);
|
|
ImGui.TableNextColumn();
|
|
DrawDutyStatus(row.Duty.IsCompleted, statusIconSpacing);
|
|
}
|
|
else
|
|
{
|
|
bool flag = _expandedGroups.Contains(row.Label);
|
|
UiThemeUtils.DrawTrackerGroupCaret(_pluginInterface.UiBuilder.IconFontFixedWidthHandle, flag);
|
|
ImGui.SameLine();
|
|
ImU8String label = new ImU8String(5, 2);
|
|
label.AppendFormatted(row.Label);
|
|
label.AppendLiteral("##grp");
|
|
label.AppendFormatted(row.GroupIndex);
|
|
if (ImGui.Selectable(label))
|
|
{
|
|
if (flag)
|
|
{
|
|
_expandedGroups.Remove(row.Label);
|
|
}
|
|
else
|
|
{
|
|
_expandedGroups.Add(row.Label);
|
|
}
|
|
result = true;
|
|
}
|
|
ImGui.TableNextColumn();
|
|
UiThemeUtils.DrawTrackerCount(row.Counts.FormattedUnlocked, row.Counts.IsUnlockedComplete, row.Counts.Total == 0);
|
|
ImGui.TableNextColumn();
|
|
UiThemeUtils.DrawTrackerCount(row.Counts.FormattedCompleted, row.Counts.IsCompletedComplete, row.Counts.CompletionTrackable == 0);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private void DrawFilterControls()
|
|
{
|
|
float x = ImGui.GetStyle().ItemSpacing.X;
|
|
float num = 2f * ImGui.GetFrameHeight() + ImGui.CalcTextSize("Locked Only").X + ImGui.CalcTextSize("Unlocked Only").X + 5f * x;
|
|
if (UiThemeUtils.SearchInput("##DutySearch", ref _searchText, 0f - num, "Search duties..."))
|
|
{
|
|
UpdateFilter();
|
|
}
|
|
ImGui.SameLine();
|
|
if (UiThemeUtils.WrappedCheckbox("Locked Only", ref _showLockedOnly))
|
|
{
|
|
if (_showLockedOnly)
|
|
{
|
|
_showUnlockedOnly = false;
|
|
}
|
|
UpdateFilter();
|
|
}
|
|
ImGui.SameLine();
|
|
if (UiThemeUtils.WrappedCheckbox("Unlocked Only", ref _showUnlockedOnly))
|
|
{
|
|
if (_showUnlockedOnly)
|
|
{
|
|
_showLockedOnly = false;
|
|
}
|
|
UpdateFilter();
|
|
}
|
|
}
|
|
|
|
private void DrawDutyTooltip(DutyInfo duty)
|
|
{
|
|
using ImRaii.TooltipDisposable tooltipDisposable = ImRaii.Tooltip();
|
|
if (!tooltipDisposable.Alive)
|
|
{
|
|
return;
|
|
}
|
|
ImGui.TextColored(in UiThemeUtils.PrimaryTextColor, duty.Name);
|
|
ImU8String text = new ImU8String(12, 1);
|
|
text.AppendLiteral("Content ID: ");
|
|
text.AppendFormatted(duty.ContentFinderConditionId);
|
|
ImGui.TextColored(in UiThemeUtils.DimTextColor, text);
|
|
ImGui.Separator();
|
|
ImU8String text2 = new ImU8String(16, 1);
|
|
text2.AppendLiteral("Level Required: ");
|
|
text2.AppendFormatted(duty.Level);
|
|
ImGui.Text(text2);
|
|
if (duty.ItemLevel > 0)
|
|
{
|
|
ImU8String text3 = new ImU8String(21, 1);
|
|
text3.AppendLiteral("Item Level Required: ");
|
|
text3.AppendFormatted(duty.ItemLevel);
|
|
ImGui.Text(text3);
|
|
}
|
|
ImU8String text4 = new ImU8String(6, 1);
|
|
text4.AppendLiteral("Type: ");
|
|
text4.AppendFormatted(duty.ContentTypeName);
|
|
ImGui.Text(text4);
|
|
if (duty.IsHighEndDuty)
|
|
{
|
|
ImGui.SameLine();
|
|
ImGui.TextColored(in UiThemeUtils.StatusActive, "(High-End)");
|
|
}
|
|
ImGui.Separator();
|
|
if (duty.IsUnlocked)
|
|
{
|
|
ImGui.TextColored(in UiThemeUtils.StatusComplete, "Status: Unlocked");
|
|
}
|
|
else
|
|
{
|
|
ImGui.TextColored(in UiThemeUtils.StatusLocked, "Status: Locked");
|
|
}
|
|
if (duty.UnlockQuests.Count > 0)
|
|
{
|
|
ImGui.Separator();
|
|
ImGui.Text("Unlock Quest(s):");
|
|
{
|
|
foreach (QuestId unlockQuest in duty.UnlockQuests)
|
|
{
|
|
if (_questData.TryGetQuestInfo(unlockQuest, out IQuestInfo questInfo))
|
|
{
|
|
var (color, icon, _) = _uiUtils.GetQuestStyle(unlockQuest);
|
|
_uiUtils.ChecklistItem($"{questInfo.Name} ({unlockQuest})", color, icon);
|
|
}
|
|
else
|
|
{
|
|
_uiUtils.ChecklistItem($"Unknown Quest ({unlockQuest})", UiThemeUtils.StatusUnobtainable, FontAwesomeIcon.Question);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
ImGui.Separator();
|
|
ImGui.TextColored(in UiThemeUtils.DimTextColor, "No unlock quest data available.");
|
|
}
|
|
|
|
private void DrawDutyStatus(bool? status, float statusIconSpacing)
|
|
{
|
|
float num = ImGui.GetColumnWidth() / 2f - statusIconSpacing;
|
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num);
|
|
if (!status.HasValue)
|
|
{
|
|
_uiUtils.ChecklistItem(string.Empty, UiThemeUtils.StatusUnobtainable, FontAwesomeIcon.Minus);
|
|
}
|
|
else if (status.Value)
|
|
{
|
|
_uiUtils.ChecklistItem(string.Empty, UiThemeUtils.StatusComplete, FontAwesomeIcon.Check);
|
|
}
|
|
else
|
|
{
|
|
_uiUtils.ChecklistItem(string.Empty, UiThemeUtils.StatusLocked, FontAwesomeIcon.Times);
|
|
}
|
|
}
|
|
|
|
private void RebuildFlatRows()
|
|
{
|
|
List<FlatRow> list = new List<FlatRow>();
|
|
foreach (DutyCategory filteredCategory in _filteredCategories)
|
|
{
|
|
DutyCounts valueOrDefault = _categoryCounts.GetValueOrDefault(filteredCategory.Name, new DutyCounts());
|
|
if (valueOrDefault.Total == 0)
|
|
{
|
|
continue;
|
|
}
|
|
list.Add(new FlatRow(RowType.Category, 0, null, filteredCategory.Name, valueOrDefault));
|
|
if (!_expandedGroups.Contains(filteredCategory.Name))
|
|
{
|
|
continue;
|
|
}
|
|
foreach (DutyInfo duty in filteredCategory.Duties)
|
|
{
|
|
list.Add(new FlatRow(RowType.Duty, 0, duty, string.Empty, new DutyCounts()));
|
|
}
|
|
}
|
|
_flatRows = list;
|
|
}
|
|
|
|
public void UpdateFilter()
|
|
{
|
|
List<DutyCategory> list = new List<DutyCategory>();
|
|
AddCategory(list, "Dungeons", _dutyUnlockData.GetDungeons());
|
|
AddCategory(list, "Hard Dungeons", _dutyUnlockData.GetHardDungeons());
|
|
AddCategory(list, "Guildhests", _dutyUnlockData.GetGuildhests());
|
|
AddCategory(list, "Trials", _dutyUnlockData.GetTrials());
|
|
AddCategory(list, "Hard Trials", _dutyUnlockData.GetHardTrials());
|
|
AddCategory(list, "Extreme Trials", _dutyUnlockData.GetExtremeTrials());
|
|
AddCategory(list, "Unreal Trials", _dutyUnlockData.GetUnrealTrials());
|
|
AddCategory(list, "Normal Raids", _dutyUnlockData.GetNormalRaids());
|
|
AddCategory(list, "Savage Raids", _dutyUnlockData.GetSavageRaids());
|
|
AddCategory(list, "Alliance Raids", _dutyUnlockData.GetAllianceRaids());
|
|
AddCategory(list, "Chaotic Alliance Raids", _dutyUnlockData.GetChaoticAllianceRaids());
|
|
AddCategory(list, "Ultimate Raids", _dutyUnlockData.GetUltimateRaids());
|
|
AddCategory(list, "Deep Dungeons", _dutyUnlockData.GetDeepDungeons());
|
|
AddCategory(list, "Variant Dungeons", _dutyUnlockData.GetVariantDungeons());
|
|
AddCategory(list, "Criterion Dungeons", _dutyUnlockData.GetCriterionDungeons());
|
|
AddCategory(list, "Criterion Savage", _dutyUnlockData.GetCriterionSavageDungeons());
|
|
_filteredCategories = list.Where((DutyCategory c) => c.Duties.Count > 0).ToList();
|
|
RefreshCounts();
|
|
RebuildFlatRows();
|
|
}
|
|
|
|
private void AddCategory(List<DutyCategory> categories, string name, IEnumerable<DutyInfo> duties)
|
|
{
|
|
List<DutyInfo> list = (from d in duties
|
|
where MatchesFilter(d)
|
|
orderby d.Level, d.ItemLevel, d.ContentFinderConditionId
|
|
select d).ToList();
|
|
if (list.Count > 0)
|
|
{
|
|
categories.Add(new DutyCategory(name, list));
|
|
}
|
|
}
|
|
|
|
private bool MatchesFilter(DutyInfo duty)
|
|
{
|
|
if (!string.IsNullOrEmpty(_searchText) && !duty.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
if (_showLockedOnly && duty.IsUnlocked)
|
|
{
|
|
return false;
|
|
}
|
|
if (_showUnlockedOnly && !duty.IsUnlocked)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void RefreshCounts()
|
|
{
|
|
_categoryCounts.Clear();
|
|
foreach (DutyCategory filteredCategory in _filteredCategories)
|
|
{
|
|
int unlocked = filteredCategory.Duties.Count((DutyInfo d) => d.IsUnlocked);
|
|
int completed = filteredCategory.Duties.Count((DutyInfo d) => d.IsCompleted == true);
|
|
int completionTrackable = filteredCategory.Duties.Count((DutyInfo d) => d.IsCompleted.HasValue);
|
|
int count = filteredCategory.Duties.Count;
|
|
_categoryCounts[filteredCategory.Name] = new DutyCounts(unlocked, completed, completionTrackable, count);
|
|
}
|
|
RebuildFlatRows();
|
|
}
|
|
|
|
public void ClearCounts(int type, int code)
|
|
{
|
|
foreach (string item in _categoryCounts.Keys.ToList())
|
|
{
|
|
_categoryCounts[item] = new DutyCounts(0, 0, 0, _categoryCounts[item].Total);
|
|
}
|
|
RebuildFlatRows();
|
|
}
|
|
}
|