Skip to content

Commit a1d60e1

Browse files
vaisestLocalIdentity
andauthored
Rework power stats (#2261)
* Rework power stats by combining minion and player dps, and separating minion stats * Also use dps sumlogic for off/def tree * Remove unnecessary import * Apply stat transform in data.powerStatList.GetFromOutput and fix anoint * Make stat weight panel larger * Fix tests * Fix remaining issues * Fix FullDPS stat fallback --------- Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent 66d2ccb commit a1d60e1

13 files changed

Lines changed: 177 additions & 82 deletions

spec/System/TestTradeQueryCurrency_spec.lua

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,6 @@ describe("TradeQuery Currency Conversion", function()
2323
end)
2424
end)
2525

26-
describe("ReduceOutput", function()
27-
it("uses selected minion stats for weighted result comparison", function()
28-
mock_tradeQuery.statSortSelectionList = { { stat = "AverageDamage" } }
29-
30-
local result = mock_tradeQuery:ReduceOutput({
31-
AverageDamage = 10,
32-
Life = 100,
33-
Minion = {
34-
AverageDamage = 250,
35-
Life = 200,
36-
},
37-
})
38-
39-
assert.are.equals(250, result.AverageDamage)
40-
assert.is_nil(result.Life)
41-
end)
42-
end)
43-
4426
describe("PriceBuilderProcessPoENinjaResponse", function()
4527
-- Pass: Processes without error, restoring map while adding a notice
4628
-- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions

spec/System/TestTradeQueryGenerator_spec.lua

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,31 @@ describe("TradeQueryGenerator", function()
3535
assert.are.equal(result, 100)
3636
end)
3737

38-
it("uses minion output for non-FullDPS stats when minion output is available", function()
39-
local baseOutput = { AverageDamage = 10, Minion = { AverageDamage = 100 } }
40-
local newOutput = { AverageDamage = 10, Minion = { AverageDamage = 250 } }
41-
local statWeights = { { stat = "AverageDamage", weightMult = 1 } }
38+
it("uses minion output for non-FullDPS stats when minion output is desired", function()
39+
local baseOutput = { Life = 10, Minion = { Life = 100 } }
40+
local newOutput = { Life = 10, Minion = { Life = 250 } }
41+
local statWeights = { { stat = "MinionLife", weightMult = 1 } }
4242
data.misc.maxStatIncrease = 1000
4343

4444
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
4545

4646
assert.are.equal(result, 2.5)
4747
end)
4848

49-
it("uses player output for FullDPS even when minion output is available", function()
49+
it("uses lower is better stats correctly", function()
50+
local baseOutput = { MaxHit = 100 }
51+
local newOutput = { MaxHit = 10 }
52+
local statWeights = { { stat = "MaxHit", weightMult = 1, transform = function(number) return -number end } }
53+
data.misc.maxStatIncrease = 1000
54+
55+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
56+
57+
local close_enough = math.abs(result - -0.1) < 0.0001
58+
assert.True(close_enough)
59+
end)
60+
61+
it("uses player and minion output for FullDPS", function()
62+
-- minion output gets assigned to the player's full dps in reality
5063
local baseOutput = { FullDPS = 100, Minion = { FullDPS = 100 } }
5164
local newOutput = { FullDPS = 250, Minion = { FullDPS = 1000 } }
5265
local statWeights = { { stat = "FullDPS", weightMult = 1 } }
@@ -57,6 +70,16 @@ describe("TradeQueryGenerator", function()
5770
assert.are.equal(result, 2.5)
5871
end)
5972

73+
it("uses player output for non-FullDPS even when minion output is available", function()
74+
local baseOutput = { Life = 100, Minion = { Life = 100 } }
75+
local newOutput = { Life = 250, Minion = { Life = 1000 } }
76+
local statWeights = { { stat = "Life", weightMult = 1 } }
77+
data.misc.maxStatIncrease = 1000
78+
79+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
80+
assert.are.equal(result, 2.5)
81+
end)
82+
6083
it("uses the fallback DPS ratio once when FullDPS is unavailable", function()
6184
local baseOutput = { Minion = { TotalDPS = 10, TotalDotDPS = 0, CombinedDPS = 10 } }
6285
local newOutput = { Minion = { TotalDPS = 25, TotalDotDPS = 0, CombinedDPS = 25 } }
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
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)
9+
10+
describe("ReduceOutput", function()
11+
it("uses selected minion stats for weighted result comparison", function()
12+
mock_tradeQuery.statSortSelectionList = { { stat = "AverageDamage" } }
13+
14+
local result = mock_tradeQuery:ReduceOutput({
15+
AverageDamage = 10,
16+
Life = 100,
17+
Minion = {
18+
AverageDamage = 250,
19+
Life = 200,
20+
},
21+
})
22+
23+
assert.are.equals(260, result.AverageDamage)
24+
assert.is_nil(result.Life)
25+
end)
26+
27+
it("keeps fallback DPS stats when FullDPS is selected but not present", function()
28+
mock_tradeQuery.statSortSelectionList = { { stat = "FullDPS", weightMult = 1 } }
29+
30+
local baseOutput = {
31+
CombinedDPS = 100,
32+
TotalDPS = 100,
33+
TotalDotDPS = 0,
34+
}
35+
local reducedOutput = mock_tradeQuery:ReduceOutput({
36+
CombinedDPS = 120,
37+
TotalDPS = 120,
38+
TotalDotDPS = 0,
39+
})
40+
41+
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, reducedOutput, mock_tradeQuery.statSortSelectionList)
42+
43+
assert.are.equals(1.2, result)
44+
end)
45+
end)
46+
end)

src/Classes/CalcsTab.lua

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

674674
function CalcsTabClass:CalculatePowerStat(selection, original, modified)
675-
if modified.Minion and selection.stat ~= "FullDPS" then
676-
original = original.Minion
677-
modified = modified.Minion
678-
end
679-
local originalValue = original[selection.stat] or 0
680-
local modifiedValue = modified[selection.stat] or 0
681-
if selection.transform then
682-
originalValue = selection.transform(originalValue)
683-
modifiedValue = selection.transform(modifiedValue)
684-
end
675+
local originalValue = data.powerStatList.GetFromOutput(original, selection)
676+
local modifiedValue = data.powerStatList.GetFromOutput(modified, selection)
685677
return originalValue - modifiedValue
686678
end
687679

@@ -692,10 +684,9 @@ function CalcsTabClass:CalculateCombinedOffDefStat(original, modified)
692684
(original.Evasion - modified.Evasion) / m_max(10000, modified.Evasion) +
693685
(original.LifeRegenRecovery - modified.LifeRegenRecovery) / 500 +
694686
(original.EnergyShieldRegenRecovery - modified.EnergyShieldRegenRecovery) / 1000
695-
if modified.Minion then
696-
return (original.Minion.CombinedDPS - modified.Minion.CombinedDPS) / modified.Minion.CombinedDPS, defence
697-
end
698-
return (original.CombinedDPS - modified.CombinedDPS) / modified.CombinedDPS, defence
687+
local modifiedDps = modified.CombinedDPS + (modified.Minion and modified.Minion.CombinedDPS or 0)
688+
local dpsIncr = original.CombinedDPS + (original.Minion and original.Minion.CombinedDPS or 0) - modifiedDps
689+
return dpsIncr / modifiedDps, defence
699690
end
700691

701692
function CalcsTabClass:GetNodeCalculator()

src/Classes/CompareTab.lua

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

26542654
-- Get baseline stat value for percentage calculation
2655-
local baseStatValue = calcBase[powerStat.stat] or 0
2656-
if powerStat.transform then
2657-
baseStatValue = powerStat.transform(baseStatValue)
2658-
end
2655+
local baseStatValue = data.powerStatList.GetFromOutput(calcBase, powerStat)
26592656

26602657
-- Helper to format an impact value and compute percentage
26612658
local function formatImpact(impact)

src/Classes/ItemDBControl.lua

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ end
202202

203203
function ItemDBClass:BuildSortOrder()
204204
wipeTable(self.sortDropList)
205-
for id,stat in pairs(data.powerStatList) do
205+
for id, stat in ipairs(data.powerStatList) do
206206
if not stat.ignoreForItems then
207207
t_insert(self.sortDropList, {
208208
label="Sort by "..stat.label,
@@ -243,10 +243,7 @@ function ItemDBClass:ListBuilder()
243243
for slotName, slot in pairs(self.itemsTab.slots) do
244244
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
245245
local output = calcFunc(item.base.flask and { toggleFlask = item } or item.base.charm and { toggleCharm = item } or { repSlotName = slotName, repItem = item }, useFullDPS)
246-
local measuredPower = output.Minion and output.Minion[self.sortMode] or output[self.sortMode] or 0
247-
if self.sortDetail.transform then
248-
measuredPower = self.sortDetail.transform(measuredPower)
249-
end
246+
local measuredPower = data.powerStatList.GetFromOutput(output, self.sortDetail)
250247
item.measuredPower = item.measuredPower and m_max(item.measuredPower, measuredPower) or measuredPower
251248
end
252249
end

src/Classes/ItemsTab.lua

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,15 @@ function ItemsTabClass:Load(xml, dbFileName)
11451145
stat = child.attrib.stat,
11461146
weightMult = tonumber(child.attrib.weightMult)
11471147
}
1148-
t_insert(self.tradeQuery.statSortSelectionList, statSort)
1148+
for _, statEntry in ipairs(data.powerStatList) do
1149+
if statSort.stat == statEntry.stat then
1150+
-- update information which can be out of data or missing in the xml
1151+
statSort.label = statEntry.label
1152+
statSort.transform = statEntry.transform
1153+
t_insert(self.tradeQuery.statSortSelectionList, statSort)
1154+
break
1155+
end
1156+
end
11491157
end
11501158
end
11511159
end

src/Classes/NotableDBControl.lua

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ end
142142

143143
function NotableDBClass:BuildSortOrder()
144144
wipeTable(self.sortDropList)
145-
for id,stat in pairs(data.powerStatList) do
145+
for id, stat in ipairs(data.powerStatList) do
146146
if not stat.ignoreForItems then
147147
t_insert(self.sortDropList, {
148148
label="Sort by "..stat.label,
@@ -166,16 +166,8 @@ function NotableDBClass:BuildSortOrder()
166166
end
167167

168168
function NotableDBClass:CalculatePowerStat(selection, original, modified)
169-
if modified.Minion then
170-
original = original.Minion
171-
modified = modified.Minion
172-
end
173-
local originalValue = original[selection.stat] or 0
174-
local modifiedValue = modified[selection.stat] or 0
175-
if selection.transform then
176-
originalValue = selection.transform(originalValue)
177-
modifiedValue = selection.transform(modifiedValue)
178-
end
169+
local originalValue = data.powerStatList.GetFromOutput(original, selection)
170+
local modifiedValue = data.powerStatList.GetFromOutput(modified, selection)
179171
return originalValue - modifiedValue
180172
end
181173

src/Classes/TradeQuery.lua

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -623,11 +623,16 @@ function TradeQueryClass:SetStatWeights(previousSelectionList)
623623
local controls = { }
624624
local statList = { }
625625
local sliderController = { index = 1 }
626-
local popupHeight = 285
626+
local popupHeight = 500
627627

628-
controls.ListControl = new("TradeStatWeightMultiplierListControl", {"TOPLEFT", nil, "TOPRIGHT"}, {-410, 45, 400, 200}, statList, sliderController)
628+
local listYOffset = 45
629+
-- account for top gap, bottom button size and gap, and a gap before buttons
630+
local listHeight = popupHeight - 45 - 30 - 10
629631

630-
for id, stat in pairs(data.powerStatList) do
632+
controls.ListControl = new("TradeStatWeightMultiplierListControl", { "TOPLEFT", nil, "TOPRIGHT" },
633+
{ -410, 45, 400, listHeight }, statList, sliderController)
634+
635+
for _, stat in ipairs(data.powerStatList) do
631636
if not stat.ignoreForItems and stat.label ~= "Name" then
632637
t_insert(statList, {
633638
label = "0 : "..stat.label,
@@ -774,7 +779,12 @@ end
774779
function TradeQueryClass:ReduceOutput(output)
775780
local smallOutput = {}
776781
for _, statTable in ipairs(self.statSortSelectionList) do
777-
smallOutput[statTable.stat] = output.Minion and output.Minion[statTable.stat] or output[statTable.stat]
782+
smallOutput[statTable.stat] = data.powerStatList.GetFromOutput(output, statTable)
783+
if statTable.stat == "FullDPS" and not output.FullDPS then
784+
smallOutput.TotalDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDPS" })
785+
smallOutput.TotalDotDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDotDPS" })
786+
smallOutput.CombinedDPS = data.powerStatList.GetFromOutput(output, { stat = "CombinedDPS" })
787+
end
778788
end
779789
return smallOutput
780790
end

src/Classes/TradeQueryGenerator.lua

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -157,26 +157,13 @@ end
157157

158158
function TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
159159
local meanStatDiff = 0
160-
local function getOutputStatValue(output, stat)
161-
if stat == "FullDPS" then
162-
if output[stat] ~= nil then
163-
return output[stat]
164-
end
165-
if output.Minion and output.Minion.CombinedDPS ~= nil then
166-
return output.Minion.CombinedDPS
167-
end
168-
end
169-
if output.Minion and output.Minion[stat] ~= nil then
170-
return output.Minion[stat]
171-
end
172-
return output[stat] or 0
173-
end
160+
174161
local function ratioModSums(...)
175162
local baseModSum = 0
176163
local newModSum = 0
177164
for _, mod in ipairs({ ... }) do
178-
baseModSum = baseModSum + getOutputStatValue(baseOutput, mod)
179-
newModSum = newModSum + getOutputStatValue(newOutput, mod)
165+
baseModSum = baseModSum + data.powerStatList.GetFromOutput(baseOutput, mod, true)
166+
newModSum = newModSum + data.powerStatList.GetFromOutput(newOutput, mod, true)
180167
end
181168

182169
if baseModSum == math.huge then
@@ -192,9 +179,9 @@ function TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, newOutput, st
192179
for _, statTable in ipairs(statWeights) do
193180
local modSumRatio
194181
if statTable.stat == "FullDPS" and not (baseOutput["FullDPS"] and newOutput["FullDPS"]) then
195-
modSumRatio = ratioModSums("TotalDPS", "TotalDotDPS", "CombinedDPS")
182+
modSumRatio = ratioModSums({ stat = "TotalDPS" }, { stat = "TotalDotDPS" }, { stat = "CombinedDPS" })
196183
else
197-
modSumRatio = ratioModSums(statTable.stat)
184+
modSumRatio = ratioModSums(statTable)
198185
end
199186
-- some weights, such as damage taken from hit need to be negated as lower is better for them
200187
if statTable.transform then

0 commit comments

Comments
 (0)