Skip to content

Commit 8312203

Browse files
committed
Add scoring breakdown to Trader result dropdown
1 parent ce8bffa commit 8312203

3 files changed

Lines changed: 198 additions & 14 deletions

File tree

spec/System/TestTradeQuery_spec.lua

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,76 @@ describe("TradeQuery", function ()
4343
assert.are.equals(1.2, result)
4444
end)
4545
end)
46-
end)
46+
47+
describe("ComputeStatDetails", function()
48+
it("uses Trader's FullDPS fallback inputs", function()
49+
mock_tradeQuery.statSortSelectionList = { { label = "Full DPS", stat = "FullDPS", weightMult = 1 } }
50+
51+
local result = mock_tradeQuery:ComputeStatDetails({
52+
CombinedDPS = 100,
53+
TotalDPS = 100,
54+
TotalDotDPS = 0,
55+
}, {
56+
CombinedDPS = 100,
57+
TotalDPS = 200,
58+
TotalDotDPS = 0,
59+
})
60+
61+
assert.are.equals(50, result[1].percentChange)
62+
end)
63+
64+
it("reports lower-is-better stat improvements as positive", function()
65+
mock_tradeQuery.statSortSelectionList = {
66+
{
67+
label = "Taken Phys dmg",
68+
stat = "PhysicalTakenHit",
69+
weightMult = 1,
70+
transform = function(value) return -value end,
71+
},
72+
}
73+
74+
local result = mock_tradeQuery:ComputeStatDetails({ PhysicalTakenHit = 100 }, { PhysicalTakenHit = 80 })
75+
76+
assert.is_true(math.abs(result[1].percentChange - 20) < 0.0001)
77+
end)
78+
79+
it("caps displayed increases to Trader's scoring maximum", function()
80+
mock_tradeQuery.statSortSelectionList = { { label = "Life", stat = "Life", weightMult = 1 } }
81+
local maxStatIncrease = data.misc.maxStatIncrease
82+
83+
local result = mock_tradeQuery:ComputeStatDetails({ Life = 1 }, { Life = maxStatIncrease + 1 })
84+
85+
assert.are.equals((maxStatIncrease - 1) * 100, result[1].percentChange)
86+
end)
87+
88+
it("reports unchanged zero-value stats as unchanged", function()
89+
mock_tradeQuery.statSortSelectionList = { { label = "Block Chance", stat = "BlockChance", weightMult = 1 } }
90+
91+
local result = mock_tradeQuery:ComputeStatDetails({ BlockChance = 0 }, { BlockChance = 0 })
92+
93+
assert.are.equals(0, result[1].percentChange)
94+
end)
95+
96+
it("reports improvements from zero as positive", function()
97+
mock_tradeQuery.statSortSelectionList = { { label = "Block Chance", stat = "BlockChance", weightMult = 1 } }
98+
local maxStatIncrease = data.misc.maxStatIncrease
99+
100+
local result = mock_tradeQuery:ComputeStatDetails({ BlockChance = 0 }, { BlockChance = 0.5 })
101+
102+
assert.are.equals((maxStatIncrease - 1) * 100, result[1].percentChange)
103+
end)
104+
end)
105+
106+
describe("GetResultScorePercent", function()
107+
it("returns the weighted average stat delta", function()
108+
local result = mock_tradeQuery:GetResultScorePercent({
109+
statDetails = {
110+
{ percentChange = 10, weightMult = 1 },
111+
{ percentChange = -10, weightMult = 0.5 },
112+
},
113+
})
114+
115+
assert.is_true(math.abs(result - (10 / 3)) < 0.0001)
116+
end)
117+
end)
118+
end)

src/Classes/DropDownControl.lua

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ local m_min = math.min
88
local m_max = math.max
99
local m_floor = math.floor
1010

11+
local function drawStrikethrough(label, y, lineHeight)
12+
local strWidth = DrawStringWidth(lineHeight, "VAR", label or "")
13+
SetDrawColor(0.6, 0.6, 0.6)
14+
DrawImage(nil, 0, y + lineHeight / 2, strWidth, 1)
15+
end
16+
1117
local DropDownClass = newClass("DropDownControl", "Control", "ControlHost", "TooltipHost", "SearchHost", function(self, anchor, rect, list, selFunc, tooltipText)
1218
self.Control(anchor, rect)
1319
self.ControlHost()
@@ -304,19 +310,26 @@ function DropDownClass:Draw(viewPort, noTooltip)
304310
-- draw selected label or search term
305311
local selLabel = nil
306312
local selDetail = nil
313+
local selStrikethrough = false
307314
if self:IsSearchActive() then
308315
selLabel = "Search: " .. self:GetSearchTermPretty()
309316
else
310317
local selItem = self.list[self.selIndex]
311318
if type(selItem) == "table" then
312319
selLabel = selItem.label
313320
selDetail = selItem.detail
321+
selStrikethrough = selItem.strikethrough
314322
else
315323
selLabel = selItem
316324
end
317325
end
318326
SetViewport(x + 2, y + 2, width - height, lineHeight)
319327
DrawString(0, 0, "LEFT", lineHeight, "VAR", selLabel or "")
328+
if selStrikethrough then
329+
drawStrikethrough(selLabel, 0, lineHeight)
330+
local textColor = enabled and 1 or 0.66
331+
SetDrawColor(textColor, textColor, textColor)
332+
end
320333
if selDetail ~= nil then
321334
local dx = DrawStringWidth(lineHeight, "VAR", selDetail)
322335
DrawString(width - dx - 22, 0, "LEFT", lineHeight, "VAR", selDetail)
@@ -376,6 +389,14 @@ function DropDownClass:Draw(viewPort, noTooltip)
376389
label = listVal
377390
end
378391
DrawString(0, y, "LEFT", lineHeight, "VAR", label)
392+
if type(listVal) == "table" and listVal.strikethrough then
393+
drawStrikethrough(label, y, lineHeight)
394+
if index == self.hoverSel or index == self.selIndex then
395+
SetDrawColor(1, 1, 1)
396+
else
397+
SetDrawColor(0.66, 0.66, 0.66)
398+
end
399+
end
379400
if detail ~= nil then
380401
local detail = listVal.detail
381402
dx = DrawStringWidth(lineHeight, "VAR", detail)
@@ -515,11 +536,15 @@ function DropDownClass:CheckDroppedWidth(enable)
515536
-- do not be smaller than the created width
516537
local dWidth = self.width
517538
for _, line in ipairs(self.list) do
539+
local detailWidth = 0
518540
if type(line) == "table" then
541+
if line.detail then
542+
detailWidth = DrawStringWidth(lineHeight, "VAR", line.detail) + 10
543+
end
519544
line = line.label or ""
520545
end
521546
-- +10 to stop clipping
522-
dWidth = m_max(dWidth, DrawStringWidth(lineHeight, "VAR", line or "") + 10)
547+
dWidth = m_max(dWidth, DrawStringWidth(lineHeight, "VAR", line or "") + detailWidth + 10)
523548
end
524549
-- no greater than self.maxDroppedWidth
525550
self.droppedWidth = m_min(dWidth + scrollWidth, self.maxDroppedWidth)

src/Classes/TradeQuery.lua

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,64 @@ function TradeQueryClass:ReduceOutput(output)
790790
return smallOutput
791791
end
792792

793+
local function getTradeStatValue(output, statTable, useFullDpsFallback)
794+
if useFullDpsFallback then
795+
return data.powerStatList.GetFromOutput(output, { stat = "TotalDPS" }, true) +
796+
data.powerStatList.GetFromOutput(output, { stat = "TotalDotDPS" }, true) +
797+
data.powerStatList.GetFromOutput(output, { stat = "CombinedDPS" }, true)
798+
end
799+
return data.powerStatList.GetFromOutput(output, statTable, true)
800+
end
801+
802+
local function getTradeStatRatio(baseOutput, newOutput, statTable)
803+
local useFullDpsFallback = statTable.stat == "FullDPS" and not (baseOutput.FullDPS and newOutput.FullDPS)
804+
local baseStat = getTradeStatValue(baseOutput, statTable, useFullDpsFallback)
805+
local newStat = getTradeStatValue(newOutput, statTable, useFullDpsFallback)
806+
if baseStat == math.huge then
807+
return newStat == math.huge and 1 or 0
808+
elseif newStat == math.huge then
809+
return data.misc.maxStatIncrease
810+
elseif baseStat == 0 then
811+
if newStat == 0 then
812+
return 1
813+
end
814+
return newStat > 0 and data.misc.maxStatIncrease or 0
815+
end
816+
return m_min(newStat / ((baseStat ~= 0) and baseStat or 1), data.misc.maxStatIncrease)
817+
end
818+
819+
function TradeQueryClass:ComputeStatDetails(baseOutput, newOutput)
820+
local details = {}
821+
for _, statTable in ipairs(self.statSortSelectionList) do
822+
local statRatio = getTradeStatRatio(baseOutput, newOutput, statTable)
823+
local percentChange = (statRatio - 1) * 100
824+
if statTable.transform then
825+
percentChange = (statTable.transform(statRatio) - statTable.transform(1)) * 100
826+
end
827+
t_insert(details, {
828+
label = statTable.label,
829+
stat = statTable.stat,
830+
percentChange = percentChange,
831+
weightMult = statTable.weightMult or 0,
832+
})
833+
end
834+
return details
835+
end
836+
837+
function TradeQueryClass:GetResultScorePercent(evaluation)
838+
if not evaluation or not evaluation.statDetails then
839+
return nil
840+
end
841+
local totalWeight = 0
842+
local scorePercent = 0
843+
for _, detail in ipairs(evaluation.statDetails) do
844+
local weightMult = detail.weightMult or 0
845+
totalWeight = totalWeight + weightMult
846+
scorePercent = scorePercent + (detail.percentChange or 0) * weightMult
847+
end
848+
return totalWeight > 0 and scorePercent / totalWeight or nil
849+
end
850+
793851
-- Method to evaluate a result by getting it's output and weight
794852
function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, baseOutput)
795853
local result = self.resultTbl[row_idx][result_index]
@@ -822,35 +880,48 @@ function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, ba
822880
end
823881
end
824882

825-
local output = self:ReduceOutput(calcFunc({ addNodes = addedNodes }))
883+
local fullNewOutput = calcFunc({ addNodes = addedNodes })
884+
local output = self:ReduceOutput(fullNewOutput)
826885
local weight = self.tradeQueryGenerator.WeightedRatioOutputs(baseOutput, output, self.statSortSelectionList)
827-
result.evaluation = {{ output = output, weight = weight }}
886+
local statDetails = self:ComputeStatDetails(baseOutput, fullNewOutput)
887+
result.evaluation = {{ output = output, weight = weight, statDetails = statDetails }}
828888
else
829889
local item = new("Item", result.item_string)
830890

831-
local output = self:ReduceOutput(calcFunc({ repSlotName = slotName, repItem = item }))
891+
local fullNewOutput = calcFunc({ repSlotName = slotName, repItem = item })
892+
local output = self:ReduceOutput(fullNewOutput)
832893
local weight = self.tradeQueryGenerator.WeightedRatioOutputs(baseOutput, output, self.statSortSelectionList)
833-
result.evaluation = {{ output = output, weight = weight }}
894+
local statDetails = self:ComputeStatDetails(baseOutput, fullNewOutput)
895+
result.evaluation = {{ output = output, weight = weight, statDetails = statDetails }}
834896
end
835897
return result.evaluation
836898
end
837899

838900
-- Method to update controls after a search is completed
839901
function TradeQueryClass:UpdateDropdownList(row_idx)
840902
local dropdownLabels = {}
903+
local dropdown = self.controls["resultDropdown".. row_idx]
841904

842-
if not self.resultTbl[row_idx] then return end
843-
844-
for result_index = 1, #self.resultTbl[row_idx] do
905+
if not dropdown or not self.resultTbl[row_idx] or not self.sortedResultTbl[row_idx] then return end
845906

907+
for result_index = 1, #self.sortedResultTbl[row_idx] do
846908
local pb_index = self.sortedResultTbl[row_idx][result_index].index
847909
local result = self.resultTbl[row_idx][pb_index]
848-
local price = string.format(" %s(%d %s)", colorCodes["CURRENCY"], result.amount, result.currency)
849-
local item = new("Item", result.item_string)
850-
table.insert(dropdownLabels, colorCodes[item.rarity] .. item.name .. price)
910+
if result then
911+
local price = s_format(" %s(%s %s)", colorCodes["CURRENCY"], tostring(result.amount), result.currency)
912+
local item = new("Item", result.item_string)
913+
local eval = self:GetResultEvaluation(row_idx, pb_index)
914+
local scorePercent = eval and self:GetResultScorePercent(eval[1])
915+
local scoreDetail = scorePercent and s_format("%s%+.1f%%", scorePercent >= 0 and colorCodes.POSITIVE or colorCodes.NEGATIVE, scorePercent)
916+
t_insert(dropdownLabels, {
917+
label = colorCodes[item.rarity] .. item.name .. price,
918+
detail = scoreDetail,
919+
strikethrough = scorePercent and scorePercent < 0,
920+
})
921+
end
851922
end
852-
self.controls["resultDropdown".. row_idx].selIndex = 1
853-
self.controls["resultDropdown".. row_idx]:SetList(dropdownLabels)
923+
dropdown.selIndex = 1
924+
dropdown:SetList(dropdownLabels)
854925
end
855926
function TradeQueryClass:ResetResultRow(rowIdx)
856927
self.itemIndexTbl[rowIdx] = nil
@@ -1176,6 +1247,8 @@ you can add them, copy the link here, and press "Price Item" to evaluate the ite
11761247
self.itemIndexTbl[row_idx] = self.sortedResultTbl[row_idx][index].index
11771248
self:SetFetchResultReturn(row_idx, self.itemIndexTbl[row_idx])
11781249
end)
1250+
controls["resultDropdown"..row_idx].enableDroppedWidth = true
1251+
controls["resultDropdown"..row_idx].maxDroppedWidth = 600
11791252
self:UpdateDropdownList(row_idx)
11801253
controls["resultDropdown"..row_idx].tooltipFunc = function(tooltip, dropdown_mode, dropdown_index, dropdown_display_string)
11811254
local sortedRow = self.sortedResultTbl[row_idx]
@@ -1192,6 +1265,20 @@ you can add them, copy the link here, and press "Price Item" to evaluate the ite
11921265
local tooltipSlot = slotTbl.selectedJewelNodeId and self.itemsTab.sockets[slotTbl.selectedJewelNodeId] or activeSlot
11931266
self.itemsTab:AddItemTooltip(tooltip, item, tooltipSlot)
11941267
tooltip:AddSeparator(10)
1268+
local eval = result.evaluation
1269+
if eval and eval[1] and eval[1].statDetails then
1270+
local scorePercent = self:GetResultScorePercent(eval[1])
1271+
local scoreColor = scorePercent and scorePercent >= 0 and colorCodes.POSITIVE or colorCodes.NEGATIVE
1272+
tooltip:AddLine(16, "^7Score Breakdown:")
1273+
for _, detail in ipairs(eval[1].statDetails) do
1274+
local color = detail.percentChange >= 0 and colorCodes.POSITIVE or colorCodes.NEGATIVE
1275+
tooltip:AddLine(16, s_format(" %s%s: %+.1f%%^7 (weight: %.2f)", color, detail.label, detail.percentChange, detail.weightMult))
1276+
end
1277+
if scorePercent then
1278+
tooltip:AddLine(16, s_format(" %sOverall: %+.1f%%", scoreColor, scorePercent))
1279+
end
1280+
tooltip:AddSeparator(10)
1281+
end
11951282
tooltip:AddLine(16, string.format("^7Price: %s %s", result.amount, result.currency))
11961283
end
11971284
controls["importButton"..row_idx] = new("ButtonControl", { "TOPLEFT", controls["resultDropdown"..row_idx], "TOPRIGHT"}, {8, 0, 100, row_height}, "Import Item", function()

0 commit comments

Comments
 (0)