LLib/Gear/GearStatsCalculator.cs

241 lines
9.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
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 static readonly uint[] CanHaveOffhand = [2, 6, 8, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32];
private readonly ExcelSheet<Item> _itemSheet;
private readonly Dictionary<(uint ItemLevel, EBaseParam BaseParam), ushort> _itemLevelStatCaps = [];
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(nameof(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);
ArgumentNullException.ThrowIfNull(baseParamSheet);
ArgumentNullException.ThrowIfNull(materiaSheet);
ArgumentNullException.ThrowIfNull(itemSheet);
_itemSheet = itemSheet;
foreach (var itemLevel in itemLevelSheet)
{
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Strength)] = itemLevel.Strength;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Dexterity)] = itemLevel.Dexterity;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Vitality)] = itemLevel.Vitality;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Intelligence)] = itemLevel.Intelligence;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Mind)] = itemLevel.Mind;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Piety)] = itemLevel.Piety;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.GP)] = itemLevel.GP;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.CP)] = itemLevel.CP;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.DamagePhys)] = itemLevel.PhysicalDamage;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.DamageMag)] = itemLevel.MagicalDamage;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.DefensePhys)] = itemLevel.Defense;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.DefenseMag)] = itemLevel.MagicDefense;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Tenacity)] = itemLevel.Tenacity;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Crit)] = itemLevel.CriticalHit;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.DirectHit)] = itemLevel.DirectHitRate;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Determination)] = itemLevel.Determination;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.SpellSpeed)] = itemLevel.SpellSpeed;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.SkillSpeed)] = itemLevel.SkillSpeed;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Gathering)] = itemLevel.Gathering;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Perception)] = itemLevel.Perception;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Craftsmanship)] = itemLevel.Craftsmanship;
_itemLevelStatCaps[(itemLevel.RowId, EBaseParam.Control)] = itemLevel.Control;
}
_equipSlotCategoryPct = baseParamSheet
.SelectMany(x => Enumerable.Range(0, x.EquipSlotCategoryPct.Count)
.Select(y => ((EBaseParam)x.RowId, y, x.EquipSlotCategoryPct[y])))
.ToDictionary(x => (x.Item1, x.Item2), x => x.Item3);
_materiaStats = materiaSheet.Where(x => x.RowId > 0 && x.BaseParam.RowId > 0)
.ToDictionary(x => x.RowId,
x => new MateriaInfo((EBaseParam)x.BaseParam.RowId, x.Value, x.Item[0].RowId > 0));
}
public unsafe EquipmentStats CalculateGearStats(InventoryItem* item)
{
List<(uint, byte)> materias = [];
byte materiaCount = 0;
for (int i = 0; i < 5; ++i)
{
var materia = item->Materia[i];
if (materia != 0)
{
materiaCount++;
materias.Add((materia, item->MateriaGrades[i]));
}
}
return CalculateGearStats(_itemSheet.GetRow(item->ItemId), item->Flags.HasFlag(InventoryItem.ItemFlags.HighQuality), materias) with
{
MateriaCount = materiaCount,
};
}
public EquipmentStats CalculateGearStats(Item item, bool highQuality,
IReadOnlyList<(uint MateriaId, byte Grade)> materias)
{
ArgumentNullException.ThrowIfNull(materias);
Dictionary<EBaseParam, StatInfo> result = [];
for (int i = 0; i < item.BaseParam.Count; ++i)
AddEquipmentStat(result, item.BaseParam[i], item.BaseParamValue[i]);
if (highQuality)
{
for (int i = 0; i < item.BaseParamSpecial.Count; ++i)
AddEquipmentStat(result, item.BaseParamSpecial[i], item.BaseParamValueSpecial[i]);
}
foreach (var materia in materias)
{
if (_materiaStats.TryGetValue(materia.MateriaId, out var materiaStat))
AddMateriaStat(item, result, materiaStat, materia.Grade);
}
return new EquipmentStats(result, 0);
}
private static void AddEquipmentStat(Dictionary<EBaseParam, StatInfo> result, RowRef<BaseParam> baseParam,
short value)
{
if (baseParam.RowId == 0)
return;
if (result.TryGetValue((EBaseParam)baseParam.RowId, out var statInfo))
result[(EBaseParam)baseParam.RowId] =
statInfo with { EquipmentValue = (short)(statInfo.EquipmentValue + value) };
else
result[(EBaseParam)baseParam.RowId] =
new StatInfo(value, 0, false);
}
private void AddMateriaStat(Item item, Dictionary<EBaseParam, StatInfo> result, MateriaInfo materiaInfo,
short grade)
{
if (!result.TryGetValue(materiaInfo.BaseParam, out var statInfo))
result[materiaInfo.BaseParam] = statInfo = new StatInfo(0, 0, false);
// overcap calculation is only done if a physical materia item is melded
if (materiaInfo.HasItem)
{
short maximumValue = (short)(GetMaximumStatValue(item, materiaInfo.BaseParam) - statInfo.EquipmentValue);
if (statInfo.MateriaValue + materiaInfo.Values[grade] > maximumValue)
{
result[materiaInfo.BaseParam] = statInfo with
{
MateriaValue = maximumValue,
Overcapped = true,
};
}
else
{
result[materiaInfo.BaseParam] = statInfo with
{
MateriaValue = (short)(statInfo.MateriaValue + materiaInfo.Values[grade])
};
}
}
else
{
result[materiaInfo.BaseParam] = statInfo with
{
MateriaValue = (short)(statInfo.MateriaValue + materiaInfo.Values[grade])
};
}
}
public short GetMaximumStatValue(Item item, EBaseParam baseParamValue)
{
return (short)Math.Round(
_itemLevelStatCaps[(item.LevelItem.RowId, baseParamValue)] *
_equipSlotCategoryPct[(baseParamValue, (int)item.EquipSlotCategory.RowId)] / 1000f,
MidpointRounding.AwayFromZero);
}
// From caraxi/SimpleTweaks
public unsafe short CalculateAverageItemLevel(InventoryContainer* container)
{
uint sum = 0U;
var calculatedSlots = 12;
for (var i = 0; i < 13; i++) {
if (i == 5) // belt
continue;
var inventoryItem = container->GetInventorySlot(i);
if (inventoryItem == null || inventoryItem->ItemId == 0) continue;
var item = _itemSheet.GetRowOrDefault(inventoryItem->ItemId);
if (item == null)
continue;
// blue mage weapon
if (item.Value.ItemUICategory.RowId == 105) {
if (i == 0)
calculatedSlots -= 1;
calculatedSlots -= 1;
continue;
}
// count main hand weapon twice if no offhand is equippable
if (i == 0 && !CanHaveOffhand.Contains(item.Value.ItemUICategory.RowId)) {
sum += item.Value.LevelItem.RowId;
i++;
}
sum += item.Value.LevelItem.RowId;
}
return (short)(sum / calculatedSlots);
}
private sealed record MateriaInfo(EBaseParam BaseParam, Collection<short> Values, bool HasItem);
}
// From caraxi/SimpleTweaks
[Sheet("BaseParam")]
[SuppressMessage("Performance", "CA1815", Justification = "Lumina doesn't implement any equality ops")]
public readonly unsafe struct ExtendedBaseParam(ExcelPage page, uint offset, uint row)
: IExcelRow<ExtendedBaseParam>
{
private const int ParamCount = 23;
public uint RowId => row;
public BaseParam BaseParam => new(page, offset, row);
public Collection<ushort> EquipSlotCategoryPct =>
new(page, offset, offset, &EquipSlotCategoryPctCtor, ParamCount);
private static ushort EquipSlotCategoryPctCtor(ExcelPage page, uint parentOffset, uint offset, uint i) =>
i == 0 ? (ushort)0 : page.ReadUInt16(offset + 8 + (i - 1) * 2);
public static ExtendedBaseParam Create(ExcelPage page, uint offset, uint row) => new(page, offset, row);
}