Keep substat caps in mind when calculating materia 'effectiveness'
This commit is contained in:
parent
ac44d91f5b
commit
ea6757cf5c
@ -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 =
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
80
Gearsetter/GameData/ItemLevelCaps.cs
Normal file
80
Gearsetter/GameData/ItemLevelCaps.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Gearsetter.GameData;
|
||||
using LLib.GameData;
|
||||
using LLib.GameData;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Gearsetter.Model;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user