Skip to content

Commit 283a745

Browse files
mcagnionclaude
andcommitted
feat(trade): filter by attribute requirements in Trader pane
Adds an "Include unusable" checkbox (off by default) to the Trader pane that hides search results whose Str/Dex/Int (or Omni) requirements the build cannot meet once equipped. Filtering is applied across all sort modes and cached per result to avoid redundant calcFunc calls. When filtering drops every result, the dropdown and total-price state are cleared and a dedicated notice is shown. Adds a matching "Attributes Requirements" checkbox (on by default) to the TradeQueryGenerator popup. When enabled, the shortfall (build requirement minus build attribute) is inserted as pseudo.pseudo_total_* min filters in the generated query so trade search only returns items that cover the missing attributes. Also hardens UI state transitions around filtered results: the result dropdown selection callback guards against stale indices, the section anchor preserves its base Y when the scrollbar offsets it, and empty sorted results no longer crash UpdateDropdownList / UpdateControlsWithItems. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0fc3283 commit 283a745

2 files changed

Lines changed: 190 additions & 29 deletions

File tree

src/Classes/TradeQuery.lua

Lines changed: 152 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ local TradeQueryClass = newClass("TradeQuery", function(self, itemsTab)
3434
-- default set of trade item sort selection
3535
self.slotTables = { }
3636
self.pbItemSortSelectionIndex = 1
37+
self.hideResultsFailingAttributeRequirements = false
3738
self.pbCurrencyConversion = { }
3839
self.currencyConversionTradeMap = { }
3940
self.lastCurrencyConversionRequest = 0
@@ -368,6 +369,20 @@ Highest Weight - Displays the order retrieved from trade]]
368369
self.controls.itemSortSelection:SetSel(self.pbItemSortSelectionIndex, true)
369370
self.controls.itemSortSelectionLabel = new("LabelControl", {"TOPRIGHT", self.controls.itemSortSelection, "TOPLEFT"}, {-4, 0, 56, 16}, "^7Sort By:")
370371

372+
-- Hide fetched results that would leave unmet attribute requirements unless unchecked.
373+
local hideAttributeRequirementsLabel = "^7Hide results failing attribute requirements"
374+
local hideAttributeRequirementsLabelWidth = DrawStringWidth(row_height - 4, "VAR", hideAttributeRequirementsLabel) + 5
375+
local hideAttributeRequirementsRect = {24 + hideAttributeRequirementsLabelWidth, 0, row_height, row_height}
376+
self.controls.hideAttributeRequirementsCheck = new("CheckBoxControl", {"LEFT", self.controls.tradeTypeSelection, "RIGHT"}, hideAttributeRequirementsRect, hideAttributeRequirementsLabel, function(state)
377+
self.hideResultsFailingAttributeRequirements = state
378+
for row_idx, _ in pairs(self.resultTbl) do
379+
self:UpdateControlsWithItems(row_idx)
380+
end
381+
end)
382+
self.controls.hideAttributeRequirementsCheck.tooltipText = "Hide fetched results when equipping the item would leave unmet Str/Dex/Int/Omniscience attribute requirements.\nUnchecked: show those results after fetching."
383+
self.hideResultsFailingAttributeRequirements = self.hideResultsFailingAttributeRequirements == true
384+
self.controls.hideAttributeRequirementsCheck.state = self.hideResultsFailingAttributeRequirements
385+
371386
-- Realm selection
372387
self.controls.realmLabel = new("LabelControl", {"LEFT", self.controls.setSelect, "RIGHT"}, {18, 0, 20, row_height - 4}, "^7Realm:")
373388
self.controls.realm = new("DropDownControl", {"LEFT", self.controls.realmLabel, "RIGHT"}, {6, 0, 150, row_height}, self.realmDropList, function(index, value)
@@ -464,7 +479,9 @@ Highest Weight - Displays the order retrieved from trade]]
464479
t_insert(slotTables, { slotName = self.itemsTab.sockets[nodeId].label, nodeId = nodeId })
465480
end
466481

467-
self.controls.sectionAnchor = new("LabelControl", {"LEFT", self.controls.tradeTypeSelection, "LEFT"}, {0, row_vertical_padding + row_height, 0, 0}, "")
482+
-- Base Y offset for sectionAnchor (used to preserve position when scrollbar shifts it)
483+
local sectionAnchorBaseY = row_vertical_padding + row_height
484+
self.controls.sectionAnchor = new("LabelControl", {"LEFT", self.controls.tradeTypeSelection, "LEFT"}, {0, sectionAnchorBaseY, 0, 0}, "")
468485
top_pane_alignment_ref = {"TOPLEFT", self.controls.sectionAnchor, "TOPLEFT"}
469486
local scrollBarShown = #slotTables > 21 -- clipping starts beyond this
470487
-- dynamically hide rows that are above or below the scrollBar
@@ -542,7 +559,7 @@ Highest Weight - Displays the order retrieved from trade]]
542559
local function scrollBarFunc()
543560
self.controls.scrollBar.height = self.pane_height-100
544561
self.controls.scrollBar:SetContentDimension(self.pane_height-100, self.effective_rows_height)
545-
self.controls.sectionAnchor.y = -self.controls.scrollBar.offset
562+
self.controls.sectionAnchor.y = sectionAnchorBaseY - self.controls.scrollBar.offset
546563
end
547564
main:OpenPopup(pane_width, self.pane_height, "Trader", self.controls, nil, nil, "close", (scrollBarShown and scrollBarFunc or nil))
548565
end
@@ -626,7 +643,7 @@ function TradeQueryClass:SetStatWeights(previousSelectionList)
626643
for row_idx in pairs(self.resultTbl) do
627644
self:UpdateControlsWithItems(row_idx)
628645
end
629-
end)
646+
end)
630647
controls.cancel = new("ButtonControl", { "BOTTOM", nil, "BOTTOM" }, { 0, -10, 80, 20 }, "Cancel", function()
631648
if previousSelectionList and #previousSelectionList > 0 then
632649
self.statSortSelectionList = copyTable(previousSelectionList, true)
@@ -719,6 +736,25 @@ function TradeQueryClass:ReduceOutput(output)
719736
return smallOutput
720737
end
721738

739+
function TradeQueryClass:GetReplacementSlotName(row_idx)
740+
local slotTbl = self.slotTables[row_idx]
741+
if not slotTbl then
742+
return nil
743+
end
744+
if slotTbl.nodeId then
745+
return "Jewel " .. tostring(slotTbl.nodeId)
746+
end
747+
if slotTbl.replacementSlotName then
748+
return slotTbl.replacementSlotName
749+
end
750+
if slotTbl.fullName then
751+
return slotTbl.fullName
752+
end
753+
if self.itemsTab.slots and self.itemsTab.slots[slotTbl.slotName] then
754+
return slotTbl.slotName
755+
end
756+
end
757+
722758
-- Method to evaluate a result by getting it's output and weight
723759
function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, baseOutput)
724760
local result = self.resultTbl[row_idx][result_index]
@@ -738,7 +774,7 @@ function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, ba
738774
self.onlyWeightedBaseOutput[row_idx][result_index] = onlyWeightedBaseOutput
739775
self.lastComparedWeightList[row_idx][result_index] = self.statSortSelectionList
740776
end
741-
local slotName = self.slotTables[row_idx].nodeId and "Jewel " .. tostring(self.slotTables[row_idx].nodeId) or self.slotTables[row_idx].slotName
777+
local slotName = self:GetReplacementSlotName(row_idx) or self.slotTables[row_idx].slotName
742778
if slotName == "Megalomaniac" then
743779
local addedNodes = {}
744780
for nodeName in (result.item_string.."\r\n"):gmatch("1 Added Passive Skill is (.-)\r?\n") do
@@ -776,18 +812,34 @@ function TradeQueryClass:UpdateDropdownList(row_idx)
776812

777813
if not self.resultTbl[row_idx] then return end
778814

779-
for result_index = 1, #self.resultTbl[row_idx] do
780-
781-
local pb_index = self.sortedResultTbl[row_idx][result_index].index
782-
local result = self.resultTbl[row_idx][pb_index]
783-
local price = string.format(" %s(%d %s)", colorCodes["CURRENCY"], result.amount, result.currency)
784-
local item = new("Item", result.item_string)
785-
table.insert(dropdownLabels, colorCodes[item.rarity] .. item.name .. price)
815+
-- Iterate the sorted (and potentially filtered) list so attribute-filtered rows are omitted from the dropdown
816+
for _, sorted in ipairs(self.sortedResultTbl[row_idx] or {}) do
817+
if sorted and sorted.index and self.resultTbl[row_idx][sorted.index] then
818+
local result = self.resultTbl[row_idx][sorted.index]
819+
local price = string.format(" %s(%d %s)", colorCodes["CURRENCY"], result.amount, result.currency)
820+
local item = new("Item", result.item_string)
821+
table.insert(dropdownLabels, colorCodes[item.rarity] .. item.name .. price)
822+
end
823+
end
824+
if self.controls["resultDropdown".. row_idx] then
825+
self.controls["resultDropdown".. row_idx].selIndex = 1
826+
self.controls["resultDropdown".. row_idx]:SetList(dropdownLabels)
786827
end
787-
self.controls["resultDropdown".. row_idx].selIndex = 1
788-
self.controls["resultDropdown".. row_idx]:SetList(dropdownLabels)
789828
end
790829
function TradeQueryClass:UpdateControlsWithItems(row_idx)
830+
local results = self.resultTbl[row_idx]
831+
if not results or #results == 0 then
832+
self.sortedResultTbl[row_idx] = {}
833+
if self.controls["resultDropdown".. row_idx] then
834+
self.controls["resultDropdown".. row_idx]:SetList({})
835+
self.controls["resultDropdown".. row_idx].selIndex = 1
836+
end
837+
self.itemIndexTbl[row_idx] = nil
838+
self.totalPrice[row_idx] = nil
839+
self.controls.fullPrice.label = "Total Price: " .. self:GetTotalPriceString()
840+
return
841+
end
842+
791843
local sortMode = self.itemSortSelectionList[self.pbItemSortSelectionIndex]
792844
local sortedItems, errMsg = self:SortFetchResults(row_idx, sortMode)
793845
if errMsg == "MissingConversionRates" then
@@ -800,6 +852,18 @@ function TradeQueryClass:UpdateControlsWithItems(row_idx)
800852
else
801853
self:SetNotice(self.controls.pbNotice, "")
802854
end
855+
if not sortedItems or #sortedItems == 0 then
856+
self:SetNotice(self.controls.pbNotice, "No usable results (attribute requirements)")
857+
self.sortedResultTbl[row_idx] = {}
858+
if self.controls["resultDropdown".. row_idx] then
859+
self.controls["resultDropdown".. row_idx]:SetList({})
860+
self.controls["resultDropdown".. row_idx].selIndex = 1
861+
end
862+
self.itemIndexTbl[row_idx] = nil
863+
self.totalPrice[row_idx] = nil
864+
self.controls.fullPrice.label = "Total Price: " .. self:GetTotalPriceString()
865+
return
866+
end
803867

804868
self.sortedResultTbl[row_idx] = sortedItems
805869
local pb_index = self.sortedResultTbl[row_idx][1].index
@@ -827,6 +891,39 @@ end
827891
-- Method to sort the fetched results
828892
function TradeQueryClass:SortFetchResults(row_idx, mode)
829893
local calcFunc, baseOutput
894+
local attrReqCache = {}
895+
local slotName = self:GetReplacementSlotName(row_idx)
896+
local results = self.resultTbl[row_idx]
897+
if not results or #results == 0 then
898+
return {}
899+
end
900+
901+
-- Returns true if the candidate item meets its attribute requirements when equipped
902+
local function meetsAttributeRequirements(result_index)
903+
if not self.hideResultsFailingAttributeRequirements or not slotName then
904+
return true
905+
end
906+
if attrReqCache[result_index] ~= nil then
907+
return attrReqCache[result_index]
908+
end
909+
if not calcFunc then
910+
calcFunc, baseOutput = self.itemsTab.build.calcsTab:GetMiscCalculator()
911+
end
912+
local item = new("Item", self.resultTbl[row_idx][result_index].item_string)
913+
local output = calcFunc({ repSlotName = slotName, repItem = item })
914+
local ok
915+
if output.ReqOmni then
916+
ok = (output.ReqOmni or 0) <= (output.Omni or 0)
917+
else
918+
local function attrOk(reqKey, attrKey)
919+
return (output[reqKey] or 0) <= (output[attrKey] or 0)
920+
end
921+
ok = attrOk("ReqStr", "Str") and attrOk("ReqDex", "Dex") and attrOk("ReqInt", "Int")
922+
end
923+
attrReqCache[result_index] = ok
924+
return ok
925+
end
926+
830927
local function getResultWeight(result_index)
831928
if not calcFunc then
832929
calcFunc, baseOutput = self.itemsTab.build.calcsTab:GetMiscCalculator()
@@ -854,13 +951,17 @@ function TradeQueryClass:SortFetchResults(row_idx, mode)
854951
local newTbl = {}
855952
if mode == self.sortModes.Weight then
856953
for index, _ in pairs(self.resultTbl[row_idx]) do
857-
t_insert(newTbl, { outputAttr = index, index = index })
954+
if meetsAttributeRequirements(index) then
955+
t_insert(newTbl, { outputAttr = index, index = index })
956+
end
858957
end
859958
return newTbl
860959
elseif mode == self.sortModes.StatValue then
861960
for result_index = 1, #self.resultTbl[row_idx] do
862961
--ConPrintf("%.3f", getResultWeight(result_index))
863-
t_insert(newTbl, { outputAttr = getResultWeight(result_index), index = result_index })
962+
if meetsAttributeRequirements(result_index) then
963+
t_insert(newTbl, { outputAttr = getResultWeight(result_index), index = result_index })
964+
end
864965
end
865966
table.sort(newTbl, function(a,b) return a.outputAttr > b.outputAttr end)
866967
elseif mode == self.sortModes.StatValuePrice then
@@ -880,9 +981,11 @@ function TradeQueryClass:SortFetchResults(row_idx, mode)
880981

881982
-- scaling factor for price
882983
local k = 0.03
883-
t_insert(newTbl,
884-
{ outputAttr = getResultWeight(result_index) - k * math.log(priceTable[result_index], 10), index =
885-
result_index })
984+
if meetsAttributeRequirements(result_index) then
985+
t_insert(newTbl,
986+
{ outputAttr = getResultWeight(result_index) - k * math.log(priceTable[result_index], 10), index =
987+
result_index })
988+
end
886989
end
887990
table.sort(newTbl, function(a,b) return a.outputAttr > b.outputAttr end)
888991
elseif mode == self.sortModes.Price then
@@ -891,7 +994,9 @@ function TradeQueryClass:SortFetchResults(row_idx, mode)
891994
return nil, "MissingConversionRates"
892995
end
893996
for result_index, price in pairs(priceTable) do
894-
t_insert(newTbl, { outputAttr = price, index = result_index })
997+
if meetsAttributeRequirements(result_index) then
998+
t_insert(newTbl, { outputAttr = price, index = result_index })
999+
end
8951000
end
8961001
table.sort(newTbl, function(a,b) return a.outputAttr < b.outputAttr end)
8971002
else
@@ -945,6 +1050,7 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro
9451050
slotTbl.slotName and (self.itemsTab.slots[slotTbl.slotName] or
9461051
slotTbl.slotName == "Watcher's Eye" and self:findValidSlotForWatchersEye() or
9471052
slotTbl.fullName and self.itemsTab.slots[slotTbl.fullName]) -- fullName for Abyssal Sockets
1053+
slotTbl.replacementSlotName = activeSlot and activeSlot.slotName or slotTbl.fullName or nil
9481054
local nameColor = slotTbl.unique and colorCodes.UNIQUE or "^7"
9491055
controls["name"..row_idx] = new("LabelControl", top_pane_alignment_ref, {0, row_idx*(row_height + row_vertical_padding), 100, row_height - 4}, nameColor..slotTbl.slotName)
9501056
controls["bestButton"..row_idx] = new("ButtonControl", { "LEFT", controls["name"..row_idx], "LEFT"}, {100 + 8, 0, 80, row_height}, "Find best", function()
@@ -1076,8 +1182,10 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro
10761182
end)
10771183
controls["changeButton"..row_idx].shown = function() return self.resultTbl[row_idx] end
10781184
controls["resultDropdown"..row_idx] = new("DropDownControl", { "TOPLEFT", controls["changeButton"..row_idx], "TOPRIGHT"}, {8, 0, 325, row_height}, {}, function(index)
1079-
self.itemIndexTbl[row_idx] = self.sortedResultTbl[row_idx][index].index
1080-
self:SetFetchResultReturn(row_idx, self.itemIndexTbl[row_idx])
1185+
if self.sortedResultTbl[row_idx] and self.sortedResultTbl[row_idx][index] then
1186+
self.itemIndexTbl[row_idx] = self.sortedResultTbl[row_idx][index].index
1187+
self:SetFetchResultReturn(row_idx, self.itemIndexTbl[row_idx])
1188+
end
10811189
end)
10821190
self:UpdateDropdownList(row_idx)
10831191
local function addMegalomaniacCompareToTooltipIfApplicable(tooltip, result_index)
@@ -1117,8 +1225,17 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro
11171225
tooltip:AddSeparator(10)
11181226
tooltip:AddLine(16, string.format("^7Price: %s %s", result.amount, result.currency))
11191227
end
1228+
local function getSelectedResult()
1229+
local selected_result_index = self.itemIndexTbl[row_idx]
1230+
local rowResults = self.resultTbl[row_idx]
1231+
return selected_result_index and rowResults and rowResults[selected_result_index], selected_result_index
1232+
end
11201233
controls["importButton"..row_idx] = new("ButtonControl", { "TOPLEFT", controls["resultDropdown"..row_idx], "TOPRIGHT"}, {8, 0, 100, row_height}, "Import Item", function()
1121-
self.itemsTab:CreateDisplayItemFromRaw(self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].item_string)
1234+
local itemResult = getSelectedResult()
1235+
if not itemResult or not itemResult.item_string then
1236+
return
1237+
end
1238+
self.itemsTab:CreateDisplayItemFromRaw(itemResult.item_string)
11221239
local item = self.itemsTab.displayItem
11231240
-- pass "true" to not auto equip it as we will have our own logic
11241241
self.itemsTab:AddDisplayItem(true)
@@ -1133,8 +1250,8 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro
11331250
end)
11341251
controls["importButton"..row_idx].tooltipFunc = function(tooltip)
11351252
tooltip:Clear()
1136-
local selected_result_index = self.itemIndexTbl[row_idx]
1137-
local item_string = self.resultTbl[row_idx][selected_result_index].item_string
1253+
local itemResult, selected_result_index = getSelectedResult()
1254+
local item_string = itemResult and itemResult.item_string
11381255
if selected_result_index and item_string then
11391256
-- TODO: item parsing bug caught here.
11401257
-- item.baseName is nil and throws error in the following AddItemTooltip func
@@ -1150,12 +1267,13 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro
11501267
end
11511268
end
11521269
controls["importButton"..row_idx].enabled = function()
1153-
return self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].item_string ~= nil
1270+
local itemResult = getSelectedResult()
1271+
return itemResult and itemResult.item_string ~= nil
11541272
end
11551273
-- Whisper so we can copy to clipboard
11561274
controls["whisperButton" .. row_idx] = new("ButtonControl",
11571275
{ "TOPLEFT", controls["importButton" .. row_idx], "TOPRIGHT" }, { 8, 0, 170, row_height }, function()
1158-
local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]]
1276+
local itemResult = getSelectedResult()
11591277

11601278
if not itemResult then return "" end
11611279

@@ -1169,7 +1287,10 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro
11691287
end
11701288

11711289
end, function()
1172-
local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]]
1290+
local itemResult = getSelectedResult()
1291+
if not itemResult then
1292+
return
1293+
end
11731294
if itemResult.whisper then
11741295
Copy(itemResult.whisper)
11751296
else
@@ -1199,7 +1320,10 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro
11991320
controls["whisperButton" .. row_idx].tooltipFunc = function(tooltip)
12001321
tooltip:Clear()
12011322
tooltip.center = true
1202-
local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]]
1323+
local itemResult = getSelectedResult()
1324+
if not itemResult then
1325+
return
1326+
end
12031327
local text = itemResult.whisper and "Copies the item purchase whisper to the clipboard" or
12041328
"Opens the search page to show the item"
12051329
tooltip:AddLine(16, text)

0 commit comments

Comments
 (0)