diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index befb96a657..8999d72607 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -1,6 +1,58 @@ +local dkjson = require "dkjson" + describe("TradeQueryGenerator", function() local mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} }) + local function findStatFilter(queryTable, id) + for _, group in ipairs(queryTable.query.stats) do + for _, filter in ipairs(group.filters or {}) do + if filter.id == id then + return filter + end + end + end + end + + local function finishQueryWithAttributeShortfall(shortfall, includeAttrReqs) + local queryGen = new("TradeQueryGenerator", { itemsTab = {} }) + local queryTable + local errMsg + queryGen.modWeights = { + { tradeModId = "explicit.stat_3299347043", weight = 1, meanStatDiff = 1 }, + } + queryGen.tradeTypeIndex = 1 + queryGen.requesterContext = {} + queryGen.requesterCallback = function(_, queryJson, queryErrMsg) + queryTable = dkjson.decode(queryJson) + errMsg = queryErrMsg + end + queryGen.calcContext = { + itemCategoryQueryStr = "ring", + special = {}, + testItem = { + BuildAndParseRaw = function() end, + }, + baseOutput = { TotalDPS = 100 }, + baseStatValue = 0, + options = { + statWeights = { { stat = "TotalDPS", weightMult = 1 } }, + includeAllWEMods = false, + includeAttrReqs = includeAttrReqs, + includeMirrored = true, + influence1 = 1, + influence2 = 1, + }, + attrReqShortfall = shortfall, + } + + local previousClosePopup = main.ClosePopup + main.ClosePopup = function() end + queryGen:FinishQuery() + main.ClosePopup = previousClosePopup + + return queryTable, errMsg + end + describe("ProcessMod", function() -- Pass: Mod line maps correctly to trade stat entry without error -- Fail: Mapping fails (e.g., no match found), indicating incomplete stat parsing for curse mods, potentially missing curse-enabling items in queries @@ -57,4 +109,25 @@ describe("TradeQueryGenerator", function() _G.MAX_FILTERS = orig_max end) end) + + describe("attribute requirement filters", function() + it("adds needed attribute pseudo filters to the generated query", function() + local queryTable, errMsg = finishQueryWithAttributeShortfall({ Str = 12, Dex = 34, Int = 56 }, true) + assert.is_nil(errMsg) + assert.are.equal(12, findStatFilter(queryTable, "pseudo.pseudo_total_strength").value.min) + assert.are.equal(34, findStatFilter(queryTable, "pseudo.pseudo_total_dexterity").value.min) + assert.are.equal(56, findStatFilter(queryTable, "pseudo.pseudo_total_intelligence").value.min) + end) + + it("omits attribute pseudo filters when disabled or no shortfall exists", function() + local disabledQuery = finishQueryWithAttributeShortfall({ Str = 12, Dex = 34, Int = 56 }, false) + local zeroQuery = finishQueryWithAttributeShortfall({ Str = 0, Dex = 0, Int = 0 }, true) + assert.is_nil(findStatFilter(disabledQuery, "pseudo.pseudo_total_strength")) + assert.is_nil(findStatFilter(disabledQuery, "pseudo.pseudo_total_dexterity")) + assert.is_nil(findStatFilter(disabledQuery, "pseudo.pseudo_total_intelligence")) + assert.is_nil(findStatFilter(zeroQuery, "pseudo.pseudo_total_strength")) + assert.is_nil(findStatFilter(zeroQuery, "pseudo.pseudo_total_dexterity")) + assert.is_nil(findStatFilter(zeroQuery, "pseudo.pseudo_total_intelligence")) + end) + end) end) diff --git a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua index 332374a839..053143bc83 100644 --- a/spec/System/TestTradeQuery_spec.lua +++ b/spec/System/TestTradeQuery_spec.lua @@ -53,5 +53,90 @@ describe("TradeQuery", function() end) assert.are.equal(0, #tooltip.lines) end) + + it("returns early from action button tooltips when filtering clears the selected result", function() + local tq = newTradeQuery({ + resultTbl = { [1] = { [1] = { item_string = "Rarity: RARE\nBehemoth Hold\nGold Ring", amount = 1, currency = "chaos" } } }, + sortedResultTbl = { [1] = {} }, + }) + buildRow1Dropdown(tq) + local tooltip = new("Tooltip") + + assert.has_no.errors(function() + tq.controls.importButton1.tooltipFunc(tooltip) + tq.controls.whisperButton1.tooltipFunc(tooltip) + end) + assert.are.equal(0, #tooltip.lines) + end) + end) + + describe("attribute requirement result filtering", function() + local function newTradeQueryWithOutput(output, slotTbl) + local calcCalls = 0 + local tq = new("TradeQuery", { itemsTab = {} }) + tq.slotTables[1] = slotTbl or { slotName = "Ring 1" } + tq.resultTbl = { + [1] = { + [1] = { item_string = "Rarity: RARE\nBehemoth Hold\nGold Ring", amount = 1, currency = "chaos" }, + }, + } + tq.sortModes = { + Weight = "(Highest) Weighted Sum", + } + tq.itemsTab.build = { + calcsTab = { + GetMiscCalculator = function() + return function() + calcCalls = calcCalls + 1 + return output + end, {} + end, + }, + } + tq.itemsTab.slots = { + ["Ring 1"] = {}, + } + return tq, function() + return calcCalls + end + end + + it("filters fetched results that do not meet attribute requirements", function() + local tq = newTradeQueryWithOutput({ ReqStr = 50, Str = 40, ReqDex = 0, Dex = 0, ReqInt = 0, Int = 0 }) + tq.hideResultsFailingAttributeRequirements = true + local sortedItems = tq:SortFetchResults(1, tq.sortModes.Weight) + assert.are.equal(0, #sortedItems) + end) + + it("keeps fetched results that meet attribute requirements", function() + local tq = newTradeQueryWithOutput({ ReqStr = 50, Str = 60, ReqDex = 30, Dex = 30, ReqInt = 20, Int = 25 }) + tq.hideResultsFailingAttributeRequirements = true + local sortedItems = tq:SortFetchResults(1, tq.sortModes.Weight) + assert.are.equal(1, #sortedItems) + assert.are.equal(1, sortedItems[1].index) + end) + + it("filters fetched results that do not meet Omniscience requirements", function() + local tq = newTradeQueryWithOutput({ ReqOmni = 100, Omni = 80 }) + tq.hideResultsFailingAttributeRequirements = true + local sortedItems = tq:SortFetchResults(1, tq.sortModes.Weight) + assert.are.equal(0, #sortedItems) + end) + + it("keeps fetched results without recalculating by default", function() + local tq, calcCalls = newTradeQueryWithOutput({ ReqStr = 50, Str = 40, ReqDex = 0, Dex = 0, ReqInt = 0, Int = 0 }) + local sortedItems = tq:SortFetchResults(1, tq.sortModes.Weight) + assert.are.equal(1, #sortedItems) + assert.are.equal(1, sortedItems[1].index) + assert.are.equal(0, calcCalls()) + end) + + it("does not apply equipment attribute filtering to rows without a replacement slot", function() + local tq, calcCalls = newTradeQueryWithOutput({ ReqStr = 50, Str = 40, ReqDex = 0, Dex = 0, ReqInt = 0, Int = 0 }, { slotName = "Megalomaniac", unique = true }) + local sortedItems = tq:SortFetchResults(1, tq.sortModes.Weight) + assert.are.equal(1, #sortedItems) + assert.are.equal(1, sortedItems[1].index) + assert.are.equal(0, calcCalls()) + end) end) end) diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 9e1308bfb9..7c24f17028 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -34,6 +34,7 @@ local TradeQueryClass = newClass("TradeQuery", function(self, itemsTab) -- default set of trade item sort selection self.slotTables = { } self.pbItemSortSelectionIndex = 1 + self.hideResultsFailingAttributeRequirements = false self.pbCurrencyConversion = { } self.currencyConversionTradeMap = { } self.lastCurrencyConversionRequest = 0 @@ -368,6 +369,20 @@ Highest Weight - Displays the order retrieved from trade]] self.controls.itemSortSelection:SetSel(self.pbItemSortSelectionIndex, true) self.controls.itemSortSelectionLabel = new("LabelControl", {"TOPRIGHT", self.controls.itemSortSelection, "TOPLEFT"}, {-4, 0, 56, 16}, "^7Sort By:") + -- Hide fetched results that would leave unmet attribute requirements unless unchecked. + local hideAttributeRequirementsLabel = "^7Hide results failing attribute requirements" + local hideAttributeRequirementsLabelWidth = DrawStringWidth(row_height - 4, "VAR", hideAttributeRequirementsLabel) + 5 + local hideAttributeRequirementsRect = {24 + hideAttributeRequirementsLabelWidth, 0, row_height, row_height} + self.controls.hideAttributeRequirementsCheck = new("CheckBoxControl", {"LEFT", self.controls.tradeTypeSelection, "RIGHT"}, hideAttributeRequirementsRect, hideAttributeRequirementsLabel, function(state) + self.hideResultsFailingAttributeRequirements = state + for row_idx, _ in pairs(self.resultTbl) do + self:UpdateControlsWithItems(row_idx) + end + end) + 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." + self.hideResultsFailingAttributeRequirements = self.hideResultsFailingAttributeRequirements == true + self.controls.hideAttributeRequirementsCheck.state = self.hideResultsFailingAttributeRequirements + -- Realm selection self.controls.realmLabel = new("LabelControl", {"LEFT", self.controls.setSelect, "RIGHT"}, {18, 0, 20, row_height - 4}, "^7Realm:") 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]] t_insert(slotTables, { slotName = self.itemsTab.sockets[nodeId].label, nodeId = nodeId }) end - self.controls.sectionAnchor = new("LabelControl", {"LEFT", self.controls.tradeTypeSelection, "LEFT"}, {0, row_vertical_padding + row_height, 0, 0}, "") + -- Base Y offset for sectionAnchor (used to preserve position when scrollbar shifts it) + local sectionAnchorBaseY = row_vertical_padding + row_height + self.controls.sectionAnchor = new("LabelControl", {"LEFT", self.controls.tradeTypeSelection, "LEFT"}, {0, sectionAnchorBaseY, 0, 0}, "") top_pane_alignment_ref = {"TOPLEFT", self.controls.sectionAnchor, "TOPLEFT"} local scrollBarShown = #slotTables > 21 -- clipping starts beyond this -- dynamically hide rows that are above or below the scrollBar @@ -542,7 +559,7 @@ Highest Weight - Displays the order retrieved from trade]] local function scrollBarFunc() self.controls.scrollBar.height = self.pane_height-100 self.controls.scrollBar:SetContentDimension(self.pane_height-100, self.effective_rows_height) - self.controls.sectionAnchor.y = -self.controls.scrollBar.offset + self.controls.sectionAnchor.y = sectionAnchorBaseY - self.controls.scrollBar.offset end main:OpenPopup(pane_width, self.pane_height, "Trader", self.controls, nil, nil, "close", (scrollBarShown and scrollBarFunc or nil)) end @@ -626,7 +643,7 @@ function TradeQueryClass:SetStatWeights(previousSelectionList) for row_idx in pairs(self.resultTbl) do self:UpdateControlsWithItems(row_idx) end - end) + end) controls.cancel = new("ButtonControl", { "BOTTOM", nil, "BOTTOM" }, { 0, -10, 80, 20 }, "Cancel", function() if previousSelectionList and #previousSelectionList > 0 then self.statSortSelectionList = copyTable(previousSelectionList, true) @@ -719,6 +736,25 @@ function TradeQueryClass:ReduceOutput(output) return smallOutput end +function TradeQueryClass:GetReplacementSlotName(row_idx) + local slotTbl = self.slotTables[row_idx] + if not slotTbl then + return nil + end + if slotTbl.nodeId then + return "Jewel " .. tostring(slotTbl.nodeId) + end + if slotTbl.replacementSlotName then + return slotTbl.replacementSlotName + end + if slotTbl.fullName then + return slotTbl.fullName + end + if self.itemsTab.slots and self.itemsTab.slots[slotTbl.slotName] then + return slotTbl.slotName + end +end + -- Method to evaluate a result by getting it's output and weight function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, baseOutput) local result = self.resultTbl[row_idx][result_index] @@ -738,7 +774,7 @@ function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, ba self.onlyWeightedBaseOutput[row_idx][result_index] = onlyWeightedBaseOutput self.lastComparedWeightList[row_idx][result_index] = self.statSortSelectionList end - local slotName = self.slotTables[row_idx].nodeId and "Jewel " .. tostring(self.slotTables[row_idx].nodeId) or self.slotTables[row_idx].slotName + local slotName = self:GetReplacementSlotName(row_idx) or self.slotTables[row_idx].slotName if slotName == "Megalomaniac" then local addedNodes = {} 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) if not self.resultTbl[row_idx] then return end - for result_index = 1, #self.resultTbl[row_idx] do - - local pb_index = self.sortedResultTbl[row_idx][result_index].index - local result = self.resultTbl[row_idx][pb_index] - local price = string.format(" %s(%d %s)", colorCodes["CURRENCY"], result.amount, result.currency) - local item = new("Item", result.item_string) - table.insert(dropdownLabels, colorCodes[item.rarity] .. item.name .. price) + -- Iterate the sorted (and potentially filtered) list so attribute-filtered rows are omitted from the dropdown + for _, sorted in ipairs(self.sortedResultTbl[row_idx] or {}) do + if sorted and sorted.index and self.resultTbl[row_idx][sorted.index] then + local result = self.resultTbl[row_idx][sorted.index] + local price = string.format(" %s(%d %s)", colorCodes["CURRENCY"], result.amount, result.currency) + local item = new("Item", result.item_string) + table.insert(dropdownLabels, colorCodes[item.rarity] .. item.name .. price) + end + end + if self.controls["resultDropdown".. row_idx] then + self.controls["resultDropdown".. row_idx].selIndex = 1 + self.controls["resultDropdown".. row_idx]:SetList(dropdownLabels) end - self.controls["resultDropdown".. row_idx].selIndex = 1 - self.controls["resultDropdown".. row_idx]:SetList(dropdownLabels) end function TradeQueryClass:UpdateControlsWithItems(row_idx) + local results = self.resultTbl[row_idx] + if not results or #results == 0 then + self.sortedResultTbl[row_idx] = {} + if self.controls["resultDropdown".. row_idx] then + self.controls["resultDropdown".. row_idx]:SetList({}) + self.controls["resultDropdown".. row_idx].selIndex = 1 + end + self.itemIndexTbl[row_idx] = nil + self.totalPrice[row_idx] = nil + self.controls.fullPrice.label = "Total Price: " .. self:GetTotalPriceString() + return + end + local sortMode = self.itemSortSelectionList[self.pbItemSortSelectionIndex] local sortedItems, errMsg = self:SortFetchResults(row_idx, sortMode) if errMsg == "MissingConversionRates" then @@ -800,6 +852,18 @@ function TradeQueryClass:UpdateControlsWithItems(row_idx) else self:SetNotice(self.controls.pbNotice, "") end + if not sortedItems or #sortedItems == 0 then + self:SetNotice(self.controls.pbNotice, "No usable results (attribute requirements)") + self.sortedResultTbl[row_idx] = {} + if self.controls["resultDropdown".. row_idx] then + self.controls["resultDropdown".. row_idx]:SetList({}) + self.controls["resultDropdown".. row_idx].selIndex = 1 + end + self.itemIndexTbl[row_idx] = nil + self.totalPrice[row_idx] = nil + self.controls.fullPrice.label = "Total Price: " .. self:GetTotalPriceString() + return + end self.sortedResultTbl[row_idx] = sortedItems local pb_index = self.sortedResultTbl[row_idx][1].index @@ -827,6 +891,39 @@ end -- Method to sort the fetched results function TradeQueryClass:SortFetchResults(row_idx, mode) local calcFunc, baseOutput + local attrReqCache = {} + local slotName = self:GetReplacementSlotName(row_idx) + local results = self.resultTbl[row_idx] + if not results or #results == 0 then + return {} + end + + -- Returns true if the candidate item meets its attribute requirements when equipped + local function meetsAttributeRequirements(result_index) + if not self.hideResultsFailingAttributeRequirements or not slotName then + return true + end + if attrReqCache[result_index] ~= nil then + return attrReqCache[result_index] + end + if not calcFunc then + calcFunc, baseOutput = self.itemsTab.build.calcsTab:GetMiscCalculator() + end + local item = new("Item", self.resultTbl[row_idx][result_index].item_string) + local output = calcFunc({ repSlotName = slotName, repItem = item }) + local ok + if output.ReqOmni then + ok = (output.ReqOmni or 0) <= (output.Omni or 0) + else + local function attrOk(reqKey, attrKey) + return (output[reqKey] or 0) <= (output[attrKey] or 0) + end + ok = attrOk("ReqStr", "Str") and attrOk("ReqDex", "Dex") and attrOk("ReqInt", "Int") + end + attrReqCache[result_index] = ok + return ok + end + local function getResultWeight(result_index) if not calcFunc then calcFunc, baseOutput = self.itemsTab.build.calcsTab:GetMiscCalculator() @@ -854,13 +951,17 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) local newTbl = {} if mode == self.sortModes.Weight then for index, _ in pairs(self.resultTbl[row_idx]) do - t_insert(newTbl, { outputAttr = index, index = index }) + if meetsAttributeRequirements(index) then + t_insert(newTbl, { outputAttr = index, index = index }) + end end return newTbl elseif mode == self.sortModes.StatValue then for result_index = 1, #self.resultTbl[row_idx] do --ConPrintf("%.3f", getResultWeight(result_index)) - t_insert(newTbl, { outputAttr = getResultWeight(result_index), index = result_index }) + if meetsAttributeRequirements(result_index) then + t_insert(newTbl, { outputAttr = getResultWeight(result_index), index = result_index }) + end end table.sort(newTbl, function(a,b) return a.outputAttr > b.outputAttr end) elseif mode == self.sortModes.StatValuePrice then @@ -880,9 +981,11 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) -- scaling factor for price local k = 0.03 - t_insert(newTbl, - { outputAttr = getResultWeight(result_index) - k * math.log(priceTable[result_index], 10), index = - result_index }) + if meetsAttributeRequirements(result_index) then + t_insert(newTbl, + { outputAttr = getResultWeight(result_index) - k * math.log(priceTable[result_index], 10), index = + result_index }) + end end table.sort(newTbl, function(a,b) return a.outputAttr > b.outputAttr end) elseif mode == self.sortModes.Price then @@ -891,7 +994,9 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) return nil, "MissingConversionRates" end for result_index, price in pairs(priceTable) do - t_insert(newTbl, { outputAttr = price, index = result_index }) + if meetsAttributeRequirements(result_index) then + t_insert(newTbl, { outputAttr = price, index = result_index }) + end end table.sort(newTbl, function(a,b) return a.outputAttr < b.outputAttr end) else @@ -945,6 +1050,7 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro slotTbl.slotName and (self.itemsTab.slots[slotTbl.slotName] or slotTbl.slotName == "Watcher's Eye" and self:findValidSlotForWatchersEye() or slotTbl.fullName and self.itemsTab.slots[slotTbl.fullName]) -- fullName for Abyssal Sockets + slotTbl.replacementSlotName = activeSlot and activeSlot.slotName or slotTbl.fullName or nil local nameColor = slotTbl.unique and colorCodes.UNIQUE or "^7" 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) 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 end) controls["changeButton"..row_idx].shown = function() return self.resultTbl[row_idx] end controls["resultDropdown"..row_idx] = new("DropDownControl", { "TOPLEFT", controls["changeButton"..row_idx], "TOPRIGHT"}, {8, 0, 325, row_height}, {}, function(index) - self.itemIndexTbl[row_idx] = self.sortedResultTbl[row_idx][index].index - self:SetFetchResultReturn(row_idx, self.itemIndexTbl[row_idx]) + if self.sortedResultTbl[row_idx] and self.sortedResultTbl[row_idx][index] then + self.itemIndexTbl[row_idx] = self.sortedResultTbl[row_idx][index].index + self:SetFetchResultReturn(row_idx, self.itemIndexTbl[row_idx]) + end end) self:UpdateDropdownList(row_idx) local function addMegalomaniacCompareToTooltipIfApplicable(tooltip, result_index) @@ -1117,8 +1225,17 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro tooltip:AddSeparator(10) tooltip:AddLine(16, string.format("^7Price: %s %s", result.amount, result.currency)) end + local function getSelectedResult() + local selected_result_index = self.itemIndexTbl[row_idx] + local rowResults = self.resultTbl[row_idx] + return selected_result_index and rowResults and rowResults[selected_result_index], selected_result_index + end controls["importButton"..row_idx] = new("ButtonControl", { "TOPLEFT", controls["resultDropdown"..row_idx], "TOPRIGHT"}, {8, 0, 100, row_height}, "Import Item", function() - self.itemsTab:CreateDisplayItemFromRaw(self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].item_string) + local itemResult = getSelectedResult() + if not itemResult or not itemResult.item_string then + return + end + self.itemsTab:CreateDisplayItemFromRaw(itemResult.item_string) local item = self.itemsTab.displayItem -- pass "true" to not auto equip it as we will have our own logic self.itemsTab:AddDisplayItem(true) @@ -1133,8 +1250,8 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end) controls["importButton"..row_idx].tooltipFunc = function(tooltip) tooltip:Clear() - local selected_result_index = self.itemIndexTbl[row_idx] - local item_string = self.resultTbl[row_idx][selected_result_index].item_string + local itemResult, selected_result_index = getSelectedResult() + local item_string = itemResult and itemResult.item_string if selected_result_index and item_string then -- TODO: item parsing bug caught here. -- 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 end end controls["importButton"..row_idx].enabled = function() - return self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].item_string ~= nil + local itemResult = getSelectedResult() + return itemResult and itemResult.item_string ~= nil end -- Whisper so we can copy to clipboard controls["whisperButton" .. row_idx] = new("ButtonControl", { "TOPLEFT", controls["importButton" .. row_idx], "TOPRIGHT" }, { 8, 0, 170, row_height }, function() - local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + local itemResult = getSelectedResult() if not itemResult then return "" end @@ -1169,7 +1287,10 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end end, function() - local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + local itemResult = getSelectedResult() + if not itemResult then + return + end if itemResult.whisper then Copy(itemResult.whisper) else @@ -1199,7 +1320,10 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro controls["whisperButton" .. row_idx].tooltipFunc = function(tooltip) tooltip:Clear() tooltip.center = true - local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + local itemResult = getSelectedResult() + if not itemResult then + return + end local text = itemResult.whisper and "Copies the item purchase whisper to the clipboard" or "Opens the search page to show the item" tooltip:AddLine(16, text) diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index eeb2fdeaab..7be583a5b1 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -799,6 +799,16 @@ function TradeQueryGeneratorClass:StartQuery(slot, options) -- Calculate base output with a blank item local calcFunc, baseOutput = self.itemsTab.build.calcsTab:GetMiscCalculator() local baseItemOutput = slot and calcFunc({ repSlotName = slot.slotName, repItem = testItem }) or baseOutput + -- Determine attribute shortfall when replacing the current item with a blank base + local attrReqShortfall = { Str = 0, Dex = 0, Int = 0 } + if slot and (not slot.slotName:find("Flask")) then + local needStr = math.max(0, (baseItemOutput.ReqStr or 0) - (baseItemOutput.Str or 0)) + local needDex = math.max(0, (baseItemOutput.ReqDex or 0) - (baseItemOutput.Dex or 0)) + local needInt = math.max(0, (baseItemOutput.ReqInt or 0) - (baseItemOutput.Int or 0)) + attrReqShortfall.Str = needStr + attrReqShortfall.Dex = needDex + attrReqShortfall.Int = needInt + end -- make weights more human readable local compStatValue = TradeQueryGeneratorClass.WeightedRatioOutputs(baseOutput, baseItemOutput, options.statWeights) * 1000 @@ -816,6 +826,7 @@ function TradeQueryGeneratorClass:StartQuery(slot, options) calcFunc = calcFunc, options = options, slot = slot, + attrReqShortfall = attrReqShortfall, } -- OnFrame will pick this up and begin the work @@ -1028,6 +1039,23 @@ function TradeQueryGeneratorClass:FinishQuery() filters = filters + 1 end + -- If enabled, require the new item to provide enough attributes to meet build requirements + if options.includeAttrReqs and self.calcContext and self.calcContext.attrReqShortfall then + local need = self.calcContext.attrReqShortfall + if need.Str and need.Str > 0 then + t_insert(andFilters.filters, { id = "pseudo.pseudo_total_strength", value = { min = need.Str } }) + filters = filters + 1 + end + if need.Dex and need.Dex > 0 then + t_insert(andFilters.filters, { id = "pseudo.pseudo_total_dexterity", value = { min = need.Dex } }) + filters = filters + 1 + end + if need.Int and need.Int > 0 then + t_insert(andFilters.filters, { id = "pseudo.pseudo_total_intelligence", value = { min = need.Int } }) + filters = filters + 1 + end + end + if #andFilters.filters > 0 then t_insert(queryTable.query.stats, andFilters) end @@ -1261,6 +1289,12 @@ Remove: %s will be removed from the search results.]], term, term, term) controls.maxLevelLabel = new("LabelControl", {"RIGHT",controls.maxLevel,"LEFT"}, {-5, 0, 0, 16}, "Max Level:") updateLastAnchor(controls.maxLevel) + -- When enabled, the generated query asks for enough attributes on the new item + controls.includeAttrReqs = new("CheckBoxControl", {"TOPLEFT",lastItemAnchor,"BOTTOMLEFT"}, {0, 5, 18}, "Attribute requirements:", function(state) end) + controls.includeAttrReqs.state = (self.lastIncludeAttrReqs == nil or self.lastIncludeAttrReqs == true) + controls.includeAttrReqs.tooltipText = "Add Str/Dex/Int pseudo filters when the current build is short on attributes.\nThis narrows the generated trade query before fetching results." + updateLastAnchor(controls.includeAttrReqs) + -- basic filtering by slot for sockets and links, Megalomaniac does not have slot and Sockets use "Jewel nodeId" if slot and not isJewelSlot and not isAbyssalJewelSlot and not slot.slotName:find("Flask") then controls.sockets = new("EditControl", {"TOPLEFT",lastItemAnchor,"BOTTOMLEFT"}, {0, 5, 70, 18}, nil, nil, "%D") @@ -1356,6 +1390,9 @@ Remove: %s will be removed from the search results.]], term, term, term) options.maxLevel = tonumber(controls.maxLevel.buf) self.lastMaxLevel = options.maxLevel end + if controls.includeAttrReqs then + self.lastIncludeAttrReqs, options.includeAttrReqs = controls.includeAttrReqs.state, controls.includeAttrReqs.state + end if controls.sockets and controls.sockets.buf then options.sockets = tonumber(controls.sockets.buf) self.lastSockets = options.sockets @@ -1374,4 +1411,4 @@ Remove: %s will be removed from the search results.]], term, term, term) main:ClosePopup() end) main:OpenPopup(400, popupHeight, "Query Options", controls) -end \ No newline at end of file +end