Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions spec/System/TestTradeQueryCurrency_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions spec/System/TestTradeQueryGenerator_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,31 @@ describe("TradeQueryGenerator", function()
assert.are.equal(result, 100)
end)

it("uses minion output for non-FullDPS stats when minion output is available", function()
local baseOutput = { AverageDamage = 10, Minion = { AverageDamage = 100 } }
local newOutput = { AverageDamage = 10, Minion = { AverageDamage = 250 } }
local statWeights = { { stat = "AverageDamage", weightMult = 1 } }
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 player output for FullDPS even when minion output is available", function()
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 = (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 } }
Expand All @@ -57,6 +70,16 @@ describe("TradeQueryGenerator", function()
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 } }
Expand Down
25 changes: 25 additions & 0 deletions spec/System/TestTradeQuery_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
describe("TradeQuery", function ()
local mock_tradeQuery

before_each(function()
mock_tradeQuery = new("TradeQuery", { itemsTab = {} })
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)
end)
end)
19 changes: 5 additions & 14 deletions src/Classes/CalcsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -672,16 +672,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

Expand All @@ -692,10 +684,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.CombinedDPS or 0)
local dpsIncr = original.CombinedDPS + (original.Minion.CombinedDPS or 0) - modifiedDps
return (original.CombinedDPS - modified.CombinedDPS) / modifiedDps, defence
end

function CalcsTabClass:GetNodeCalculator()
Expand Down
5 changes: 1 addition & 4 deletions src/Classes/CompareTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2652,10 +2652,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)
Expand Down
7 changes: 2 additions & 5 deletions src/Classes/ItemDBControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,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,
Expand Down Expand Up @@ -243,10 +243,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.charm and { toggleCharm = 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 = item.measuredPower and m_max(item.measuredPower, measuredPower) or measuredPower
end
end
Expand Down
10 changes: 9 additions & 1 deletion src/Classes/ItemsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,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
Expand Down
14 changes: 3 additions & 11 deletions src/Classes/NotableDBControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,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,
Expand All @@ -159,16 +159,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

Expand Down
13 changes: 9 additions & 4 deletions src/Classes/TradeQuery.lua
Original file line number Diff line number Diff line change
Expand Up @@ -623,11 +623,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,
Expand Down Expand Up @@ -774,7 +779,7 @@ end
function TradeQueryClass:ReduceOutput(output)
local smallOutput = {}
for _, statTable in ipairs(self.statSortSelectionList) do
smallOutput[statTable.stat] = output.Minion and output.Minion[statTable.stat] or output[statTable.stat]
smallOutput[statTable.stat] = data.powerStatList.GetFromOutput(output, statTable)
end
return smallOutput
end
Expand Down
23 changes: 5 additions & 18 deletions src/Classes/TradeQueryGenerator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -157,26 +157,13 @@ end

function TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, newOutput, statWeights)
local meanStatDiff = 0
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
return output[stat] or 0
end

local function ratioModSums(...)
local baseModSum = 0
local newModSum = 0
for _, mod in ipairs({ ... }) do
baseModSum = baseModSum + getOutputStatValue(baseOutput, mod)
newModSum = newModSum + getOutputStatValue(newOutput, mod)
baseModSum = baseModSum + data.powerStatList.GetFromOutput(baseOutput, mod)
newModSum = newModSum + data.powerStatList.GetFromOutput(newOutput, mod)
end

if baseModSum == math.huge then
Expand All @@ -192,9 +179,9 @@ function TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, newOutput, st
for _, statTable in ipairs(statWeights) do
local modSumRatio
if statTable.stat == "FullDPS" and not (baseOutput["FullDPS"] and newOutput["FullDPS"]) then
modSumRatio = ratioModSums("TotalDPS", "TotalDotDPS", "CombinedDPS")
modSumRatio = ratioModSums({ stat = "TotalDPS" }, { stat = "TotalDotDPS" }, { stat = "CombinedDPS" })
else
modSumRatio = ratioModSums(statTable.stat)
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
Expand Down
2 changes: 1 addition & 1 deletion src/Classes/TradeStatWeightMultiplierListControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/Classes/TreeTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
62 changes: 62 additions & 0 deletions src/Modules/Data.lua
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,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},
Expand Down Expand Up @@ -168,6 +178,58 @@ data.powerStatList = {
{ stat="EffectiveLootRarityMod", label="Rarity of Items found" },
}

---@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)
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,
Expand Down
Loading