diff --git a/Gearsetter.Test/ItemSortingTest.cs b/Gearsetter.Test/ItemSortingTest.cs index f6743e5..181c78b 100644 --- a/Gearsetter.Test/ItemSortingTest.cs +++ b/Gearsetter.Test/ItemSortingTest.cs @@ -30,7 +30,9 @@ public sealed class ItemSortingTest ClassJob = EClassJob.Marauder, EquipSlotCategory = EEquipSlotCategory.Ears, ItemUiCategory = 41, - Items = initialItemIds.Select(rowId => new EquipmentItem(items.GetRow(rowId)!, false)).ToList(), + Items = initialItemIds.Select(rowId => new EquipmentItem(items.GetRow(rowId)!, false)) + .Cast() + .ToList(), }; diff --git a/Gearsetter/GameData/EBaseParam.cs b/Gearsetter/GameData/EBaseParam.cs index 88d81d3..7ad9c6d 100644 --- a/Gearsetter/GameData/EBaseParam.cs +++ b/Gearsetter/GameData/EBaseParam.cs @@ -23,7 +23,7 @@ internal enum EBaseParam : byte DirectHit = 22, Determination = 44, SpellSpeed = 46, - SkillSpeed = 23, + SkillSpeed = 45, Craftsmanship = 70, Control = 71, diff --git a/Gearsetter/GameData/GameDataHolder.cs b/Gearsetter/GameData/GameDataHolder.cs index 6f649c3..2aa92c5 100644 --- a/Gearsetter/GameData/GameDataHolder.cs +++ b/Gearsetter/GameData/GameDataHolder.cs @@ -83,6 +83,9 @@ internal sealed class GameDataHolder .ThenBy(x => x.OrderMinor) .Select(x => (x.RowId, x.Name.ToString())) .ToList(); + Materias = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0 && Enum.IsDefined(typeof(EBaseParam), (byte)x.BaseParam.Row)) + .ToDictionary(x => x.RowId, x => new MateriaStat((EBaseParam)x.BaseParam.Row, x.Value)); _allItemLists = dataManager.GetExcelSheet()! @@ -117,7 +120,7 @@ internal sealed class GameDataHolder ClassJob = x.Key.ClassJob, EquipSlotCategory = x.Key.EquipSlotCategory, ItemUiCategory = x.Key.ItemUiCategory, - Items = x.ToList(), + Items = x.Cast().ToList(), }) .ToList() .AsReadOnly(); @@ -125,10 +128,10 @@ internal sealed class GameDataHolder UpdateAndSortLists(); } - public IReadOnlyList<(EClassJob ClassJob, string Name)> ClassJobNames { get; } public IReadOnlyList<(uint ItemUiCategory, string Name)> ItemUiCategoryNames { get; } public Dictionary PrimaryStats { get; } + public Dictionary Materias { get; set; } public Dictionary StatNames { get; } = new() { diff --git a/Gearsetter/Gearsetter.csproj b/Gearsetter/Gearsetter.csproj index d13af4c..5ef4279 100644 --- a/Gearsetter/Gearsetter.csproj +++ b/Gearsetter/Gearsetter.csproj @@ -1,7 +1,7 @@ net8.0-windows - 0.6 + 0.7 12 enable true diff --git a/Gearsetter/GearsetterPlugin.cs b/Gearsetter/GearsetterPlugin.cs index c12605c..79486de 100644 --- a/Gearsetter/GearsetterPlugin.cs +++ b/Gearsetter/GearsetterPlugin.cs @@ -17,6 +17,7 @@ using Gearsetter.Model; using Gearsetter.Windows; using Lumina.Excel.GeneratedSheets; using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; +using InventoryItem = FFXIVClientStructs.FFXIV.Client.Game.InventoryItem; namespace Gearsetter; @@ -149,7 +150,7 @@ public sealed class GearsetterPlugin : IDalamudPlugin } private unsafe bool HandleGearset(RaptureGearsetModule.GearsetEntry* gearset, - Dictionary<(uint ItemId, bool Hq), int> inventoryItems, byte? level) + Dictionary<(uint ItemId, bool Hq), List> inventoryItems, byte? level) { string name = GetGearsetName(gearset); if (name.Contains('_', StringComparison.Ordinal) || @@ -210,7 +211,8 @@ public sealed class GearsetterPlugin : IDalamudPlugin => Encoding.UTF8.GetString(gearset->Name, 0x2F).Split((char)0)[0]; private unsafe List HandleGearsetItem(string label, RaptureGearsetModule.GearsetEntry* gearset, - RaptureGearsetModule.GearsetItem[] gearsetItem, Dictionary<(uint ItemId, bool Hq), int> inventoryItems, + RaptureGearsetModule.GearsetItem[] gearsetItem, + Dictionary<(uint ItemId, bool Hq), List> inventoryItems, EEquipSlotCategory equipSlotCategory, byte? level) { EClassJob classJob = (EClassJob)gearset->ClassJob; @@ -230,7 +232,7 @@ public sealed class GearsetterPlugin : IDalamudPlugin return new List(); } - EquipmentItem?[] currentItems = gearsetItem.Select(x => new + BaseItem?[] currentItems = gearsetItem.Select(x => new { ItemId = x.ItemID % 1_000_000, Hq = x.ItemID > 1_000_000 @@ -253,35 +255,38 @@ public sealed class GearsetterPlugin : IDalamudPlugin if (level == null) level = GetLevel(classJob); - var bestItems = availableList.Items - .Where(x => x.Level <= level) - .SelectMany(x => - { - if (inventoryItems.TryGetValue((x.ItemId, x.Hq), out int count) && count > 0) - return Enumerable.Repeat(x, count); - else - return []; - }) - .Take(gearsetItem.Length) - .ToList(); - _pluginLog.Debug( - $"{equipSlotCategory}: {string.Join(" ", currentItems.Select(x => $"{x?.ItemId}|{x?.Hq}"))}"); - foreach (var currentItem in currentItems) + try { - var foundIndex = bestItems.FindIndex(x => - currentItem != null && currentItem.ItemId == x.ItemId && currentItem.Hq == x.Hq); - if (foundIndex >= 0) - bestItems.RemoveAt(foundIndex); - } + availableList.ApplyFromInventory(inventoryItems, true); - return bestItems - .Select(x => new SeString(new TextPayload($"{label}: ")) - .Append(SeString.CreateItemLink(x.ItemId, x.Hq))).ToList(); + var bestItems = availableList.Items + .Where(x => x.Level <= level) + .Where(x => x is Model.InventoryItem) + .Take(gearsetItem.Length) + .ToList(); + _pluginLog.Debug( + $"{equipSlotCategory}: {string.Join(" ", currentItems.Select(x => $"{x?.ItemId}|{x?.Hq}"))}"); + foreach (var currentItem in currentItems) + { + var foundIndex = bestItems.FindIndex(x => + currentItem != null && currentItem.ItemId == x.ItemId && currentItem.Hq == x.Hq); + if (foundIndex >= 0) + bestItems.RemoveAt(foundIndex); + } + + return bestItems + .Select(x => new SeString(new TextPayload($"{label}: ")) + .Append(SeString.CreateItemLink(x.ItemId, x.Hq))).ToList(); + } + finally + { + availableList.ClearFromInventory(); + } } private unsafe List HandleOffHand(RaptureGearsetModule.GearsetEntry* gearset, - Dictionary<(uint ItemId, bool Hq), int> inventoryItems, byte? level) + Dictionary<(uint ItemId, bool Hq), List> inventoryItems, byte? level) { var mainHand = gearset->ItemsSpan[0]; if (mainHand.ItemID == 0) @@ -300,9 +305,9 @@ public sealed class GearsetterPlugin : IDalamudPlugin private unsafe void ChangeGearset(uint commandId, SeString seString) => RaptureGearsetModule.Instance()->EquipGearset((byte)commandId); - public unsafe Dictionary<(uint ItemId, bool Hq), int> GetAllInventoryItems() + internal unsafe Dictionary<(uint ItemId, bool Hq), List> GetAllInventoryItems() { - Dictionary<(uint, bool), int> inventoryItems = new(); + Dictionary<(uint, bool), List> inventoryItems = new(); InventoryManager* inventoryManager = InventoryManager.Instance(); foreach (var inventoryType in _gameDataHolder.DefaultInventoryTypes) { @@ -313,10 +318,27 @@ public sealed class GearsetterPlugin : IDalamudPlugin if (item != null && item->ItemID != 0) { var key = (item->ItemID, item->Flags.HasFlag(InventoryItem.ItemFlags.HQ)); - if (inventoryItems.TryGetValue(key, out var value)) - inventoryItems[key] = value + 1; - else - inventoryItems[key] = 1; + if (!inventoryItems.TryGetValue(key, out var list)) + { + list = new List(); + inventoryItems[key] = list; + } + + + byte materiaCount = item->GetMateriaCount(); + var materias = Enumerable.Range(0, materiaCount) + .Select(slot => + { + if (_gameDataHolder.Materias.TryGetValue(item->GetMateriaId((byte)slot), + out MateriaStat? value)) + return (value, item->GetMateriaGrade((byte)slot)); + else + return null; + }) + .Where(x => x != null) + .Select(x => x!.Value) + .ToList(); + list.Add(new MateriaStats(materias)); } } } diff --git a/Gearsetter/Model/BaseItem.cs b/Gearsetter/Model/BaseItem.cs new file mode 100644 index 0000000..a9c1f02 --- /dev/null +++ b/Gearsetter/Model/BaseItem.cs @@ -0,0 +1,59 @@ +using Gearsetter.GameData; +using Lumina.Excel.GeneratedSheets; + +namespace Gearsetter.Model; + +internal abstract record BaseItem(Item Item, bool Hq, MateriaStats? MateriaStats = null) +{ + public Item Item { get; } = Item; + public uint ItemId { get; } = Item.RowId; + public bool Hq { get; } = Hq; + public bool CanBeHq { get; } = Item.CanBeHq; + public string Name { get; } = Item.Name.ToString(); + public byte Level { get; } = Item.LevelEquip; + public uint ItemLevel { get; } = Item.LevelItem.Row; + public byte Rarity { get; } = Item.Rarity; + public EEquipSlotCategory EquipSlotCategory { get; } = (EEquipSlotCategory)Item.EquipSlotCategory.Row; + public uint ItemUiCategory { get; } = Item.ItemUICategory.Row; + public abstract EClassJob ClassJob { get; init; } + public EquipmentStats Stats { get; } = new(Item, Hq, MateriaStats); + + public int PrimaryStat { get; init; } = -1; + + public int Damage + { + get + { + if (ClassJob.DealsMagicDamage()) + return Item.DamageMag + Stats.Get(EBaseParam.DamageMag); + else if (ClassJob.DealsPhysicalDamage()) + return Item.DamagePhys + Stats.Get(EBaseParam.DamagePhys); + else + return 0; + } + } + + public bool HasAnyStat(params EBaseParam[] substats) + { + foreach (EBaseParam substat in substats) + { + if (Stats.Get(substat) > 0) + return true; + } + + return false; + } + + public bool IsCombatRelicWithoutSubstats() + { + return Rarity == 4 + && EquipSlotCategory is EEquipSlotCategory.OneHandedMainHand + or EEquipSlotCategory.TwoHandedMainHand + or EEquipSlotCategory.Shield + && !ClassJob.IsCrafter() + && !ClassJob.IsGatherer() + && !HasAnyStat(EBaseParam.Crit, + EBaseParam.DirectHit, EBaseParam.Determination, EBaseParam.SkillSpeed, + EBaseParam.SpellSpeed, EBaseParam.Tenacity); + } +} diff --git a/Gearsetter/Model/EquipmentItem.cs b/Gearsetter/Model/EquipmentItem.cs index f690570..d2cb8b1 100644 --- a/Gearsetter/Model/EquipmentItem.cs +++ b/Gearsetter/Model/EquipmentItem.cs @@ -3,57 +3,7 @@ using Lumina.Excel.GeneratedSheets; namespace Gearsetter.Model; -internal sealed record EquipmentItem(Item Item, bool Hq) +internal sealed record EquipmentItem(Item Item, bool Hq) : BaseItem(Item, Hq) { - public Item Item { get; } = Item; - public uint ItemId { get; } = Item.RowId; - public bool Hq { get; } = Hq; - public bool CanBeHq { get; } = Item.CanBeHq; - public string Name { get; } = Item.Name.ToString(); - public byte Level { get; } = Item.LevelEquip; - public uint ItemLevel { get; } = Item.LevelItem.Row; - public byte Rarity { get; } = Item.Rarity; - public EEquipSlotCategory EquipSlotCategory { get; } = (EEquipSlotCategory)Item.EquipSlotCategory.Row; - public uint ItemUiCategory { get; } = Item.ItemUICategory.Row; - public EClassJob ClassJob { get; init; } = EClassJob.Adventurer; - public EquipmentStats Stats { get; } = new(Item, Hq); - - public int PrimaryStat { get; init; } = -1; - - public int Damage - { - get - { - if (ClassJob.DealsMagicDamage()) - return Item.DamageMag + Stats.Get(EBaseParam.DamageMag); - else if (ClassJob.DealsPhysicalDamage()) - return Item.DamagePhys + Stats.Get(EBaseParam.DamagePhys); - else - return 0; - } - } - - public bool HasAnyStat(params EBaseParam[] substats) - { - foreach (EBaseParam substat in substats) - { - if (Stats.Get(substat) > 0) - return true; - } - - return false; - } - - public bool IsCombatRelicWithoutSubstats() - { - return Rarity == 4 - && EquipSlotCategory is EEquipSlotCategory.OneHandedMainHand - or EEquipSlotCategory.TwoHandedMainHand - or EEquipSlotCategory.Shield - && !ClassJob.IsCrafter() - && !ClassJob.IsGatherer() - && !HasAnyStat(EBaseParam.Crit, - EBaseParam.DirectHit, EBaseParam.Determination, EBaseParam.SkillSpeed, - EBaseParam.SpellSpeed, EBaseParam.Tenacity); - } + public override EClassJob ClassJob { get; init; } = EClassJob.Adventurer; } diff --git a/Gearsetter/Model/EquipmentStats.cs b/Gearsetter/Model/EquipmentStats.cs index 3b36aea..72860f2 100644 --- a/Gearsetter/Model/EquipmentStats.cs +++ b/Gearsetter/Model/EquipmentStats.cs @@ -7,28 +7,52 @@ namespace Gearsetter.Model; internal sealed class EquipmentStats { - private readonly Dictionary _values; + private readonly Dictionary _equipmentValues; + private readonly Dictionary _materiaValues; - public EquipmentStats(Item item, bool hq) + public EquipmentStats(Item item, bool hq, MateriaStats? materiaStats) { - _values = item.UnkData59.Where(x => x.BaseParam > 0) + _equipmentValues = item.UnkData59.Where(x => x.BaseParam > 0) .ToDictionary(x => (EBaseParam)x.BaseParam, x => x.BaseParamValue); if (hq) { foreach (var hqstat in item.UnkData73.Select(x => ((EBaseParam)x.BaseParamSpecial, x.BaseParamValueSpecial))) { - if (_values.TryGetValue(hqstat.Item1, out var stat)) - _values[hqstat.Item1] = (short)(stat + hqstat.BaseParamValueSpecial); + if (_equipmentValues.TryGetValue(hqstat.Item1, out var stat)) + _equipmentValues[hqstat.Item1] = (short)(stat + hqstat.BaseParamValueSpecial); else - _values[hqstat.Item1] = hqstat.BaseParamValueSpecial; + _equipmentValues[hqstat.Item1] = hqstat.BaseParamValueSpecial; + } + } + + _materiaValues = new(); + if (materiaStats != null) + { + foreach (var materiaStat in materiaStats.Values) + { + if (_materiaValues.TryGetValue(materiaStat.Key, out var stat)) + _materiaValues[materiaStat.Key] = (short)(stat + materiaStat.Value); + else + _materiaValues[materiaStat.Key] = materiaStat.Value; } } } public short Get(EBaseParam param) { - _values.TryGetValue(param, out short v); + return (short)(GetEquipment(param) + GetMateria(param)); + } + + public short GetEquipment(EBaseParam param) + { + _equipmentValues.TryGetValue(param, out short v); + return v; + } + + public short GetMateria(EBaseParam param) + { + _materiaValues.TryGetValue(param, out short v); return v; } } diff --git a/Gearsetter/Model/InventoryItem.cs b/Gearsetter/Model/InventoryItem.cs new file mode 100644 index 0000000..ba1e2e3 --- /dev/null +++ b/Gearsetter/Model/InventoryItem.cs @@ -0,0 +1,9 @@ +using Gearsetter.GameData; +using Lumina.Excel.GeneratedSheets; + +namespace Gearsetter.Model; + +internal sealed record InventoryItem(Item Item, bool Hq, MateriaStats MateriaStats, EClassJob ClassJob) + : BaseItem(Item, Hq, MateriaStats) +{ +} diff --git a/Gearsetter/Model/ItemList.cs b/Gearsetter/Model/ItemList.cs index 61a31f0..4bdcbc6 100644 --- a/Gearsetter/Model/ItemList.cs +++ b/Gearsetter/Model/ItemList.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using Dalamud.Logging; using Gearsetter.GameData; namespace Gearsetter.Model; @@ -20,7 +19,8 @@ internal sealed class ItemList public required EClassJob ClassJob { get; init; } public required EEquipSlotCategory EquipSlotCategory { get; init; } public required uint ItemUiCategory { get; init; } - public required List Items { get; set; } + public required List Items { get; set; } + public EBaseParam PrimaryStat { get; set; } public IReadOnlyList SubstatPriorities { get; set; } = new List(); public void Sort() @@ -30,11 +30,11 @@ internal sealed class ItemList .ToList(); var defaultItems = Items .Except(preferredItems) - .OrderDescending(new EquipmentItemComparer(SubstatPriorities)) + .OrderDescending(new ItemComparer(SubstatPriorities)) .ToList(); // insert the preferred items - foreach (EquipmentItem preferredItem in preferredItems) + foreach (BaseItem preferredItem in preferredItems) { int level = PreferredItems[preferredItem.ItemId]; int index = defaultItems.FindIndex(x => x.Level < level); @@ -68,15 +68,48 @@ internal sealed class ItemList if (primaryStats.TryGetValue(ClassJob, out EBaseParam primaryStat)) { + PrimaryStat = primaryStat; Items = Items + .Where(x => x is EquipmentItem) + .Cast() .Select(x => x with { PrimaryStat = x.Stats.Get(primaryStat) }) + .Cast() .ToList(); } } - private sealed class EquipmentItemComparer(IReadOnlyList substatPriorities) : IComparer + public void ApplyFromInventory(Dictionary<(uint ItemId, bool Hq), List> inventoryItems, + bool includeWithoutMateria) { - public int Compare(EquipmentItem? a, EquipmentItem? b) + foreach (var inventoryItem in inventoryItems) + { + var basicItem = Items.SingleOrDefault(x => + x.ItemId == inventoryItem.Key.ItemId && x.Hq == inventoryItem.Key.Hq); + if (basicItem == null) + continue; + + foreach (var materias in inventoryItem.Value) + { + if (includeWithoutMateria || materias.Values.Count > 0) + Items.Add( + new InventoryItem(basicItem.Item, basicItem.Hq, materias, basicItem.ClassJob) + { + PrimaryStat = basicItem.Stats.Get(PrimaryStat) + }); + } + } + + Sort(); + } + + public void ClearFromInventory() + { + Items.RemoveAll(x => x is InventoryItem); + } + + private sealed class ItemComparer(IReadOnlyList substatPriorities) : IComparer + { + public int Compare(BaseItem? a, BaseItem? b) { ArgumentNullException.ThrowIfNull(a); ArgumentNullException.ThrowIfNull(b); diff --git a/Gearsetter/Model/MateriaStat.cs b/Gearsetter/Model/MateriaStat.cs new file mode 100644 index 0000000..e0dd6b5 --- /dev/null +++ b/Gearsetter/Model/MateriaStat.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Linq; +using Gearsetter.GameData; + +namespace Gearsetter.Model; + +internal sealed class MateriaStat(EBaseParam baseParam, short[] values) +{ + public EBaseParam BaseParam { get; } = baseParam; + public List Values { get; } = values.ToList(); +} diff --git a/Gearsetter/Model/MateriaStats.cs b/Gearsetter/Model/MateriaStats.cs new file mode 100644 index 0000000..e160c5b --- /dev/null +++ b/Gearsetter/Model/MateriaStats.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Gearsetter.GameData; + +namespace Gearsetter.Model; + +internal sealed class MateriaStats +{ + private readonly List<(EBaseParam, short)> _list; + + public MateriaStats(IEnumerable<(MateriaStat Stat, byte Grade)> materias) + { + foreach (var materia in materias) + { + if (Values.TryGetValue(materia.Stat.BaseParam, out short value)) + Values[materia.Stat.BaseParam] = (short)(value + materia.Stat.Values[materia.Grade]); + else + Values[materia.Stat.BaseParam] = materia.Stat.Values[materia.Grade]; + + ++Count; + } + + _list = Enum.GetValues() + .Select(x => (x, Values.GetValueOrDefault(x, (short)0))) + .Where(x => x.Item2 > 0) + .ToList(); + } + + public Dictionary Values { get; } = new(); + public byte Count { get; } + + public override bool Equals(object? obj) + { + if (obj is not MateriaStats other) + return false; + + return _list == other._list; + } + + public override int GetHashCode() + { + int hash = 19; + hash = hash * 31 + Count; + foreach (var item in _list) + hash = hash * 31 + item.GetHashCode(); + return hash; + } +} diff --git a/Gearsetter/Windows/EquipmentBrowserWindow.cs b/Gearsetter/Windows/EquipmentBrowserWindow.cs index e11355b..85a2b52 100644 --- a/Gearsetter/Windows/EquipmentBrowserWindow.cs +++ b/Gearsetter/Windows/EquipmentBrowserWindow.cs @@ -9,6 +9,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Gearsetter.GameData; +using Gearsetter.Model; using ImGuiNET; namespace Gearsetter.Windows; @@ -109,102 +110,117 @@ internal sealed class EquipmentBrowserWindow : Window ImGui.SameLine(); ImGui.Checkbox("Hide normal quality items", ref _hideNormalQualityItems); - Dictionary<(uint, bool), int>? ownedItems = null; - if (_onlyShowOwnedItems) - ownedItems = _plugin.GetAllInventoryItems(); - - byte maxLevel = byte.MaxValue; - if (_onlyShowEquippableItems) - maxLevel = _plugin.GetLevel(_selectedClassJob); - - bool includeDamage = _selectedEquipmentCategory is EEquipSlotCategory.OneHandedMainHand - or EEquipSlotCategory.Shield - or EEquipSlotCategory.TwoHandedMainHand - && !itemList.ClassJob.IsCrafter() - && !itemList.ClassJob.IsGatherer(); - if (ImGui.BeginTable("ItemList", 2 + (includeDamage ? 1 : 0) + itemList.SubstatPriorities.Count, - ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) + Dictionary<(uint ItemId, bool Hq), List> ownedItems = _plugin.GetAllInventoryItems(); + try { - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.None, 300); - ImGui.TableSetupColumn("Level", ImGuiTableColumnFlags.WidthFixed, 50); - if (includeDamage) - ImGui.TableSetupColumn("Damage", ImGuiTableColumnFlags.WidthFixed, 50); - foreach (var substat in itemList.SubstatPriorities) - ImGui.TableSetupColumn(_dataHolder.StatNames[substat], ImGuiTableColumnFlags.WidthFixed, 50); + itemList.ApplyFromInventory(ownedItems, _onlyShowOwnedItems); - ImGui.TableHeadersRow(); + byte maxLevel = byte.MaxValue; + if (_onlyShowEquippableItems) + maxLevel = _plugin.GetLevel(_selectedClassJob); - foreach (var item in itemList.Items) + bool includeDamage = _selectedEquipmentCategory is EEquipSlotCategory.OneHandedMainHand + or EEquipSlotCategory.Shield + or EEquipSlotCategory.TwoHandedMainHand + && !itemList.ClassJob.IsCrafter() + && !itemList.ClassJob.IsGatherer(); + if (ImGui.BeginTable("ItemList", 2 + (includeDamage ? 1 : 0) + itemList.SubstatPriorities.Count, + ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable)) { - if (ownedItems != null && !ownedItems.ContainsKey((item.ItemId, item.Hq))) - continue; + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.None, 300); + ImGui.TableSetupColumn("Level", ImGuiTableColumnFlags.WidthFixed, 50); + if (includeDamage) + ImGui.TableSetupColumn("Damage", ImGuiTableColumnFlags.WidthFixed, 50); + foreach (var substat in itemList.SubstatPriorities) + ImGui.TableSetupColumn(_dataHolder.StatNames[substat], ImGuiTableColumnFlags.WidthFixed, 50); - if (item.Level > maxLevel) - continue; + ImGui.TableHeadersRow(); - if (_hideNormalQualityItems && item.CanBeHq && !item.Hq) - continue; - - ImGui.TableNextRow(); - - if (ImGui.TableNextColumn()) + foreach (var item in itemList.Items.DistinctBy(x => new { x.ItemId, x.Hq, Materia = x.MateriaStats?.GetHashCode() })) { - Vector4? color = item.Rarity switch + if (item is not InventoryItem) { - 2 => ImGuiColors.ParsedGreen, - 3 => ImGuiColors.ParsedBlue, - 4 => ImGuiColors.ParsedPurple, - 7 => ImGuiColors.ParsedPink, - _ => null, - }; - - string name = item.Name; - if (item.Hq) - name += $" {SeIconChar.HighQuality.ToIconString()}"; - - if (color != null) - ImGui.TextColored(color.Value, name); - else - ImGui.Text(name); - - if (ImGui.IsItemClicked()) - { - try - { - _chatGui.Print(SeString.CreateItemLink(item.ItemId, item.Hq)); - } - catch (Exception) - { - // doesn't matter, just nice-to-have - } + if (_onlyShowOwnedItems) + continue; } - } - if (ImGui.TableNextColumn()) - { - if (item.Level >= 50 && item.Level % 10 == 0) - ImGui.Text(string.Create(CultureInfo.InvariantCulture, $"{item.Level} ({item.ItemLevel})")); - else - ImGui.Text(item.Level.ToString(CultureInfo.InvariantCulture)); - } + if (item.Level > maxLevel) + continue; - if (includeDamage && ImGui.TableNextColumn()) - ImGui.Text(item.Damage.ToString(CultureInfo.CurrentCulture)); + if (_hideNormalQualityItems && item.CanBeHq && !item.Hq) + continue; + + ImGui.TableNextRow(); - foreach (EBaseParam substat in itemList.SubstatPriorities) - { if (ImGui.TableNextColumn()) { - var stat = item.Stats.Get(substat); - if (stat == 0) - ImGui.Text("-"); + Vector4? color = item.Rarity switch + { + 2 => ImGuiColors.ParsedGreen, + 3 => ImGuiColors.ParsedBlue, + 4 => ImGuiColors.ParsedPurple, + 7 => ImGuiColors.ParsedPink, + _ => null, + }; + + string name = item.Name; + if (item.Hq) + name += $" {SeIconChar.HighQuality.ToIconString()}"; + if (item is InventoryItem { MateriaStats: not null } inventoryItem) + name += + $" {string.Join("", Enumerable.Repeat(SeIconChar.Circle.ToIconString(), inventoryItem.MateriaStats.Count))}"; + + if (color != null) + ImGui.TextColored(color.Value, name); else - ImGui.Text(stat.ToString(CultureInfo.CurrentCulture)); + ImGui.Text(name); + + if (ImGui.IsItemClicked()) + { + try + { + _chatGui.Print(SeString.CreateItemLink(item.ItemId, item.Hq)); + } + catch (Exception) + { + // doesn't matter, just nice-to-have + } + } + } + + if (ImGui.TableNextColumn()) + { + if (item.Level >= 50 && item.Level % 10 == 0) + ImGui.Text(string.Create(CultureInfo.InvariantCulture, $"{item.Level} ({item.ItemLevel})")); + else + ImGui.Text(item.Level.ToString(CultureInfo.InvariantCulture)); + } + + if (includeDamage && ImGui.TableNextColumn()) + ImGui.Text(item.Damage.ToString(CultureInfo.CurrentCulture)); + + foreach (EBaseParam substat in itemList.SubstatPriorities) + { + if (ImGui.TableNextColumn()) + { + var estat = item.Stats.GetEquipment(substat); + var mstat = item.Stats.GetMateria(substat); + if (estat == 0 && mstat == 0) + ImGui.Text("-"); + else if (mstat == 0) + ImGui.Text(string.Create(CultureInfo.InvariantCulture, $"{estat}")); + else + ImGui.Text(string.Create(CultureInfo.InvariantCulture, $"{estat} +{mstat}")); + } } } - } - ImGui.EndTable(); + ImGui.EndTable(); + } + } + finally + { + itemList.ClearFromInventory(); } }