Keep substat caps in mind when calculating materia 'effectiveness'

This commit is contained in:
Liza 2024-11-21 17:52:31 +01:00
parent ac44d91f5b
commit ea6757cf5c
Signed by: liza
GPG Key ID: 7199F8D727D55F67
9 changed files with 139 additions and 35 deletions

View File

@ -45,7 +45,10 @@ public sealed class ItemSortingTest
.Where(x => x.PrimaryStat > 0)
.ToDictionary(x => (EClassJob)x.RowId, x => (EBaseParam)x.PrimaryStat);
itemList.UpdateStats(primaryStats, new Configuration());
var itemLevelCaps = new ItemLevelCaps(_lumina.GetExcelSheet<ItemLevel>()!,
_lumina.GetExcelSheet<ItemLevelCaps.ExtendedBaseParam>()!);
itemList.UpdateStats(primaryStats, new Configuration(), itemLevelCaps);
itemList.Sort();
List<uint> expectedItems =

View File

@ -14,12 +14,14 @@ namespace Gearsetter.GameData;
internal sealed class GameDataHolder
{
private readonly Configuration _configuration;
private readonly ItemLevelCaps _itemLevelCaps;
private readonly Dictionary<uint, List<EClassJob>> _classJobCategories;
private readonly IReadOnlyList<ItemList> _allItemLists;
public GameDataHolder(IDataManager dataManager, Configuration configuration)
{
_configuration = configuration;
_itemLevelCaps = new ItemLevelCaps(dataManager);
_classJobCategories = dataManager.GetExcelSheet<ClassJobCategory>()
.ToDictionary(x => x.RowId, x =>
new Dictionary<EClassJob, bool>
@ -179,7 +181,7 @@ internal sealed class GameDataHolder
{
foreach (ItemList itemList in _allItemLists)
{
itemList.UpdateStats(PrimaryStats, _configuration);
itemList.UpdateStats(PrimaryStats, _configuration, _itemLevelCaps);
itemList.Sort();
}
}

View File

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using Dalamud.Plugin.Services;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Gearsetter.GameData;
internal sealed class ItemLevelCaps
{
private readonly ExcelSheet<ExtendedBaseParam> _baseParamSheet;
private readonly Dictionary<(uint, EBaseParam), ushort> _caps = [];
public ItemLevelCaps(IDataManager dataManager)
: this(dataManager.GetExcelSheet<ItemLevel>(), dataManager.GetExcelSheet<ExtendedBaseParam>())
{
}
public ItemLevelCaps(ExcelSheet<ItemLevel> itemLevelSheet, ExcelSheet<ExtendedBaseParam> baseParamSheet)
{
_baseParamSheet = baseParamSheet;
foreach (var itemLevel in itemLevelSheet)
{
_caps[(itemLevel.RowId, EBaseParam.Strength)] = itemLevel.Strength;
_caps[(itemLevel.RowId, EBaseParam.Dexterity)] = itemLevel.Dexterity;
_caps[(itemLevel.RowId, EBaseParam.Vitality)] = itemLevel.Vitality;
_caps[(itemLevel.RowId, EBaseParam.Intelligence)] = itemLevel.Intelligence;
_caps[(itemLevel.RowId, EBaseParam.Mind)] = itemLevel.Mind;
_caps[(itemLevel.RowId, EBaseParam.Piety)] = itemLevel.Piety;
_caps[(itemLevel.RowId, EBaseParam.GP)] = itemLevel.GP;
_caps[(itemLevel.RowId, EBaseParam.CP)] = itemLevel.CP;
_caps[(itemLevel.RowId, EBaseParam.DamagePhys)] = itemLevel.PhysicalDamage;
_caps[(itemLevel.RowId, EBaseParam.DamageMag)] = itemLevel.MagicalDamage;
_caps[(itemLevel.RowId, EBaseParam.DefensePhys)] = itemLevel.Defense;
_caps[(itemLevel.RowId, EBaseParam.DefenseMag)] = itemLevel.MagicDefense;
_caps[(itemLevel.RowId, EBaseParam.Tenacity)] = itemLevel.Tenacity;
_caps[(itemLevel.RowId, EBaseParam.Crit)] = itemLevel.CriticalHit;
_caps[(itemLevel.RowId, EBaseParam.DirectHit)] = itemLevel.DirectHitRate;
_caps[(itemLevel.RowId, EBaseParam.Determination)] = itemLevel.Determination;
_caps[(itemLevel.RowId, EBaseParam.SpellSpeed)] = itemLevel.SpellSpeed;
_caps[(itemLevel.RowId, EBaseParam.SkillSpeed)] = itemLevel.SkillSpeed;
_caps[(itemLevel.RowId, EBaseParam.Gathering)] = itemLevel.Gathering;
_caps[(itemLevel.RowId, EBaseParam.Perception)] = itemLevel.Perception;
_caps[(itemLevel.RowId, EBaseParam.Craftsmanship)] = itemLevel.Craftsmanship;
_caps[(itemLevel.RowId, EBaseParam.Control)] = itemLevel.Control;
}
}
public short GetMaximum(Item item, EBaseParam baseParamValue)
{
var baseParam = _baseParamSheet.GetRow((uint)baseParamValue);
return (short)Math.Round(
_caps[(item.LevelItem.RowId, baseParamValue)] *
(baseParam.EquipSlotCategoryPct[(int)item.EquipSlotCategory.RowId] / 1000f), MidpointRounding.AwayFromZero);
}
// From caraxi/SimpleTWeaks
[Sheet("BaseParam")]
public readonly unsafe struct ExtendedBaseParam(ExcelPage page, uint offset, uint row)
: IExcelRow<ExtendedBaseParam>
{
private const int ParamCount = 23;
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);
public uint RowId => row;
}
}

View File

@ -1,6 +1,6 @@
<Project Sdk="Dalamud.NET.Sdk/11.0.0">
<PropertyGroup>
<Version>2.0</Version>
<Version>2.1</Version>
<OutputPath>dist</OutputPath>
</PropertyGroup>

View File

@ -1,4 +1,5 @@
using Gearsetter.GameData;
using System.Linq;
using Gearsetter.GameData;
using LLib.GameData;
using Lumina.Excel.Sheets;
@ -26,24 +27,16 @@ internal abstract record BaseItem(Item Item, bool Hq, MateriaStats? MateriaStats
get
{
if (ClassJob.DealsMagicDamage())
return Item.DamageMag + Stats.Get(EBaseParam.DamageMag);
return Item.DamageMag + Stats.Get(EBaseParam.DamageMag, null);
else if (ClassJob.DealsPhysicalDamage())
return Item.DamagePhys + Stats.Get(EBaseParam.DamagePhys);
return Item.DamagePhys + Stats.Get(EBaseParam.DamagePhys, null);
else
return 0;
}
}
public bool HasAnyStat(params EBaseParam[] substats)
{
foreach (EBaseParam substat in substats)
{
if (Stats.Get(substat) > 0)
return true;
}
return false;
}
=> substats.Any(x => Stats.Has(x));
public bool IsCombatRelicWithoutSubstats()
{

View File

@ -1,5 +1,4 @@
using Gearsetter.GameData;
using LLib.GameData;
using LLib.GameData;
using Lumina.Excel.Sheets;
namespace Gearsetter.Model;

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Gearsetter.GameData;
using Lumina.Excel.Sheets;
@ -7,11 +8,13 @@ namespace Gearsetter.Model;
internal sealed class EquipmentStats
{
private readonly Item _item;
private readonly Dictionary<EBaseParam, short> _equipmentValues;
private readonly Dictionary<EBaseParam, short> _materiaValues;
public EquipmentStats(Item item, bool hq, MateriaStats? materiaStats)
{
_item = item;
_equipmentValues = Enumerable.Range(0, item.BaseParam.Count)
.Where(i => item.BaseParam[i].RowId > 0)
.ToDictionary(i => (EBaseParam)item.BaseParam[i].RowId, i => item.BaseParamValue[i]);
@ -44,9 +47,9 @@ internal sealed class EquipmentStats
}
}
public short Get(EBaseParam param)
public short Get(EBaseParam param, ItemLevelCaps? itemLevelCaps)
{
return (short)(GetEquipment(param) + GetMateria(param));
return (short)(GetEquipment(param) + GetMateria(param, itemLevelCaps));
}
public short GetEquipment(EBaseParam param)
@ -55,9 +58,26 @@ internal sealed class EquipmentStats
return v;
}
public short GetMateria(EBaseParam param)
public short GetMateria(EBaseParam param, ItemLevelCaps? itemLevelCaps)
{
_materiaValues.TryGetValue(param, out short v);
if (v == 0)
return v;
if (param != EBaseParam.DamagePhys && param != EBaseParam.DamageMag)
{
// This isn't necessary accurate for Eureka relics, which can (in theory) have +1000 critical hit, but
// they're both outdated and the limits aren't relevant for a decision of 'which gear piece is better';
// worst case it'll suggest the i405 savage weapon instead.
ArgumentNullException.ThrowIfNull(itemLevelCaps);
short max = itemLevelCaps.GetMaximum(_item, param);
short equipped = _equipmentValues.GetValueOrDefault(param);
if (v + equipped > max)
return (short)(max - equipped);
}
return v;
}
public bool Has(EBaseParam substat) => _equipmentValues.ContainsKey(substat) || _materiaValues.ContainsKey(substat);
}

View File

@ -22,16 +22,18 @@ internal sealed class ItemList
public required uint ItemUiCategory { get; init; }
public required List<BaseItem> Items { get; set; }
public EBaseParam PrimaryStat { get; set; }
public IReadOnlyList<EBaseParam> SubstatPriorities { get; set; } = new List<EBaseParam>();
public IReadOnlyList<EBaseParam> SubstatPriorities { get; private set; } = new List<EBaseParam>();
public ItemLevelCaps ItemLevelCaps { get; private set; } = null!;
public void Sort()
{
Items = Items
.OrderDescending(new ItemComparer(SubstatPriorities))
.OrderDescending(new ItemComparer(SubstatPriorities, ItemLevelCaps))
.ToList();
}
public void UpdateStats(Dictionary<EClassJob, EBaseParam> primaryStats, Configuration configuration)
public void UpdateStats(Dictionary<EClassJob, EBaseParam> primaryStats, Configuration configuration,
ItemLevelCaps itemLevelCaps)
{
if (ClassJob.IsTank())
SubstatPriorities = configuration.StatPriorityTanks;
@ -50,13 +52,15 @@ internal sealed class ItemList
else
SubstatPriorities = [];
ItemLevelCaps = itemLevelCaps;
if (primaryStats.TryGetValue(ClassJob, out EBaseParam primaryStat))
{
PrimaryStat = primaryStat;
Items = Items
.Where(x => x is EquipmentItem)
.Cast<EquipmentItem>()
.Select(x => x with { PrimaryStat = x.Stats.Get(primaryStat) })
.Select(x => x with { PrimaryStat = x.Stats.Get(primaryStat, itemLevelCaps) })
.Cast<BaseItem>()
.ToList();
}
@ -78,7 +82,7 @@ internal sealed class ItemList
Items.Add(
new InventoryItem(basicItem.Item, basicItem.Hq, materias, basicItem.ClassJob)
{
PrimaryStat = basicItem.Stats.Get(PrimaryStat)
PrimaryStat = basicItem.Stats.Get(PrimaryStat, ItemLevelCaps)
});
}
}
@ -91,7 +95,10 @@ internal sealed class ItemList
Items.RemoveAll(x => x is InventoryItem);
}
private sealed class ItemComparer(IReadOnlyList<EBaseParam> substatPriorities) : IComparer<BaseItem>
private sealed class ItemComparer(
IReadOnlyList<EBaseParam> substatPriorities,
ItemLevelCaps itemLevelCaps
) : IComparer<BaseItem>
{
public int Compare(BaseItem? a, BaseItem? b)
{
@ -120,14 +127,14 @@ internal sealed class ItemList
return primaryStatA.CompareTo(primaryStatB);
// gear: vitality wins
int vitalityA = a.Stats.Get(EBaseParam.Vitality);
int vitalityB = b.Stats.Get(EBaseParam.Vitality);
int vitalityA = a.Stats.Get(EBaseParam.Vitality, itemLevelCaps);
int vitalityB = b.Stats.Get(EBaseParam.Vitality, itemLevelCaps);
if (vitalityA != vitalityB)
return vitalityA.CompareTo(vitalityB);
// sum of relevant substats
int sumOfSubstatsA = substatPriorities.Sum(x => a.Stats.Get(x));
int sumOfSubstatsB = substatPriorities.Sum(x => b.Stats.Get(x));
int sumOfSubstatsA = substatPriorities.Sum(x => a.Stats.Get(x, itemLevelCaps));
int sumOfSubstatsB = substatPriorities.Sum(x => b.Stats.Get(x, itemLevelCaps));
// some relics have no substats in the sheets, since they can be allocated dynamically
// they are -generally- better/equal to any other weapon on that ilvl
@ -162,8 +169,8 @@ internal sealed class ItemList
// individual substats
foreach (EBaseParam substat in substatPriorities)
{
int substatA = a.Stats.Get(substat);
int substatB = b.Stats.Get(substat);
int substatA = a.Stats.Get(substat, itemLevelCaps);
int substatB = b.Stats.Get(substat, itemLevelCaps);
if (substatA != substatB)
return substatA.CompareTo(substatB);
}
@ -172,7 +179,7 @@ internal sealed class ItemList
return string.CompareOrdinal(a.Name, b.Name);
}
public static bool TryGetPreferredItemPriority(BaseItem self, BaseItem other, out byte priority)
private static bool TryGetPreferredItemPriority(BaseItem self, BaseItem other, out byte priority)
{
if (PreferredItems.TryGetValue(self.ItemId, out byte levelSelf))
{

View File

@ -215,7 +215,7 @@ internal sealed class EquipmentBrowserWindow : LWindow
if (ImGui.TableNextColumn())
{
var estat = item.Stats.GetEquipment(substat);
var mstat = item.Stats.GetMateria(substat);
var mstat = item.Stats.GetMateria(substat, itemList.ItemLevelCaps);
if (estat == 0 && mstat == 0)
ImGui.Text("-");
else if (mstat == 0)