using System; using System.Collections.Generic; using System.Linq; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Lumina.Excel; using Lumina.Excel.Sheets; namespace LLib.Gear; public sealed class GearStatsCalculator { private sealed record MateriaInfo(EBaseParam BaseParam, Collection Values, bool HasItem); private const uint EternityRingItemId = 8575u; private static readonly uint[] CanHaveOffhand = new uint[14] { 2u, 6u, 8u, 12u, 14u, 16u, 18u, 20u, 22u, 24u, 26u, 28u, 30u, 32u }; private readonly ExcelSheet _itemSheet; private readonly Dictionary<(uint ItemLevel, EBaseParam BaseParam), ushort> _itemLevelStatCaps = new Dictionary<(uint, EBaseParam), ushort>(); private readonly Dictionary<(EBaseParam BaseParam, int EquipSlotCategory), ushort> _equipSlotCategoryPct; private readonly Dictionary _materiaStats; public GearStatsCalculator(IDataManager? dataManager) : this(dataManager?.GetExcelSheet() ?? throw new ArgumentNullException("dataManager"), dataManager.GetExcelSheet(), dataManager.GetExcelSheet(), dataManager.GetExcelSheet()) { } public GearStatsCalculator(ExcelSheet itemLevelSheet, ExcelSheet baseParamSheet, ExcelSheet materiaSheet, ExcelSheet itemSheet) { ArgumentNullException.ThrowIfNull(itemLevelSheet, "itemLevelSheet"); ArgumentNullException.ThrowIfNull(baseParamSheet, "baseParamSheet"); ArgumentNullException.ThrowIfNull(materiaSheet, "materiaSheet"); ArgumentNullException.ThrowIfNull(itemSheet, "itemSheet"); _itemSheet = itemSheet; foreach (ItemLevel item in itemLevelSheet) { _itemLevelStatCaps[(item.RowId, EBaseParam.Strength)] = item.Strength; _itemLevelStatCaps[(item.RowId, EBaseParam.Dexterity)] = item.Dexterity; _itemLevelStatCaps[(item.RowId, EBaseParam.Vitality)] = item.Vitality; _itemLevelStatCaps[(item.RowId, EBaseParam.Intelligence)] = item.Intelligence; _itemLevelStatCaps[(item.RowId, EBaseParam.Mind)] = item.Mind; _itemLevelStatCaps[(item.RowId, EBaseParam.Piety)] = item.Piety; _itemLevelStatCaps[(item.RowId, EBaseParam.GP)] = item.GP; _itemLevelStatCaps[(item.RowId, EBaseParam.CP)] = item.CP; _itemLevelStatCaps[(item.RowId, EBaseParam.DamagePhys)] = item.PhysicalDamage; _itemLevelStatCaps[(item.RowId, EBaseParam.DamageMag)] = item.MagicalDamage; _itemLevelStatCaps[(item.RowId, EBaseParam.DefensePhys)] = item.Defense; _itemLevelStatCaps[(item.RowId, EBaseParam.DefenseMag)] = item.MagicDefense; _itemLevelStatCaps[(item.RowId, EBaseParam.Tenacity)] = item.Tenacity; _itemLevelStatCaps[(item.RowId, EBaseParam.Crit)] = item.CriticalHit; _itemLevelStatCaps[(item.RowId, EBaseParam.DirectHit)] = item.DirectHitRate; _itemLevelStatCaps[(item.RowId, EBaseParam.Determination)] = item.Determination; _itemLevelStatCaps[(item.RowId, EBaseParam.SpellSpeed)] = item.SpellSpeed; _itemLevelStatCaps[(item.RowId, EBaseParam.SkillSpeed)] = item.SkillSpeed; _itemLevelStatCaps[(item.RowId, EBaseParam.Gathering)] = item.Gathering; _itemLevelStatCaps[(item.RowId, EBaseParam.Perception)] = item.Perception; _itemLevelStatCaps[(item.RowId, EBaseParam.Craftsmanship)] = item.Craftsmanship; _itemLevelStatCaps[(item.RowId, EBaseParam.Control)] = item.Control; } _equipSlotCategoryPct = baseParamSheet.SelectMany((ExtendedBaseParam x) => from y in Enumerable.Range(0, x.EquipSlotCategoryPct.Count) select ((EBaseParam)x.RowId, y: y, x.EquipSlotCategoryPct[y])).ToDictionary(((EBaseParam, int y, ushort) x) => (x.Item1, x.y), ((EBaseParam, int y, ushort) x) => x.Item3); _materiaStats = materiaSheet.Where((Materia x) => x.RowId != 0 && x.BaseParam.RowId != 0).ToDictionary((Materia x) => x.RowId, (Materia x) => new MateriaInfo((EBaseParam)x.BaseParam.RowId, x.Value, x.Item[0].RowId != 0)); } public unsafe EquipmentStats CalculateGearStats(InventoryItem* item) { List<(uint, byte)> list = new List<(uint, byte)>(); byte b = 0; if (item->ItemId != 8575) { for (int i = 0; i < 5; i++) { ushort num = item->Materia[i]; if (num != 0) { b++; list.Add((num, item->MateriaGrades[i])); } } } return CalculateGearStats(_itemSheet.GetRow(item->ItemId), item->Flags.HasFlag(InventoryItem.ItemFlags.HighQuality), list)with { MateriaCount = b }; } public EquipmentStats CalculateGearStats(Item item, bool highQuality, IReadOnlyList<(uint MateriaId, byte Grade)> materias) { ArgumentNullException.ThrowIfNull(materias, "materias"); Dictionary dictionary = new Dictionary(); for (int i = 0; i < item.BaseParam.Count; i++) { AddEquipmentStat(dictionary, item.BaseParam[i], item.BaseParamValue[i]); } if (highQuality) { for (int j = 0; j < item.BaseParamSpecial.Count; j++) { AddEquipmentStat(dictionary, item.BaseParamSpecial[j], item.BaseParamValueSpecial[j]); } } foreach (var materia in materias) { if (_materiaStats.TryGetValue(materia.MateriaId, out MateriaInfo value)) { AddMateriaStat(item, dictionary, value, materia.Grade); } } return new EquipmentStats(dictionary, 0); } private static void AddEquipmentStat(Dictionary result, RowRef baseParam, short value) { if (baseParam.RowId != 0) { if (result.TryGetValue((EBaseParam)baseParam.RowId, out StatInfo value2)) { result[(EBaseParam)baseParam.RowId] = value2 with { EquipmentValue = (short)(value2.EquipmentValue + value) }; } else { result[(EBaseParam)baseParam.RowId] = new StatInfo(value, 0, Overcapped: false); } } } private void AddMateriaStat(Item item, Dictionary result, MateriaInfo materiaInfo, short grade) { if (!result.TryGetValue(materiaInfo.BaseParam, out StatInfo value)) { value = (result[materiaInfo.BaseParam] = new StatInfo(0, 0, Overcapped: false)); } if (materiaInfo.HasItem) { short num = (short)(GetMaximumStatValue(item, materiaInfo.BaseParam) - value.EquipmentValue); if (value.MateriaValue + materiaInfo.Values[grade] > num) { result[materiaInfo.BaseParam] = value with { MateriaValue = num, Overcapped = true }; } else { result[materiaInfo.BaseParam] = value with { MateriaValue = (short)(value.MateriaValue + materiaInfo.Values[grade]) }; } } else { result[materiaInfo.BaseParam] = value with { MateriaValue = (short)(value.MateriaValue + materiaInfo.Values[grade]) }; } } public short GetMaximumStatValue(Item item, EBaseParam baseParamValue) { if (_itemLevelStatCaps.TryGetValue((item.LevelItem.RowId, baseParamValue), out var value)) { return (short)Math.Round((float)(value * _equipSlotCategoryPct[(baseParamValue, (int)item.EquipSlotCategory.RowId)]) / 1000f, MidpointRounding.AwayFromZero); } return 0; } public unsafe short CalculateAverageItemLevel(InventoryContainer* container) { uint num = 0u; int num2 = 12; for (int i = 0; i < 13; i++) { if (i == 5) { continue; } InventoryItem* inventorySlot = container->GetInventorySlot(i); if (inventorySlot == null || inventorySlot->ItemId == 0) { continue; } Item? rowOrDefault = _itemSheet.GetRowOrDefault(inventorySlot->ItemId); if (!rowOrDefault.HasValue) { continue; } if (rowOrDefault.Value.ItemUICategory.RowId == 105) { if (i == 0) { num2--; } num2--; continue; } if (i == 0 && !CanHaveOffhand.Contains(rowOrDefault.Value.ItemUICategory.RowId)) { num += rowOrDefault.Value.LevelItem.RowId; i++; } num += rowOrDefault.Value.LevelItem.RowId; } return (short)(num / num2); } }