From cf89a6131f627de64e4945c6f0898a2a393d6e44 Mon Sep 17 00:00:00 2001 From: vaisest Date: Fri, 26 Jun 2026 14:31:39 +0000 Subject: [PATCH 1/5] Apply changes from https://github.com/PathOfBuildingCommunity/PathOfBuilding-PoE2/pull/2261 --- .../TestTradeQueryCurrency_spec.lua.rej | 26 +++++++++++++ .../TestTradeQueryGenerator_spec.lua.rej | 10 +++++ spec/System/TestTradeQuery_spec.lua.rej | 38 +++++++++++++++++++ src/Classes/CalcsTab.lua | 19 +++------- src/Classes/CompareTab.lua | 5 +-- src/Classes/ItemDBControl.lua | 2 +- src/Classes/ItemDBControl.lua.rej | 11 ++++++ src/Classes/ItemsTab.lua | 10 ++++- src/Classes/NotableDBControl.lua | 14 ++----- src/Classes/TradeQuery.lua | 11 ++++-- src/Classes/TradeQuery.lua.rej | 13 +++++++ src/Classes/TradeQueryGenerator.lua.rej | 12 ++++++ .../TradeStatWeightMultiplierListControl.lua | 2 +- src/Classes/TreeTab.lua.rej | 10 +++++ src/Modules/Data.lua | 10 +++++ src/Modules/Data.lua.rej | 10 +++++ 16 files changed, 168 insertions(+), 35 deletions(-) create mode 100644 spec/System/TestTradeQueryCurrency_spec.lua.rej create mode 100644 spec/System/TestTradeQueryGenerator_spec.lua.rej create mode 100644 spec/System/TestTradeQuery_spec.lua.rej create mode 100644 src/Classes/ItemDBControl.lua.rej create mode 100644 src/Classes/TradeQuery.lua.rej create mode 100644 src/Classes/TradeQueryGenerator.lua.rej create mode 100644 src/Classes/TreeTab.lua.rej create mode 100644 src/Modules/Data.lua.rej diff --git a/spec/System/TestTradeQueryCurrency_spec.lua.rej b/spec/System/TestTradeQueryCurrency_spec.lua.rej new file mode 100644 index 00000000000..870856ac481 --- /dev/null +++ b/spec/System/TestTradeQueryCurrency_spec.lua.rej @@ -0,0 +1,26 @@ +diff a/spec/System/TestTradeQueryCurrency_spec.lua b/spec/System/TestTradeQueryCurrency_spec.lua (rejected hunks) +@@ -23,24 +23,6 @@ describe("TradeQuery Currency Conversion", function() + end) + end) + +- describe("ReduceOutput", function() +- it("uses selected minion stats for weighted result comparison", function() +- mock_tradeQuery.statSortSelectionList = { { stat = "AverageDamage" } } +- +- local result = mock_tradeQuery:ReduceOutput({ +- AverageDamage = 10, +- Life = 100, +- Minion = { +- AverageDamage = 250, +- Life = 200, +- }, +- }) +- +- assert.are.equals(250, result.AverageDamage) +- assert.is_nil(result.Life) +- end) +- end) +- + describe("PriceBuilderProcessPoENinjaResponse", function() + -- Pass: Processes without error, restoring map while adding a notice + -- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions diff --git a/spec/System/TestTradeQueryGenerator_spec.lua.rej b/spec/System/TestTradeQueryGenerator_spec.lua.rej new file mode 100644 index 00000000000..4360660c03b --- /dev/null +++ b/spec/System/TestTradeQueryGenerator_spec.lua.rej @@ -0,0 +1,10 @@ +diff a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua (rejected hunks) +@@ -54,7 +54,7 @@ describe("TradeQueryGenerator", function() + + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + +- local close_enough = (result - -0.1) < 0.0001 ++ local close_enough = math.abs(result - -0.1) < 0.0001 + assert.True(close_enough) + end) + diff --git a/spec/System/TestTradeQuery_spec.lua.rej b/spec/System/TestTradeQuery_spec.lua.rej new file mode 100644 index 00000000000..7d2c443fde9 --- /dev/null +++ b/spec/System/TestTradeQuery_spec.lua.rej @@ -0,0 +1,38 @@ +diff a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua (rejected hunks) +@@ -1,8 +1,10 @@ + describe("TradeQuery", function () + local mock_tradeQuery ++ local mock_queryGen + + before_each(function() + mock_tradeQuery = new("TradeQuery", { itemsTab = {} }) ++ mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} }) + end) + + describe("ReduceOutput", function() +@@ -21,5 +23,24 @@ describe("TradeQuery", function () + assert.are.equals(260, result.AverageDamage) + assert.is_nil(result.Life) + end) ++ ++ it("keeps fallback DPS stats when FullDPS is selected but not present", function() ++ mock_tradeQuery.statSortSelectionList = { { stat = "FullDPS", weightMult = 1 } } ++ ++ local baseOutput = { ++ CombinedDPS = 100, ++ TotalDPS = 100, ++ TotalDotDPS = 0, ++ } ++ local reducedOutput = mock_tradeQuery:ReduceOutput({ ++ CombinedDPS = 120, ++ TotalDPS = 120, ++ TotalDotDPS = 0, ++ }) ++ ++ local result = mock_queryGen.WeightedRatioOutputs(baseOutput, reducedOutput, mock_tradeQuery.statSortSelectionList) ++ ++ assert.are.equals(1.2, result) ++ end) + end) + end) +\ No newline at end of file diff --git a/src/Classes/CalcsTab.lua b/src/Classes/CalcsTab.lua index 690103ba096..a7139797377 100644 --- a/src/Classes/CalcsTab.lua +++ b/src/Classes/CalcsTab.lua @@ -712,16 +712,8 @@ function CalcsTabClass:PowerBuilder() end function CalcsTabClass:CalculatePowerStat(selection, original, modified) - if modified.Minion and selection.stat ~= "FullDPS" then - original = original.Minion - modified = modified.Minion - end - local originalValue = original[selection.stat] or 0 - local modifiedValue = modified[selection.stat] or 0 - if selection.transform then - originalValue = selection.transform(originalValue) - modifiedValue = selection.transform(modifiedValue) - end + local originalValue = data.powerStatList.GetFromOutput(original, selection) + local modifiedValue = data.powerStatList.GetFromOutput(modified, selection) return originalValue - modifiedValue end @@ -732,10 +724,9 @@ function CalcsTabClass:CalculateCombinedOffDefStat(original, modified) (original.Evasion - modified.Evasion) / m_max(10000, modified.Evasion) + (original.LifeRegenRecovery - modified.LifeRegenRecovery) / 500 + (original.EnergyShieldRegenRecovery - modified.EnergyShieldRegenRecovery) / 1000 - if modified.Minion then - return (original.Minion.CombinedDPS - modified.Minion.CombinedDPS) / modified.Minion.CombinedDPS, defence - end - return (original.CombinedDPS - modified.CombinedDPS) / modified.CombinedDPS, defence + local modifiedDps = modified.CombinedDPS + (modified.Minion and modified.Minion.CombinedDPS or 0) + local dpsIncr = original.CombinedDPS + (original.Minion and original.Minion.CombinedDPS or 0) - modifiedDps + return dpsIncr / modifiedDps, defence end function CalcsTabClass:GetNodeCalculator() diff --git a/src/Classes/CompareTab.lua b/src/Classes/CompareTab.lua index 24506313c6c..946c580f4ba 100644 --- a/src/Classes/CompareTab.lua +++ b/src/Classes/CompareTab.lua @@ -2544,10 +2544,7 @@ function CompareTabClass:ComparePowerBuilder(compareEntry, powerStat, categories end -- Get baseline stat value for percentage calculation - local baseStatValue = calcBase[powerStat.stat] or 0 - if powerStat.transform then - baseStatValue = powerStat.transform(baseStatValue) - end + local baseStatValue = data.powerStatList.GetFromOutput(calcBase, powerStat) -- Helper to format an impact value and compute percentage local function formatImpact(impact) diff --git a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua index 3136d1a8476..0fc8303b2f3 100644 --- a/src/Classes/ItemDBControl.lua +++ b/src/Classes/ItemDBControl.lua @@ -190,7 +190,7 @@ end function ItemDBClass:BuildSortOrder() wipeTable(self.sortDropList) - for id,stat in pairs(data.powerStatList) do + for id, stat in ipairs(data.powerStatList) do if not stat.ignoreForItems then t_insert(self.sortDropList, { label="Sort by "..stat.label, diff --git a/src/Classes/ItemDBControl.lua.rej b/src/Classes/ItemDBControl.lua.rej new file mode 100644 index 00000000000..bb65957caa8 --- /dev/null +++ b/src/Classes/ItemDBControl.lua.rej @@ -0,0 +1,11 @@ +diff a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua (rejected hunks) +@@ -244,9 +244,6 @@ function ItemDBClass:ListBuilder() + 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 + local output = calcFunc(item.base.flask and { toggleFlask = item } or item.base.charm and { toggleCharm = item } or { repSlotName = slotName, repItem = item }, useFullDPS) + local measuredPower = data.powerStatList.GetFromOutput(output, self.sortDetail) +- if self.sortDetail.transform then +- measuredPower = self.sortDetail.transform(measuredPower) +- end + item.measuredPower = item.measuredPower and m_max(item.measuredPower, measuredPower) or measuredPower + end + end diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index e6083ea7839..6320011f410 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -1122,7 +1122,15 @@ function ItemsTabClass:Load(xml, dbFileName) stat = child.attrib.stat, weightMult = tonumber(child.attrib.weightMult) } - t_insert(self.tradeQuery.statSortSelectionList, statSort) + for _, statEntry in ipairs(data.powerStatList) do + if statSort.stat == statEntry.stat then + -- update information which can be out of data or missing in the xml + statSort.label = statEntry.label + statSort.transform = statEntry.transform + t_insert(self.tradeQuery.statSortSelectionList, statSort) + break + end + end end end end diff --git a/src/Classes/NotableDBControl.lua b/src/Classes/NotableDBControl.lua index d08ec4080c9..e3fb85b5bb4 100644 --- a/src/Classes/NotableDBControl.lua +++ b/src/Classes/NotableDBControl.lua @@ -87,7 +87,7 @@ end function NotableDBClass:BuildSortOrder() wipeTable(self.sortDropList) - for id,stat in pairs(data.powerStatList) do + for id, stat in ipairs(data.powerStatList) do if not stat.ignoreForItems then t_insert(self.sortDropList, { label="Sort by "..stat.label, @@ -111,16 +111,8 @@ function NotableDBClass:BuildSortOrder() end function NotableDBClass:CalculatePowerStat(selection, original, modified) - if modified.Minion then - original = original.Minion - modified = modified.Minion - end - local originalValue = original[selection.stat] or 0 - local modifiedValue = modified[selection.stat] or 0 - if selection.transform then - originalValue = selection.transform(originalValue) - modifiedValue = selection.transform(modifiedValue) - end + local originalValue = data.powerStatList.GetFromOutput(original, selection) + local modifiedValue = data.powerStatList.GetFromOutput(modified, selection) return originalValue - modifiedValue end diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 9e1308bfb9e..697fa84f879 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -553,11 +553,16 @@ function TradeQueryClass:SetStatWeights(previousSelectionList) local controls = { } local statList = { } local sliderController = { index = 1 } - local popupHeight = 285 + local popupHeight = 500 - controls.ListControl = new("TradeStatWeightMultiplierListControl", {"TOPLEFT", nil, "TOPRIGHT"}, {-410, 45, 400, 200}, statList, sliderController) + local listYOffset = 45 + -- account for top gap, bottom button size and gap, and a gap before buttons + local listHeight = popupHeight - 45 - 30 - 10 - for id, stat in pairs(data.powerStatList) do + controls.ListControl = new("TradeStatWeightMultiplierListControl", { "TOPLEFT", nil, "TOPRIGHT" }, + { -410, 45, 400, listHeight }, statList, sliderController) + + for _, stat in ipairs(data.powerStatList) do if not stat.ignoreForItems and stat.label ~= "Name" then t_insert(statList, { label = "0 : "..stat.label, diff --git a/src/Classes/TradeQuery.lua.rej b/src/Classes/TradeQuery.lua.rej new file mode 100644 index 00000000000..20900eede40 --- /dev/null +++ b/src/Classes/TradeQuery.lua.rej @@ -0,0 +1,13 @@ +diff a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua (rejected hunks) +@@ -780,6 +780,11 @@ function TradeQueryClass:ReduceOutput(output) + local smallOutput = {} + for _, statTable in ipairs(self.statSortSelectionList) do + smallOutput[statTable.stat] = data.powerStatList.GetFromOutput(output, statTable) ++ if statTable.stat == "FullDPS" and not output.FullDPS then ++ smallOutput.TotalDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDPS" }) ++ smallOutput.TotalDotDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDotDPS" }) ++ smallOutput.CombinedDPS = data.powerStatList.GetFromOutput(output, { stat = "CombinedDPS" }) ++ end + end + return smallOutput + end diff --git a/src/Classes/TradeQueryGenerator.lua.rej b/src/Classes/TradeQueryGenerator.lua.rej new file mode 100644 index 00000000000..9255b3fe21c --- /dev/null +++ b/src/Classes/TradeQueryGenerator.lua.rej @@ -0,0 +1,12 @@ +diff a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua (rejected hunks) +@@ -162,8 +162,8 @@ function TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, newOutput, st + local baseModSum = 0 + local newModSum = 0 + for _, mod in ipairs({ ... }) do +- baseModSum = baseModSum + data.powerStatList.GetFromOutput(baseOutput, mod) +- newModSum = newModSum + data.powerStatList.GetFromOutput(newOutput, mod) ++ baseModSum = baseModSum + data.powerStatList.GetFromOutput(baseOutput, mod, true) ++ newModSum = newModSum + data.powerStatList.GetFromOutput(newOutput, mod, true) + end + + if baseModSum == math.huge then diff --git a/src/Classes/TradeStatWeightMultiplierListControl.lua b/src/Classes/TradeStatWeightMultiplierListControl.lua index 0d528470f53..f0260d89de0 100644 --- a/src/Classes/TradeStatWeightMultiplierListControl.lua +++ b/src/Classes/TradeStatWeightMultiplierListControl.lua @@ -25,7 +25,7 @@ end function TradeStatWeightMultiplierListControlClass:AddValueTooltip(tooltip, index, data) tooltip:Clear() if not self.noTooltip then - tooltip:AddLine(16, "^7Double click to modify this stats weight multiplier.") + tooltip:AddLine(16, "^7Click to modify this stats weight multiplier.") end end diff --git a/src/Classes/TreeTab.lua.rej b/src/Classes/TreeTab.lua.rej new file mode 100644 index 00000000000..48703412368 --- /dev/null +++ b/src/Classes/TreeTab.lua.rej @@ -0,0 +1,10 @@ +diff a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua (rejected hunks) +@@ -1850,7 +1850,7 @@ function TreeTabClass:FindTimelessJewel() + + controls.fallbackWeightsLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 225, 0, 16}, "^7Fallback Weight Mode:") + local fallbackWeightsList = { } +- for id, stat in pairs(data.powerStatList) do ++ for _, stat in ipairs(data.powerStatList) do + if not stat.ignoreForItems and stat.label ~= "Name" then + t_insert(fallbackWeightsList, { + label = "Sort by " .. stat.label, diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index a880019c7f8..c581cb661ff 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -108,6 +108,16 @@ data = { } -- Misc data tables LoadModule("Data/Misc", data) +---@class StatTable +---@field stat? string stat ID +---@field label string A short description of the stat +---@field transform fun(in: number|string): number|string A function to e.g. invert the value, if the stat represents something where lower is better +---@field combinedOffDef? boolean +---@field ignoreForNodes? boolean +---@field ignoreForItems? boolean +---@field reverseSort? boolean + +---@type StatTable[] data.powerStatList = { { stat=nil, label="Offence/Defence", combinedOffDef=true, ignoreForItems=true }, { stat=nil, label="Name", itemField="Name", ignoreForNodes=true, reverseSort=true, transform=function(value) return value:gsub("^The ","") end}, diff --git a/src/Modules/Data.lua.rej b/src/Modules/Data.lua.rej new file mode 100644 index 00000000000..0b84cbfa092 --- /dev/null +++ b/src/Modules/Data.lua.rej @@ -0,0 +1,10 @@ +diff a/src/Modules/Data.lua b/src/Modules/Data.lua (rejected hunks) +@@ -189,7 +189,7 @@ function data.powerStatList.GetFromOutput(output, statTable, skipTransform) + return output[statTable.stat] or 0 + end + -- if the user doesn't have full dps, we default to adding the player and minion dps together +- return (output.CombinedDPS or 0) + (output.Minion and output.Minion.CombinedDPS) ++ return (output.CombinedDPS or 0) + (output.Minion and output.Minion.CombinedDPS or 0) + end + -- minion-only stats + local minionStat = statTable.stat:match("^Minion(.+)") From d8d01f2173a9ec3ea607d8b75c4762b0d4a5d0f1 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:37:59 +0300 Subject: [PATCH 2/5] Fix merge conflicts --- .../TestTradeQueryCurrency_spec.lua.rej | 26 -------- spec/System/TestTradeQueryGenerator_spec.lua | 66 +++++++++++++++++++ .../TestTradeQueryGenerator_spec.lua.rej | 10 --- spec/System/TestTradeQuery_spec.lua | 44 +++++++++++++ spec/System/TestTradeQuery_spec.lua.rej | 38 ----------- src/Classes/ItemDBControl.lua | 5 +- src/Classes/ItemDBControl.lua.rej | 11 ---- src/Classes/TradeQuery.lua | 7 +- src/Classes/TradeQuery.lua.rej | 13 ---- src/Classes/TradeQueryGenerator.lua | 16 +++-- src/Classes/TradeQueryGenerator.lua.rej | 12 ---- src/Classes/TreeTab.lua | 2 +- src/Classes/TreeTab.lua.rej | 10 --- src/Modules/Data.lua | 56 +++++++++++++++- src/Modules/Data.lua.rej | 10 --- 15 files changed, 185 insertions(+), 141 deletions(-) delete mode 100644 spec/System/TestTradeQueryCurrency_spec.lua.rej delete mode 100644 spec/System/TestTradeQueryGenerator_spec.lua.rej delete mode 100644 spec/System/TestTradeQuery_spec.lua.rej delete mode 100644 src/Classes/ItemDBControl.lua.rej delete mode 100644 src/Classes/TradeQuery.lua.rej delete mode 100644 src/Classes/TradeQueryGenerator.lua.rej delete mode 100644 src/Classes/TreeTab.lua.rej delete mode 100644 src/Modules/Data.lua.rej diff --git a/spec/System/TestTradeQueryCurrency_spec.lua.rej b/spec/System/TestTradeQueryCurrency_spec.lua.rej deleted file mode 100644 index 870856ac481..00000000000 --- a/spec/System/TestTradeQueryCurrency_spec.lua.rej +++ /dev/null @@ -1,26 +0,0 @@ -diff a/spec/System/TestTradeQueryCurrency_spec.lua b/spec/System/TestTradeQueryCurrency_spec.lua (rejected hunks) -@@ -23,24 +23,6 @@ describe("TradeQuery Currency Conversion", function() - end) - end) - -- describe("ReduceOutput", function() -- it("uses selected minion stats for weighted result comparison", function() -- mock_tradeQuery.statSortSelectionList = { { stat = "AverageDamage" } } -- -- local result = mock_tradeQuery:ReduceOutput({ -- AverageDamage = 10, -- Life = 100, -- Minion = { -- AverageDamage = 250, -- Life = 200, -- }, -- }) -- -- assert.are.equals(250, result.AverageDamage) -- assert.is_nil(result.Life) -- end) -- end) -- - describe("PriceBuilderProcessPoENinjaResponse", function() - -- Pass: Processes without error, restoring map while adding a notice - -- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index befb96a6570..a7e937b4d78 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -35,6 +35,72 @@ describe("TradeQueryGenerator", function() local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) assert.are.equal(result, 100) end) + it("uses minion output for non-FullDPS stats when minion output is desired", function() + local baseOutput = { Life = 10, Minion = { Life = 100 } } + local newOutput = { Life = 10, Minion = { Life = 250 } } + local statWeights = { { stat = "MinionLife", weightMult = 1 } } + data.misc.maxStatIncrease = 1000 + + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + + assert.are.equal(result, 2.5) + end) + + it("uses lower is better stats correctly", function() + local baseOutput = { MaxHit = 100 } + local newOutput = { MaxHit = 10 } + local statWeights = { { stat = "MaxHit", weightMult = 1, transform = function(number) return -number end } } + data.misc.maxStatIncrease = 1000 + + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + + local close_enough = math.abs(result - -0.1) < 0.0001 + assert.True(close_enough) + end) + + it("uses player and minion output for FullDPS", function() + -- minion output gets assigned to the player's full dps in reality + local baseOutput = { FullDPS = 100, Minion = { FullDPS = 100 } } + local newOutput = { FullDPS = 250, Minion = { FullDPS = 1000 } } + local statWeights = { { stat = "FullDPS", weightMult = 1 } } + data.misc.maxStatIncrease = 1000 + + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + + assert.are.equal(result, 2.5) + end) + + it("uses player output for non-FullDPS even when minion output is available", function() + local baseOutput = { Life = 100, Minion = { Life = 100 } } + local newOutput = { Life = 250, Minion = { Life = 1000 } } + local statWeights = { { stat = "Life", weightMult = 1 } } + data.misc.maxStatIncrease = 1000 + + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + assert.are.equal(result, 2.5) + end) + + it("uses the fallback DPS ratio once when FullDPS is unavailable", function() + local baseOutput = { Minion = { TotalDPS = 10, TotalDotDPS = 0, CombinedDPS = 10 } } + local newOutput = { Minion = { TotalDPS = 25, TotalDotDPS = 0, CombinedDPS = 25 } } + local statWeights = { { stat = "FullDPS", weightMult = 1 } } + data.misc.maxStatIncrease = 1000 + + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + + assert.are.equal(result, 2.5) + end) + + it("falls back to player output when the selected stat is not on minion output", function() + local baseOutput = { Spirit = 100, Minion = { AverageDamage = 100 } } + local newOutput = { Spirit = 120, Minion = { AverageDamage = 100 } } + local statWeights = { { stat = "Spirit", weightMult = 1 } } + data.misc.maxStatIncrease = 1000 + + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + + assert.are.equal(result, 1.2) + end) end) describe("Filter prioritization", function() diff --git a/spec/System/TestTradeQueryGenerator_spec.lua.rej b/spec/System/TestTradeQueryGenerator_spec.lua.rej deleted file mode 100644 index 4360660c03b..00000000000 --- a/spec/System/TestTradeQueryGenerator_spec.lua.rej +++ /dev/null @@ -1,10 +0,0 @@ -diff a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua (rejected hunks) -@@ -54,7 +54,7 @@ describe("TradeQueryGenerator", function() - - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) - -- local close_enough = (result - -0.1) < 0.0001 -+ local close_enough = math.abs(result - -0.1) < 0.0001 - assert.True(close_enough) - end) - diff --git a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua index 332374a8395..31a62e94ded 100644 --- a/spec/System/TestTradeQuery_spec.lua +++ b/spec/System/TestTradeQuery_spec.lua @@ -1,4 +1,11 @@ describe("TradeQuery", function() + local mock_tradeQuery + local mock_queryGen + + before_each(function() + mock_tradeQuery = new("TradeQuery", { itemsTab = {} }) + mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} }) + end) describe("result dropdown tooltipFunc", function() -- Builds a TradeQuery with the strict minimum needed for -- PriceItemRowDisplay to construct row 1 without exploding. Only the @@ -54,4 +61,41 @@ describe("TradeQuery", function() assert.are.equal(0, #tooltip.lines) end) end) + describe("ReduceOutput", function() + it("uses selected minion stats for weighted result comparison", function() + mock_tradeQuery.statSortSelectionList = { { stat = "AverageDamage" } } + + local result = mock_tradeQuery:ReduceOutput({ + AverageDamage = 10, + Life = 100, + Minion = { + AverageDamage = 250, + Life = 200, + }, + }) + + assert.are.equals(260, result.AverageDamage) + assert.is_nil(result.Life) + end) + + it("keeps fallback DPS stats when FullDPS is selected but not present", function() + mock_tradeQuery.statSortSelectionList = { { stat = "FullDPS", weightMult = 1 } } + + local baseOutput = { + CombinedDPS = 100, + TotalDPS = 100, + TotalDotDPS = 0, + } + local reducedOutput = mock_tradeQuery:ReduceOutput({ + CombinedDPS = 120, + TotalDPS = 120, + TotalDotDPS = 0, + }) + + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, reducedOutput, + mock_tradeQuery.statSortSelectionList) + + assert.are.equals(1.2, result) + end) + end) end) diff --git a/spec/System/TestTradeQuery_spec.lua.rej b/spec/System/TestTradeQuery_spec.lua.rej deleted file mode 100644 index 7d2c443fde9..00000000000 --- a/spec/System/TestTradeQuery_spec.lua.rej +++ /dev/null @@ -1,38 +0,0 @@ -diff a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua (rejected hunks) -@@ -1,8 +1,10 @@ - describe("TradeQuery", function () - local mock_tradeQuery -+ local mock_queryGen - - before_each(function() - mock_tradeQuery = new("TradeQuery", { itemsTab = {} }) -+ mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} }) - end) - - describe("ReduceOutput", function() -@@ -21,5 +23,24 @@ describe("TradeQuery", function () - assert.are.equals(260, result.AverageDamage) - assert.is_nil(result.Life) - end) -+ -+ it("keeps fallback DPS stats when FullDPS is selected but not present", function() -+ mock_tradeQuery.statSortSelectionList = { { stat = "FullDPS", weightMult = 1 } } -+ -+ local baseOutput = { -+ CombinedDPS = 100, -+ TotalDPS = 100, -+ TotalDotDPS = 0, -+ } -+ local reducedOutput = mock_tradeQuery:ReduceOutput({ -+ CombinedDPS = 120, -+ TotalDPS = 120, -+ TotalDotDPS = 0, -+ }) -+ -+ local result = mock_queryGen.WeightedRatioOutputs(baseOutput, reducedOutput, mock_tradeQuery.statSortSelectionList) -+ -+ assert.are.equals(1.2, result) -+ end) - end) - end) -\ No newline at end of file diff --git a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua index 0fc8303b2f3..77964163c43 100644 --- a/src/Classes/ItemDBControl.lua +++ b/src/Classes/ItemDBControl.lua @@ -231,10 +231,7 @@ function ItemDBClass:ListBuilder() for slotName, slot in pairs(self.itemsTab.slots) do 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 local output = calcFunc(item.base.flask and { toggleFlask = item } or item.base.tincture and { toggleTincture = item } or { repSlotName = slotName, repItem = item }, useFullDPS) - local measuredPower = output.Minion and output.Minion[self.sortMode] or output[self.sortMode] or 0 - if self.sortDetail.transform then - measuredPower = self.sortDetail.transform(measuredPower) - end + local measuredPower = data.powerStatList.GetFromOutput(output, self.sortDetail) item.measuredPower = m_max(item.measuredPower, measuredPower) end end diff --git a/src/Classes/ItemDBControl.lua.rej b/src/Classes/ItemDBControl.lua.rej deleted file mode 100644 index bb65957caa8..00000000000 --- a/src/Classes/ItemDBControl.lua.rej +++ /dev/null @@ -1,11 +0,0 @@ -diff a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua (rejected hunks) -@@ -244,9 +244,6 @@ function ItemDBClass:ListBuilder() - 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 - local output = calcFunc(item.base.flask and { toggleFlask = item } or item.base.charm and { toggleCharm = item } or { repSlotName = slotName, repItem = item }, useFullDPS) - local measuredPower = data.powerStatList.GetFromOutput(output, self.sortDetail) -- if self.sortDetail.transform then -- measuredPower = self.sortDetail.transform(measuredPower) -- end - item.measuredPower = item.measuredPower and m_max(item.measuredPower, measuredPower) or measuredPower - end - end diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 697fa84f879..a022c8f2dd7 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -719,7 +719,12 @@ end function TradeQueryClass:ReduceOutput(output) local smallOutput = {} for _, statTable in ipairs(self.statSortSelectionList) do - smallOutput[statTable.stat] = output[statTable.stat] + smallOutput[statTable.stat] = data.powerStatList.GetFromOutput(output, statTable) + if statTable.stat == "FullDPS" and not output.FullDPS then + smallOutput.TotalDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDPS" }) + smallOutput.TotalDotDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDotDPS" }) + smallOutput.CombinedDPS = data.powerStatList.GetFromOutput(output, { stat = "CombinedDPS" }) + end end return smallOutput end diff --git a/src/Classes/TradeQuery.lua.rej b/src/Classes/TradeQuery.lua.rej deleted file mode 100644 index 20900eede40..00000000000 --- a/src/Classes/TradeQuery.lua.rej +++ /dev/null @@ -1,13 +0,0 @@ -diff a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua (rejected hunks) -@@ -780,6 +780,11 @@ function TradeQueryClass:ReduceOutput(output) - local smallOutput = {} - for _, statTable in ipairs(self.statSortSelectionList) do - smallOutput[statTable.stat] = data.powerStatList.GetFromOutput(output, statTable) -+ if statTable.stat == "FullDPS" and not output.FullDPS then -+ smallOutput.TotalDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDPS" }) -+ smallOutput.TotalDotDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDotDPS" }) -+ smallOutput.CombinedDPS = data.powerStatList.GetFromOutput(output, { stat = "CombinedDPS" }) -+ end - end - return smallOutput - end diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index eeb2fdeaab2..7a200b0833d 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -173,12 +173,13 @@ end function TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, newOutput, statWeights) local meanStatDiff = 0 + local function ratioModSums(...) local baseModSum = 0 local newModSum = 0 for _, mod in ipairs({ ... }) do - baseModSum = baseModSum + (baseOutput[mod] or 0) - newModSum = newModSum + (newOutput[mod] or 0) + baseModSum = baseModSum + data.powerStatList.GetFromOutput(baseOutput, mod, true) + newModSum = newModSum + data.powerStatList.GetFromOutput(newOutput, mod, true) end if baseModSum == math.huge then @@ -192,10 +193,17 @@ function TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, newOutput, st end end for _, statTable in ipairs(statWeights) do + local modSumRatio if statTable.stat == "FullDPS" and not (baseOutput["FullDPS"] and newOutput["FullDPS"]) then - meanStatDiff = meanStatDiff + ratioModSums("TotalDPS", "TotalDotDPS", "CombinedDPS") * statTable.weightMult + modSumRatio = ratioModSums({ stat = "TotalDPS" }, { stat = "TotalDotDPS" }, { stat = "CombinedDPS" }) + else + modSumRatio = ratioModSums(statTable) + end + -- some weights, such as damage taken from hit need to be negated as lower is better for them + if statTable.transform then + modSumRatio = statTable.transform(modSumRatio) end - meanStatDiff = meanStatDiff + ratioModSums(statTable.stat) * statTable.weightMult + meanStatDiff = meanStatDiff + modSumRatio * statTable.weightMult end return meanStatDiff end diff --git a/src/Classes/TradeQueryGenerator.lua.rej b/src/Classes/TradeQueryGenerator.lua.rej deleted file mode 100644 index 9255b3fe21c..00000000000 --- a/src/Classes/TradeQueryGenerator.lua.rej +++ /dev/null @@ -1,12 +0,0 @@ -diff a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua (rejected hunks) -@@ -162,8 +162,8 @@ function TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, newOutput, st - local baseModSum = 0 - local newModSum = 0 - for _, mod in ipairs({ ... }) do -- baseModSum = baseModSum + data.powerStatList.GetFromOutput(baseOutput, mod) -- newModSum = newModSum + data.powerStatList.GetFromOutput(newOutput, mod) -+ baseModSum = baseModSum + data.powerStatList.GetFromOutput(baseOutput, mod, true) -+ newModSum = newModSum + data.powerStatList.GetFromOutput(newOutput, mod, true) - end - - if baseModSum == math.huge then diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 77bfe3ffdbf..b84ce742e7b 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -1991,7 +1991,7 @@ function TreeTabClass:FindTimelessJewel() end local fallbackWeightsList = { } - for id, stat in pairs(data.powerStatList) do + for _, stat in ipairs(data.powerStatList) do if not stat.ignoreForItems and stat.label ~= "Name" then t_insert(fallbackWeightsList, { label = "Sort by " .. stat.label, diff --git a/src/Classes/TreeTab.lua.rej b/src/Classes/TreeTab.lua.rej deleted file mode 100644 index 48703412368..00000000000 --- a/src/Classes/TreeTab.lua.rej +++ /dev/null @@ -1,10 +0,0 @@ -diff a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua (rejected hunks) -@@ -1850,7 +1850,7 @@ function TreeTabClass:FindTimelessJewel() - - controls.fallbackWeightsLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 225, 0, 16}, "^7Fallback Weight Mode:") - local fallbackWeightsList = { } -- for id, stat in pairs(data.powerStatList) do -+ for _, stat in ipairs(data.powerStatList) do - if not stat.ignoreForItems and stat.label ~= "Name" then - t_insert(fallbackWeightsList, { - label = "Sort by " .. stat.label, diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index c581cb661ff..5b25c267216 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -108,10 +108,11 @@ data = { } -- Misc data tables LoadModule("Data/Misc", data) +---@alias TransformFunc fun(in: number|string): (number|string)? ---@class StatTable ---@field stat? string stat ID ---@field label string A short description of the stat ----@field transform fun(in: number|string): number|string A function to e.g. invert the value, if the stat represents something where lower is better +---@field transform TransformFunc?: number|string A function to e.g. invert the value, if the stat represents something where lower is better ---@field combinedOffDef? boolean ---@field ignoreForNodes? boolean ---@field ignoreForItems? boolean @@ -170,6 +171,59 @@ data.powerStatList = { { stat="SpellSuppressionChance", label="Spell Suppression Chance" }, } +---@param output any Calc output +---@param statTable StatTable Table with stats as in data.powerStatList +---@param skipTransform? boolean Whether the stat transform should be skipped. This is useful if you want to e.g. divide two less is better stats +---@return number +function data.powerStatList.GetFromOutput(output, statTable, skipTransform) + local function getEntry() + if statTable.stat == "FullDPS" then + if output[statTable.stat] ~= nil then + return output[statTable.stat] or 0 + end + -- if the user doesn't have full dps, we default to adding the player and minion dps together + return (output.CombinedDPS or 0) + (output.Minion and output.Minion.CombinedDPS or 0) + end + -- minion-only stats + local minionStat = statTable.stat:match("^Minion(.+)") + if minionStat then + return output.Minion and output.Minion[minionStat] or 0 + end + -- damage stats use a combination of player and minion dps + local isDamageStat = statTable.stat == "AverageDamage" or statTable.stat == "TotalDot" or + statTable.stat:match("DPS") + if isDamageStat then + return (output[statTable.stat] or 0) + (output.Minion and output.Minion[statTable.stat] or 0) + end + return output[statTable.stat] or 0 + end + if statTable.transform and not skipTransform then + return statTable.transform(getEntry()) + end + return getEntry() +end + +-- these stats don't exist on minions or generally don't exist on both player and minion +local minionNonApplicableStats = { + AverageDamage = true, + TotalDot = true, + Str = true, + Dex = true, + Int = true, + Spirit = true, + EffectiveLootRarityMod = true, +} +for i = 1, #data.powerStatList do + local statEntry = data.powerStatList[i] + if (not statEntry.stat) or statEntry.stat:match("DPS") or minionNonApplicableStats[statEntry.stat] then + goto statContinue + end + local minionStat = copyTable(statEntry) + minionStat.stat = "Minion" .. minionStat.stat + minionStat.label = "Minion " .. minionStat.label + t_insert(data.powerStatList, minionStat) + ::statContinue:: +end data.misc = { -- magic numbers ServerTickTime = 0.033, ServerTickRate = 1 / 0.033, diff --git a/src/Modules/Data.lua.rej b/src/Modules/Data.lua.rej deleted file mode 100644 index 0b84cbfa092..00000000000 --- a/src/Modules/Data.lua.rej +++ /dev/null @@ -1,10 +0,0 @@ -diff a/src/Modules/Data.lua b/src/Modules/Data.lua (rejected hunks) -@@ -189,7 +189,7 @@ function data.powerStatList.GetFromOutput(output, statTable, skipTransform) - return output[statTable.stat] or 0 - end - -- if the user doesn't have full dps, we default to adding the player and minion dps together -- return (output.CombinedDPS or 0) + (output.Minion and output.Minion.CombinedDPS) -+ return (output.CombinedDPS or 0) + (output.Minion and output.Minion.CombinedDPS or 0) - end - -- minion-only stats - local minionStat = statTable.stat:match("^Minion(.+)") From c00641f4398298e35adf727074ced860cb84734d Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 5 Jul 2026 13:30:29 +0300 Subject: [PATCH 3/5] Rework timeless fallback weights --- src/Classes/TreeTab.lua | 61 ++++++++++++----------------------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index b84ce742e7b..c551b3f06c3 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -1825,54 +1825,29 @@ function TreeTabClass:FindTimelessJewel() end end - local function generateFallbackWeights(nodes, selection) + local function generateFallbackWeights(nodes, powerStat) local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator(self.build) local newList = { } - local baseOutput = calcFunc() - if baseOutput.Minion then - baseOutput = baseOutput.Minion - end - local baseValue = baseOutput[selection.stat] or 1 - if selection.transform then - baseValue = selection.transform(baseValue) - end + local basePower = data.powerStatList.GetFromOutput(calcBase, powerStat) for _, newNode in ipairs(nodes) do - local output = nil - if newNode.calcMultiple then - output = calcFunc({ addNodes = { [newNode.node[1]] = true } }) - else - output = calcFunc({ addNodes = { [newNode] = true } }) - end - if output.Minion then - output = output.Minion - end - local outputValue = output[selection.stat] or 0 - if selection.transform then - outputValue = selection.transform(outputValue) - end - outputValue = outputValue / baseValue - if outputValue ~= outputValue then - outputValue = 1 - end - t_insert(newList, { - id = newNode.id, - weight1 = (outputValue - 1) / (newNode.divisor or 1) - }) - if newNode.calcMultiple then - output = calcFunc({ addNodes = { [newNode.node[2]] = true } }) - if output.Minion then - output = output.Minion - end - outputValue = output[selection.stat] or 0 - if selection.transform then - outputValue = selection.transform(outputValue) - end - outputValue = outputValue / baseValue - if outputValue ~= outputValue then - outputValue = 1 + local powerEntry = { id = newNode.id } + -- nodes that have multiple lines are represented as a list in newNode.node + local nodeLines = newNode.node or { newNode } + for i = 1, #nodeLines do + local node = nodeLines[i] + local nodeOutput = calcFunc({ addNodes = { [node] = true } }) + local nodePower = data.powerStatList.GetFromOutput(nodeOutput, powerStat) + -- avoid infinity + if basePower == 0 then + powerEntry["weight" .. i] = 0 + else + local powerGain = (nodePower - basePower) / + -- normalize with absolute base power so that the result isn't negative + math.abs(basePower) + powerEntry["weight" .. i] = powerGain / (newNode.divisor or 1) end - newList[#newList].weight2 = (outputValue - 1) / (newNode.divisor or 1) end + t_insert(newList, powerEntry) end return newList end From 3a8f43ccdaec6f063463d6cac78bf80b4df7f7d0 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 5 Jul 2026 13:44:12 +0300 Subject: [PATCH 4/5] Removed dead variable --- src/Classes/TradeQuery.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index a022c8f2dd7..a0afe80006b 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -555,7 +555,6 @@ function TradeQueryClass:SetStatWeights(previousSelectionList) local sliderController = { index = 1 } local popupHeight = 500 - local listYOffset = 45 -- account for top gap, bottom button size and gap, and a gap before buttons local listHeight = popupHeight - 45 - 30 - 10 From a09a9b004f356ef2f258c378fcfa86f2cdeb39b2 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Mon, 20 Jul 2026 23:47:39 +1000 Subject: [PATCH 5/5] 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 --- spec/System/TestItemDBControl_spec.lua | 54 ++++++++++++++++++++ spec/System/TestTradeQueryGenerator_spec.lua | 24 ++++----- spec/System/TestTradeQuery_spec.lua | 16 ++++++ src/Classes/ItemDBControl.lua | 4 +- src/Classes/ItemsTab.lua | 52 ++++--------------- src/Classes/TradeQuery.lua | 2 +- 6 files changed, 95 insertions(+), 57 deletions(-) create mode 100644 spec/System/TestItemDBControl_spec.lua diff --git a/spec/System/TestItemDBControl_spec.lua b/spec/System/TestItemDBControl_spec.lua new file mode 100644 index 00000000000..b6e4e702187 --- /dev/null +++ b/spec/System/TestItemDBControl_spec.lua @@ -0,0 +1,54 @@ +describe("ItemDBControl", function() + it("sorts lower-is-better stats below zero", function() + local function makeItem(name) + return { + name = name, + base = {}, + enchantModLines = {}, + implicitModLines = {}, + explicitModLines = {}, + baseModList = {}, + } + end + local betterItem = makeItem("Better Item") + local worseItem = makeItem("Worse Item") + local invalidItem = makeItem("Invalid Item") + local takenDamage = { + [betterItem] = 80, + [worseItem] = 120, + } + local itemsTab = { + activeItemSet = { useSecondWeaponSet = false }, + slots = { ["Body Armour"] = {} }, + build = { + calcsTab = { + GetMiscCalculator = function() + return function(args) + return { PhysicalTakenHit = takenDamage[args.repItem] } + end + end, + }, + }, + IsItemValidForSlot = function(_, item) + return item ~= invalidItem + end, + } + local control = new("ItemDBControl", nil, { 0, 0, 100, 100 }, itemsTab, { + list = { invalidItem, betterItem, worseItem }, + }, "RARE") + control.sortDetail = { + stat = "PhysicalTakenHit", + transform = function(value) return -value end, + } + control.sortOrder = { control.sortControl.STAT, control.sortControl.NAME } + + control:ListBuilder() + + assert.are.equal(betterItem, control.list[1]) + assert.are.equal(worseItem, control.list[2]) + assert.are.equal(invalidItem, control.list[3]) + assert.are.equal(-80, betterItem.measuredPower) + assert.are.equal(-120, worseItem.measuredPower) + assert.are.equal(-math.huge, invalidItem.measuredPower) + end) +end) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index a7e937b4d78..e4d93d9aafe 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -15,6 +15,17 @@ describe("TradeQueryGenerator", function() end) describe("WeightedRatioOutputs", function() + local maxStatIncrease + + before_each(function() + maxStatIncrease = data.misc.maxStatIncrease + data.misc.maxStatIncrease = 1000 + end) + + after_each(function() + data.misc.maxStatIncrease = maxStatIncrease + end) + -- Pass: Returns 0, avoiding math errors -- Fail: Returns NaN/inf or crashes, indicating unhandled infinite values, causing evaluation failures in infinite-scaling builds it("handles infinite base", function() @@ -31,7 +42,6 @@ describe("TradeQueryGenerator", function() local baseOutput = { TotalDPS = 0 } local newOutput = { TotalDPS = 100 } local statWeights = { { stat = "TotalDPS", weightMult = 1 } } - data.misc.maxStatIncrease = 1000 local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) assert.are.equal(result, 100) end) @@ -39,8 +49,6 @@ describe("TradeQueryGenerator", function() local baseOutput = { Life = 10, Minion = { Life = 100 } } local newOutput = { Life = 10, Minion = { Life = 250 } } local statWeights = { { stat = "MinionLife", weightMult = 1 } } - data.misc.maxStatIncrease = 1000 - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) assert.are.equal(result, 2.5) @@ -50,8 +58,6 @@ describe("TradeQueryGenerator", function() local baseOutput = { MaxHit = 100 } local newOutput = { MaxHit = 10 } local statWeights = { { stat = "MaxHit", weightMult = 1, transform = function(number) return -number end } } - data.misc.maxStatIncrease = 1000 - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) local close_enough = math.abs(result - -0.1) < 0.0001 @@ -63,8 +69,6 @@ describe("TradeQueryGenerator", function() local baseOutput = { FullDPS = 100, Minion = { FullDPS = 100 } } local newOutput = { FullDPS = 250, Minion = { FullDPS = 1000 } } local statWeights = { { stat = "FullDPS", weightMult = 1 } } - data.misc.maxStatIncrease = 1000 - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) assert.are.equal(result, 2.5) @@ -74,8 +78,6 @@ describe("TradeQueryGenerator", function() local baseOutput = { Life = 100, Minion = { Life = 100 } } local newOutput = { Life = 250, Minion = { Life = 1000 } } local statWeights = { { stat = "Life", weightMult = 1 } } - data.misc.maxStatIncrease = 1000 - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) assert.are.equal(result, 2.5) end) @@ -84,8 +86,6 @@ describe("TradeQueryGenerator", function() local baseOutput = { Minion = { TotalDPS = 10, TotalDotDPS = 0, CombinedDPS = 10 } } local newOutput = { Minion = { TotalDPS = 25, TotalDotDPS = 0, CombinedDPS = 25 } } local statWeights = { { stat = "FullDPS", weightMult = 1 } } - data.misc.maxStatIncrease = 1000 - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) assert.are.equal(result, 2.5) @@ -95,8 +95,6 @@ describe("TradeQueryGenerator", function() local baseOutput = { Spirit = 100, Minion = { AverageDamage = 100 } } local newOutput = { Spirit = 120, Minion = { AverageDamage = 100 } } local statWeights = { { stat = "Spirit", weightMult = 1 } } - data.misc.maxStatIncrease = 1000 - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) assert.are.equal(result, 1.2) diff --git a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua index 31a62e94ded..68eaf5d32c7 100644 --- a/spec/System/TestTradeQuery_spec.lua +++ b/spec/System/TestTradeQuery_spec.lua @@ -62,6 +62,22 @@ describe("TradeQuery", function() end) end) describe("ReduceOutput", function() + it("preserves lower-is-better values for weighted result comparison", function() + local weights = { + { stat = "PhysicalTakenHit", weightMult = 1, transform = function(value) return -value end }, + } + mock_tradeQuery.statSortSelectionList = weights + + local baseOutput = { PhysicalTakenHit = 100 } + local betterOutput = mock_tradeQuery:ReduceOutput({ PhysicalTakenHit = 80 }) + local worseOutput = mock_tradeQuery:ReduceOutput({ PhysicalTakenHit = 120 }) + local betterWeight = mock_queryGen.WeightedRatioOutputs(baseOutput, betterOutput, weights) + local worseWeight = mock_queryGen.WeightedRatioOutputs(baseOutput, worseOutput, weights) + + assert.are.equal(80, betterOutput.PhysicalTakenHit) + assert.is_true(betterWeight > worseWeight) + end) + it("uses selected minion stats for weighted result comparison", function() mock_tradeQuery.statSortSelectionList = { { stat = "AverageDamage" } } diff --git a/src/Classes/ItemDBControl.lua b/src/Classes/ItemDBControl.lua index 77964163c43..8cd0eab1c26 100644 --- a/src/Classes/ItemDBControl.lua +++ b/src/Classes/ItemDBControl.lua @@ -227,7 +227,7 @@ function ItemDBClass:ListBuilder() local start = GetTime() local calcFunc, calcBase = self.itemsTab.build.calcsTab:GetMiscCalculator(self.build) for itemIndex, item in ipairs(list) do - item.measuredPower = 0 + item.measuredPower = -math.huge for slotName, slot in pairs(self.itemsTab.slots) do 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 local output = calcFunc(item.base.flask and { toggleFlask = item } or item.base.tincture and { toggleTincture = item } or { repSlotName = slotName, repItem = item }, useFullDPS) @@ -360,4 +360,4 @@ function ItemDBClass:OnHoverKeyUp(key) itemLib.wiki.openItem(item) end end -end \ No newline at end of file +end diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 6320011f410..b122e921d38 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -64,32 +64,14 @@ end local function buildModSortList() local sortList = { { label = "Default", stat = nil } } - local sortTransforms = { } + local sortStats = { } for _, entry in ipairs(data.powerStatList) do if entry.stat and not entry.ignoreForNodes then t_insert(sortList, { label = entry.label, stat = entry.stat }) - sortTransforms[entry.stat] = entry.transform + sortStats[entry.stat] = entry end end - return sortList, sortTransforms -end - -local function getOutputStatValue(output, stat) - if stat == "FullDPS" then - if output[stat] ~= nil then - return output[stat] - end - if output.Minion and output.Minion.CombinedDPS ~= nil then - return output.Minion.CombinedDPS - end - end - if output.Minion and output.Minion[stat] ~= nil then - return output.Minion[stat] - end - if output[stat] ~= nil then - return output[stat] - end - return 0 + return sortList, sortStats end local ItemsTabClass = newClass("ItemsTab", "UndoHandler", "ControlHost", "Control", function(self, build) @@ -2317,7 +2299,7 @@ function ItemsTabClass:EnchantDisplayItem(enchantSlot) end end local enchantmentList = { } - local sortList, sortTransforms = buildModSortList() + local sortList, sortStats = buildModSortList() local function setDefaultSortOrder() for index, entry in ipairs(enchantmentList) do entry.defaultSortOrder = index @@ -2385,10 +2367,7 @@ function ItemsTabClass:EnchantDisplayItem(enchantSlot) end item:BuildAndParseRaw() local output = calcFunc({ repSlotName = slotName, repItem = item }, useFullDPS) - local value = getOutputStatValue(output, stat) - if sortTransforms[stat] then - value = sortTransforms[stat](value) - end + local value = data.powerStatList.GetFromOutput(output, sortStats[stat]) entry.sortValues[stat] = value return value end @@ -2635,7 +2614,7 @@ function ItemsTabClass:CorruptDisplayItem(modType) local controls = { } local implicitList = { } local sourceList = { "Corrupted", "Scourge" } - local sortList, sortTransforms = buildModSortList() + local sortList, sortStats = buildModSortList() local function buildImplicitList(modType) if implicitList[modType] then return @@ -2694,10 +2673,7 @@ function ItemsTabClass:CorruptDisplayItem(modType) end item:BuildAndParseRaw() local output = calcFunc({ repSlotName = slotName, repItem = item }, useFullDPS) - local value = getOutputStatValue(output, stat) - if sortTransforms[stat] then - value = sortTransforms[stat](value) - end + local value = data.powerStatList.GetFromOutput(output, sortStats[stat]) entry.sortValues[stat] = value return value end @@ -2916,7 +2892,7 @@ function ItemsTabClass:AddCustomModifierToDisplayItem() local controls = { } local sourceList = { } local modList = { } - local sortList, sortTransforms = buildModSortList() + local sortList, sortStats = buildModSortList() local function setDefaultSortOrder() for index, listMod in ipairs(modList) do listMod.defaultSortOrder = index @@ -2936,10 +2912,7 @@ function ItemsTabClass:AddCustomModifierToDisplayItem() end item:BuildAndParseRaw() local output = calcFunc({ repSlotName = slotName, repItem = item }, useFullDPS) - local value = getOutputStatValue(output, stat) - if sortTransforms[stat] then - value = sortTransforms[stat](value) - end + local value = data.powerStatList.GetFromOutput(output, sortStats[stat]) listMod.sortValues[stat] = value return value end @@ -3336,7 +3309,7 @@ function ItemsTabClass:AddImplicitToDisplayItem() local sourceList = { } local modList = { } local modGroups = {} - local sortList, sortTransforms = buildModSortList() + local sortList, sortStats = buildModSortList() local function setDefaultSortOrder() for groupIndex, group in ipairs(modGroups) do group.defaultSortOrder = groupIndex @@ -3524,10 +3497,7 @@ function ItemsTabClass:AddImplicitToDisplayItem() applyCandidateMod(item, listMod) item:BuildAndParseRaw() local output = calcFunc({ repSlotName = slotName, repItem = item }, useFullDPS) - local value = getOutputStatValue(output, stat) - if sortTransforms[stat] then - value = sortTransforms[stat](value) - end + local value = data.powerStatList.GetFromOutput(output, sortStats[stat]) listMod.sortValues[stat] = value return value end diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index a0afe80006b..8a024580dc8 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -718,7 +718,7 @@ end function TradeQueryClass:ReduceOutput(output) local smallOutput = {} for _, statTable in ipairs(self.statSortSelectionList) do - smallOutput[statTable.stat] = data.powerStatList.GetFromOutput(output, statTable) + smallOutput[statTable.stat] = data.powerStatList.GetFromOutput(output, statTable, true) if statTable.stat == "FullDPS" and not output.FullDPS then smallOutput.TotalDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDPS" }) smallOutput.TotalDotDPS = data.powerStatList.GetFromOutput(output, { stat = "TotalDotDPS" })