qstbak/LLib/LLib.Gear/GearStatsCalculator.cs
2025-10-09 07:47:19 +10:00

219 lines
7.6 KiB
C#

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<short> 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<Item> _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<uint, MateriaInfo> _materiaStats;
public GearStatsCalculator(IDataManager? dataManager)
: this(dataManager?.GetExcelSheet<ItemLevel>() ?? throw new ArgumentNullException("dataManager"), dataManager.GetExcelSheet<ExtendedBaseParam>(), dataManager.GetExcelSheet<Materia>(), dataManager.GetExcelSheet<Item>())
{
}
public GearStatsCalculator(ExcelSheet<ItemLevel> itemLevelSheet, ExcelSheet<ExtendedBaseParam> baseParamSheet, ExcelSheet<Materia> materiaSheet, ExcelSheet<Item> 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<EBaseParam, StatInfo> dictionary = new Dictionary<EBaseParam, StatInfo>();
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<EBaseParam, StatInfo> result, RowRef<BaseParam> 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<EBaseParam, StatInfo> 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);
}
}