Skip to content
Merged
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
54 changes: 54 additions & 0 deletions spec/System/TestItemDBControl_spec.lua
Original file line number Diff line number Diff line change
@@ -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)
66 changes: 65 additions & 1 deletion spec/System/TestTradeQueryGenerator_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -31,10 +42,63 @@ 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)
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 } }
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 } }
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 } }
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 } }
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 } }
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 } }
local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights)

assert.are.equal(result, 1.2)
end)
end)

describe("Filter prioritization", function()
Expand Down
60 changes: 60 additions & 0 deletions spec/System/TestTradeQuery_spec.lua
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -54,4 +61,57 @@ describe("TradeQuery", function()
assert.are.equal(0, #tooltip.lines)
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" } }

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)
19 changes: 5 additions & 14 deletions src/Classes/CalcsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down
5 changes: 1 addition & 4 deletions src/Classes/CompareTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 4 additions & 7 deletions src/Classes/ItemDBControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -227,14 +227,11 @@ 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)
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
Expand Down Expand Up @@ -363,4 +360,4 @@ function ItemDBClass:OnHoverKeyUp(key)
itemLib.wiki.openItem(item)
end
end
end
end
Loading
Loading