Skip to content

Commit 3f7cc0d

Browse files
vaisestLocalIdentity
andauthored
Port minion stat sorting options for trade and tree (#9930)
* Apply changes from PathOfBuildingCommunity/PathOfBuilding-PoE2#2261 * Fix merge conflicts * Rework timeless fallback weights * Removed dead variable * Fix calcs and clean up some code Fixes the implementaiton of lower is better stats and cleans up it's usage Fixes the unique list not using the minion stat sorting correctly --------- Co-authored-by: vaisest <vaisest@users.noreply.github.com> Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent d1939ae commit 3f7cc0d

13 files changed

Lines changed: 321 additions & 132 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
describe("ItemDBControl", function()
2+
it("sorts lower-is-better stats below zero", function()
3+
local function makeItem(name)
4+
return {
5+
name = name,
6+
base = {},
7+
enchantModLines = {},
8+
implicitModLines = {},
9+
explicitModLines = {},
10+
baseModList = {},
11+
}
12+
end
13+
local betterItem = makeItem("Better Item")
14+
local worseItem = makeItem("Worse Item")
15+
local invalidItem = makeItem("Invalid Item")
16+
local takenDamage = {
17+
[betterItem] = 80,
18+
[worseItem] = 120,
19+
}
20+
local itemsTab = {
21+
activeItemSet = { useSecondWeaponSet = false },
22+
slots = { ["Body Armour"] = {} },
23+
build = {
24+
calcsTab = {
25+
GetMiscCalculator = function()
26+
return function(args)
27+
return { PhysicalTakenHit = takenDamage[args.repItem] }
28+
end
29+
end,
30+
},
31+
},
32+
IsItemValidForSlot = function(_, item)
33+
return item ~= invalidItem
34+
end,
35+
}
36+
local control = new("ItemDBControl", nil, { 0, 0, 100, 100 }, itemsTab, {
37+
list = { invalidItem, betterItem, worseItem },
38+
}, "RARE")
39+
control.sortDetail = {
40+
stat = "PhysicalTakenHit",
41+
transform = function(value) return -value end,
42+
}
43+
control.sortOrder = { control.sortControl.STAT, control.sortControl.NAME }
44+
45+
control:ListBuilder()
46+
47+
assert.are.equal(betterItem, control.list[1])
48+
assert.are.equal(worseItem, control.list[2])
49+
assert.are.equal(invalidItem, control.list[3])
50+
assert.are.equal(-80, betterItem.measuredPower)
51+
assert.are.equal(-120, worseItem.measuredPower)
52+
assert.are.equal(-math.huge, invalidItem.measuredPower)
53+
end)
54+
end)

spec/System/TestTradeQueryGenerator_spec.lua

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ describe("TradeQueryGenerator", function()
1515
end)
1616

1717
describe("WeightedRatioOutputs", function()
18+
local maxStatIncrease
19+
20+
before_each(function()
21+
maxStatIncrease = data.misc.maxStatIncrease
22+
data.misc.maxStatIncrease = 1000
23+
end)
24+
25+
after_each(function()
26+
data.misc.maxStatIncrease = maxStatIncrease
27+
end)
28+
1829
-- Pass: Returns 0, avoiding math errors
1930
-- Fail: Returns NaN/inf or crashes, indicating unhandled infinite values, causing evaluation failures in infinite-scaling builds
2031
it("handles infinite base", function()
@@ -31,10 +42,63 @@ describe("TradeQueryGenerator", function()
3142
local baseOutput = { TotalDPS = 0 }
3243
local newOutput = { TotalDPS = 100 }
3344
local statWeights = { { stat = "TotalDPS", weightMult = 1 } }
34-
data.misc.maxStatIncrease = 1000
3545
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
3646
assert.are.equal(result, 100)
3747
end)
48+
it("uses minion output for non-FullDPS stats when minion output is desired", function()
49+
local baseOutput = { Life = 10, Minion = { Life = 100 } }
50+
local newOutput = { Life = 10, Minion = { Life = 250 } }
51+
local statWeights = { { stat = "MinionLife", weightMult = 1 } }
52+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
53+
54+
assert.are.equal(result, 2.5)
55+
end)
56+
57+
it("uses lower is better stats correctly", function()
58+
local baseOutput = { MaxHit = 100 }
59+
local newOutput = { MaxHit = 10 }
60+
local statWeights = { { stat = "MaxHit", weightMult = 1, transform = function(number) return -number end } }
61+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
62+
63+
local close_enough = math.abs(result - -0.1) < 0.0001
64+
assert.True(close_enough)
65+
end)
66+
67+
it("uses player and minion output for FullDPS", function()
68+
-- minion output gets assigned to the player's full dps in reality
69+
local baseOutput = { FullDPS = 100, Minion = { FullDPS = 100 } }
70+
local newOutput = { FullDPS = 250, Minion = { FullDPS = 1000 } }
71+
local statWeights = { { stat = "FullDPS", weightMult = 1 } }
72+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
73+
74+
assert.are.equal(result, 2.5)
75+
end)
76+
77+
it("uses player output for non-FullDPS even when minion output is available", function()
78+
local baseOutput = { Life = 100, Minion = { Life = 100 } }
79+
local newOutput = { Life = 250, Minion = { Life = 1000 } }
80+
local statWeights = { { stat = "Life", weightMult = 1 } }
81+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
82+
assert.are.equal(result, 2.5)
83+
end)
84+
85+
it("uses the fallback DPS ratio once when FullDPS is unavailable", function()
86+
local baseOutput = { Minion = { TotalDPS = 10, TotalDotDPS = 0, CombinedDPS = 10 } }
87+
local newOutput = { Minion = { TotalDPS = 25, TotalDotDPS = 0, CombinedDPS = 25 } }
88+
local statWeights = { { stat = "FullDPS", weightMult = 1 } }
89+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
90+
91+
assert.are.equal(result, 2.5)
92+
end)
93+
94+
it("falls back to player output when the selected stat is not on minion output", function()
95+
local baseOutput = { Spirit = 100, Minion = { AverageDamage = 100 } }
96+
local newOutput = { Spirit = 120, Minion = { AverageDamage = 100 } }
97+
local statWeights = { { stat = "Spirit", weightMult = 1 } }
98+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
99+
100+
assert.are.equal(result, 1.2)
101+
end)
38102
end)
39103

40104
describe("Filter prioritization", function()

spec/System/TestTradeQuery_spec.lua

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
describe("TradeQuery", function()
2+
local mock_tradeQuery
3+
local mock_queryGen
4+
5+
before_each(function()
6+
mock_tradeQuery = new("TradeQuery", { itemsTab = {} })
7+
mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} })
8+
end)
29
describe("result dropdown tooltipFunc", function()
310
-- Builds a TradeQuery with the strict minimum needed for
411
-- PriceItemRowDisplay to construct row 1 without exploding. Only the
@@ -54,4 +61,57 @@ describe("TradeQuery", function()
5461
assert.are.equal(0, #tooltip.lines)
5562
end)
5663
end)
64+
describe("ReduceOutput", function()
65+
it("preserves lower-is-better values for weighted result comparison", function()
66+
local weights = {
67+
{ stat = "PhysicalTakenHit", weightMult = 1, transform = function(value) return -value end },
68+
}
69+
mock_tradeQuery.statSortSelectionList = weights
70+
71+
local baseOutput = { PhysicalTakenHit = 100 }
72+
local betterOutput = mock_tradeQuery:ReduceOutput({ PhysicalTakenHit = 80 })
73+
local worseOutput = mock_tradeQuery:ReduceOutput({ PhysicalTakenHit = 120 })
74+
local betterWeight = mock_queryGen.WeightedRatioOutputs(baseOutput, betterOutput, weights)
75+
local worseWeight = mock_queryGen.WeightedRatioOutputs(baseOutput, worseOutput, weights)
76+
77+
assert.are.equal(80, betterOutput.PhysicalTakenHit)
78+
assert.is_true(betterWeight > worseWeight)
79+
end)
80+
81+
it("uses selected minion stats for weighted result comparison", function()
82+
mock_tradeQuery.statSortSelectionList = { { stat = "AverageDamage" } }
83+
84+
local result = mock_tradeQuery:ReduceOutput({
85+
AverageDamage = 10,
86+
Life = 100,
87+
Minion = {
88+
AverageDamage = 250,
89+
Life = 200,
90+
},
91+
})
92+
93+
assert.are.equals(260, result.AverageDamage)
94+
assert.is_nil(result.Life)
95+
end)
96+
97+
it("keeps fallback DPS stats when FullDPS is selected but not present", function()
98+
mock_tradeQuery.statSortSelectionList = { { stat = "FullDPS", weightMult = 1 } }
99+
100+
local baseOutput = {
101+
CombinedDPS = 100,
102+
TotalDPS = 100,
103+
TotalDotDPS = 0,
104+
}
105+
local reducedOutput = mock_tradeQuery:ReduceOutput({
106+
CombinedDPS = 120,
107+
TotalDPS = 120,
108+
TotalDotDPS = 0,
109+
})
110+
111+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, reducedOutput,
112+
mock_tradeQuery.statSortSelectionList)
113+
114+
assert.are.equals(1.2, result)
115+
end)
116+
end)
57117
end)

src/Classes/CalcsTab.lua

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -712,16 +712,8 @@ function CalcsTabClass:PowerBuilder()
712712
end
713713

714714
function CalcsTabClass:CalculatePowerStat(selection, original, modified)
715-
if modified.Minion and selection.stat ~= "FullDPS" then
716-
original = original.Minion
717-
modified = modified.Minion
718-
end
719-
local originalValue = original[selection.stat] or 0
720-
local modifiedValue = modified[selection.stat] or 0
721-
if selection.transform then
722-
originalValue = selection.transform(originalValue)
723-
modifiedValue = selection.transform(modifiedValue)
724-
end
715+
local originalValue = data.powerStatList.GetFromOutput(original, selection)
716+
local modifiedValue = data.powerStatList.GetFromOutput(modified, selection)
725717
return originalValue - modifiedValue
726718
end
727719

@@ -732,10 +724,9 @@ function CalcsTabClass:CalculateCombinedOffDefStat(original, modified)
732724
(original.Evasion - modified.Evasion) / m_max(10000, modified.Evasion) +
733725
(original.LifeRegenRecovery - modified.LifeRegenRecovery) / 500 +
734726
(original.EnergyShieldRegenRecovery - modified.EnergyShieldRegenRecovery) / 1000
735-
if modified.Minion then
736-
return (original.Minion.CombinedDPS - modified.Minion.CombinedDPS) / modified.Minion.CombinedDPS, defence
737-
end
738-
return (original.CombinedDPS - modified.CombinedDPS) / modified.CombinedDPS, defence
727+
local modifiedDps = modified.CombinedDPS + (modified.Minion and modified.Minion.CombinedDPS or 0)
728+
local dpsIncr = original.CombinedDPS + (original.Minion and original.Minion.CombinedDPS or 0) - modifiedDps
729+
return dpsIncr / modifiedDps, defence
739730
end
740731

741732
function CalcsTabClass:GetNodeCalculator()

src/Classes/CompareTab.lua

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2577,10 +2577,7 @@ function CompareTabClass:ComparePowerBuilder(compareEntry, powerStat, categories
25772577
end
25782578

25792579
-- Get baseline stat value for percentage calculation
2580-
local baseStatValue = calcBase[powerStat.stat] or 0
2581-
if powerStat.transform then
2582-
baseStatValue = powerStat.transform(baseStatValue)
2583-
end
2580+
local baseStatValue = data.powerStatList.GetFromOutput(calcBase, powerStat)
25842581

25852582
-- Helper to format an impact value and compute percentage
25862583
local function formatImpact(impact)

src/Classes/ItemDBControl.lua

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ end
190190

191191
function ItemDBClass:BuildSortOrder()
192192
wipeTable(self.sortDropList)
193-
for id,stat in pairs(data.powerStatList) do
193+
for id, stat in ipairs(data.powerStatList) do
194194
if not stat.ignoreForItems then
195195
t_insert(self.sortDropList, {
196196
label="Sort by "..stat.label,
@@ -227,14 +227,11 @@ function ItemDBClass:ListBuilder()
227227
local start = GetTime()
228228
local calcFunc, calcBase = self.itemsTab.build.calcsTab:GetMiscCalculator(self.build)
229229
for itemIndex, item in ipairs(list) do
230-
item.measuredPower = 0
230+
item.measuredPower = -math.huge
231231
for slotName, slot in pairs(self.itemsTab.slots) do
232232
if self.itemsTab:IsItemValidForSlot(item, slotName) and not slot.inactive and (not slot.weaponSet or slot.weaponSet == (self.itemsTab.activeItemSet.useSecondWeaponSet and 2 or 1)) then
233233
local output = calcFunc(item.base.flask and { toggleFlask = item } or item.base.tincture and { toggleTincture = item } or { repSlotName = slotName, repItem = item }, useFullDPS)
234-
local measuredPower = output.Minion and output.Minion[self.sortMode] or output[self.sortMode] or 0
235-
if self.sortDetail.transform then
236-
measuredPower = self.sortDetail.transform(measuredPower)
237-
end
234+
local measuredPower = data.powerStatList.GetFromOutput(output, self.sortDetail)
238235
item.measuredPower = m_max(item.measuredPower, measuredPower)
239236
end
240237
end
@@ -363,4 +360,4 @@ function ItemDBClass:OnHoverKeyUp(key)
363360
itemLib.wiki.openItem(item)
364361
end
365362
end
366-
end
363+
end

0 commit comments

Comments
 (0)