diff --git a/spec/System/TestTradeQueryCurrency_spec.lua b/spec/System/TestTradeQueryCurrency_spec.lua index d3cccb5298..d412af2950 100644 --- a/spec/System/TestTradeQueryCurrency_spec.lua +++ b/spec/System/TestTradeQueryCurrency_spec.lua @@ -1,5 +1,9 @@ describe("TradeQuery Currency Conversion", function() - local mock_tradeQuery = new("TradeQuery", { itemsTab = {} }) + local mock_tradeQuery + + before_each(function() + mock_tradeQuery = new("TradeQuery", { itemsTab = {} }) + end) -- test case for commit: "Skip callback on errors to prevent incomplete conversions" describe("FetchCurrencyConversionTable", function() @@ -40,15 +44,19 @@ describe("TradeQuery Currency Conversion", function() end) describe("PriceBuilderProcessPoENinjaResponse", function() - -- Pass: Processes without error, restoring map + -- Pass: Processes without error, restoring map while adding a notice -- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions it("handles unmapped currency", function() local orig_conv = mock_tradeQuery.currencyConversionTradeMap mock_tradeQuery.currencyConversionTradeMap = { div = "id" } - local resp = { exotic = 10 } + mock_tradeQuery.pbLeague = "league" + mock_tradeQuery.pbCurrencyConversion = { league = {} } + mock_tradeQuery.controls.pbNotice = { label = ""} + local resp = { exotic = 10 } mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp) -- No crash expected assert.is_true(true) + assert.is_true(mock_tradeQuery.controls.pbNotice.label == "No currencies received from PoE Ninja") mock_tradeQuery.currencyConversionTradeMap = orig_conv end) end) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 23b5a40164..84c5f487b3 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -1585,14 +1585,14 @@ function ItemsTabClass:DeleteItem(item, deferUndoState) end end -local function copyAnointsAndEldritchImplicits(newItem, activeItemSet, items) - local newItemType = newItem.base.type - if activeItemSet[newItemType] then - local currentItem = activeItemSet[newItemType].selItemId and items[activeItemSet[newItemType].selItemId] +function ItemsTabClass:CopyAnointsAndEldritchImplicits(newItem, copyEldritchImplicits, overwrite, sourceSlotName) + local newItemType = sourceSlotName or (newItem.base.weapon and "Weapon 1" or newItem.base.type) + if self.activeItemSet[newItemType] then + local currentItem = self.activeItemSet[newItemType].selItemId and self.items[self.activeItemSet[newItemType].selItemId] -- if you don't have an equipped item that matches the type of the newItem, no need to do anything if currentItem then -- if the new item is anointable and does not have an anoint and your current respective item does, apply that anoint to the new item - if isAnointable(newItem) and #newItem.enchantModLines == 0 and activeItemSet[newItemType].selItemId > 0 then + if isAnointable(newItem) and (#newItem.enchantModLines == 0 or overwrite) and self.activeItemSet[newItemType].selItemId > 0 then local currentAnoint = currentItem.enchantModLines if currentAnoint and #currentAnoint == 1 then -- skip if amulet has more than one anoint e.g. Stranglegasp newItem.enchantModLines = currentAnoint @@ -1607,12 +1607,20 @@ local function copyAnointsAndEldritchImplicits(newItem, activeItemSet, items) return end end - if main.migrateEldritchImplicits and isValueInTable(eldritchBaseTypes, newItem.base.type) and isValueInTable(eldritchRarities, newItem.rarity) - and #newItem.implicitModLines == 0 and not newItem.corrupted and (currentItem.cleansing or currentItem.tangle) and currentItem.implicitModLines then + + local modifiableItem = not (newItem.corrupted or newItem.mirrored) + if copyEldritchImplicits and isValueInTable(eldritchBaseTypes, newItem.base.type) and isValueInTable(eldritchRarities, newItem.rarity) + and (#newItem.implicitModLines == 0 or overwrite) and modifiableItem and (currentItem.cleansing or currentItem.tangle) and currentItem.implicitModLines then newItem.implicitModLines = currentItem.implicitModLines newItem.tangle = currentItem.tangle newItem.cleansing = currentItem.cleansing end + + -- harvest and heist enchantments on modifiable body armour or weapons + if (newItem.base.weapon or newItem.base.type == "Body Armour") and (#newItem.enchantModLines == 0 or overwrite) and self.activeItemSet[newItemType].selItemId > 0 and modifiableItem and currentItem.enchantModLines then + newItem.enchantModLines = currentItem.enchantModLines + end + newItem:BuildAndParseRaw() end end @@ -1622,7 +1630,7 @@ end function ItemsTabClass:CreateDisplayItemFromRaw(itemRaw, normalise) local newItem = new("Item", itemRaw) if newItem.base then - copyAnointsAndEldritchImplicits(newItem, self.activeItemSet, self.items) + self:CopyAnointsAndEldritchImplicits(newItem, main.migrateEldritchImplicits, false) if normalise then newItem:NormaliseQuality() newItem:BuildModList() diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index f102f193c3..793dd7822c 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -184,12 +184,17 @@ function TradeQueryClass:PriceBuilderProcessPoENinjaResponse(resp) if resp then -- Populate the chaos-converted values for each tradeId for currencyName, chaosEquivalent in pairs(resp) do + local currencyName = currencyName:lower() if self.currencyConversionTradeMap[currencyName] then self.pbCurrencyConversion[self.pbLeague][self.currencyConversionTradeMap[currencyName]] = chaosEquivalent else ConPrintf("Unhandled Currency Name: '"..currencyName.."'") end end + -- if nothing was actually found, we should add a notice + if next(self.pbCurrencyConversion[self.pbLeague]) == nil then + self:SetNotice(self.controls.pbNotice, "No currencies received from PoE Ninja") + end else self:SetNotice(self.controls.pbNotice, "PoE Ninja JSON Processing Error") end @@ -277,7 +282,23 @@ You can click this button to enter your POESESSID. - You can only generate weighted searches for public leagues. (Generated searches can be modified on trade site to work on other leagues and realms)]] --- Fetches Box + -- Buyout selection + self.tradeTypes = { + "Instant buyout", + "Instant buyout and in person", + "In person (online in league)", + "In person (online)", + "Any (includes offline)" + } + + self.controls.tradeTypeSelection = new("DropDownControl", { "TOPLEFT", self.controls.poesessidButton, "BOTTOMLEFT" }, + { 0, row_vertical_padding, 188, row_height }, self.tradeTypes, function(index, value) + self.tradeTypeIndex = index + end) + -- remember previous choice + self.controls.tradeTypeSelection:SetSel(self.tradeTypeIndex or 1) + + -- Fetches Box self.maxFetchPerSearchDefault = 2 self.controls.fetchCountEdit = new("EditControl", {"TOPRIGHT", nil, "TOPRIGHT"}, {-12, 19, 154, row_height}, "", "Fetch Pages", "%D", 3, function(buf) self.maxFetchPages = m_min(m_max(tonumber(buf) or self.maxFetchPerSearchDefault, 1), 10) @@ -338,20 +359,12 @@ on trade site to work on other leagues and realms)]] [[Weighted Sum searches will always sort using descending weighted sum Additional post filtering options can be done these include: Highest Stat Value - Sort from highest to lowest Stat Value change of equipping item -Highest Stat Value / Price - Sorts from highest to lowest Stat Value per currency +Highest Stat Value / Price - Sorts from highest to lowest by estimated Stat Value per currency Lowest Price - Sorts from lowest to highest price of retrieved items Highest Weight - Displays the order retrieved from trade]] - self.controls.itemSortSelection:SetSel(self.pbItemSortSelectionIndex) - self.controls.itemSortSelectionLabel = new("LabelControl", {"TOPRIGHT", self.controls.itemSortSelection, "TOPLEFT"}, {-4, 0, 60, 16}, "^7Sort By:") - - -- Use Enchant in DPS sorting - self.controls.enchantInSort = new("CheckBoxControl", {"TOPRIGHT",self.controls.fetchCountEdit,"TOPLEFT"}, {-8, 0, row_height}, "Include Enchants:", function(state) - self.enchantInSort = state - for row_idx, _ in pairs(self.resultTbl) do - self:UpdateControlsWithItems(row_idx) - end - end) - self.controls.enchantInSort.tooltipText = "This includes enchants in sorting that occurs after trade results have been retrieved" + -- avoid calling selFunc to avoid updating controls before they are initialised + self.controls.itemSortSelection:SetSel(self.pbItemSortSelectionIndex, true) + self.controls.itemSortSelectionLabel = new("LabelControl", {"TOPRIGHT", self.controls.itemSortSelection, "TOPLEFT"}, {-4, 0, 56, 16}, "^7Sort By:") -- Realm selection self.controls.realmLabel = new("LabelControl", {"LEFT", self.controls.setSelect, "RIGHT"}, {18, 0, 20, row_height - 4}, "^7Realm:") @@ -449,7 +462,7 @@ 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.poesessidButton, "LEFT"}, {0, 0, 0, 0}, "") + self.controls.sectionAnchor = new("LabelControl", {"LEFT", self.controls.tradeTypeSelection, "LEFT"}, {0, row_vertical_padding + row_height, 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 @@ -747,10 +760,7 @@ function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, ba table.sort(result.evaluation, function(a, b) return a.weight > b.weight end) else local item = new("Item", result.item_string) - if not self.enchantInSort then -- Calc item DPS without anoint or enchant as these can generally be added after. - item.enchantModLines = { } - item:BuildAndParseRaw() - end + local output = self:ReduceOutput(calcFunc({ repSlotName = slotName, repItem = item })) local weight = self.tradeQueryGenerator.WeightedRatioOutputs(baseOutput, output, self.statSortSelectionList) result.evaluation = {{ output = output, weight = weight }} @@ -759,6 +769,22 @@ function TradeQueryClass:GetResultEvaluation(row_idx, result_index, calcFunc, ba end -- Method to update controls after a search is completed +function TradeQueryClass:UpdateDropdownList(row_idx) + local dropdownLabels = {} + + 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) + end + self.controls["resultDropdown".. row_idx].selIndex = 1 + self.controls["resultDropdown".. row_idx]:SetList(dropdownLabels) +end function TradeQueryClass:UpdateControlsWithItems(row_idx) local sortMode = self.itemSortSelectionList[self.pbItemSortSelectionIndex] local sortedItems, errMsg = self:SortFetchResults(row_idx, sortMode) @@ -782,14 +808,7 @@ function TradeQueryClass:UpdateControlsWithItems(row_idx) amount = self.resultTbl[row_idx][pb_index].amount, } self.controls.fullPrice.label = "Total Price: " .. self:GetTotalPriceString() - local dropdownLabels = {} - for result_index = 1, #self.resultTbl[row_idx] do - local pb_index = self.sortedResultTbl[row_idx][result_index].index - local item = new("Item", self.resultTbl[row_idx][pb_index].item_string) - table.insert(dropdownLabels, colorCodes[item.rarity]..item.name) - end - self.controls["resultDropdown".. row_idx].selIndex = 1 - self.controls["resultDropdown".. row_idx]:SetList(dropdownLabels) + self:UpdateDropdownList(row_idx) end -- Method to set the current result return in the pane based of an index @@ -838,6 +857,7 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) 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 }) end table.sort(newTbl, function(a,b) return a.outputAttr > b.outputAttr end) @@ -847,7 +867,20 @@ function TradeQueryClass:SortFetchResults(row_idx, mode) return nil, "MissingConversionRates" end for result_index = 1, #self.resultTbl[row_idx] do - t_insert(newTbl, { outputAttr = getResultWeight(result_index) / priceTable[result_index], index = result_index }) + -- generally, because we are filtering our results to only the top + -- contenders, we will end up with a small spread of result weights. + -- this is however not true for prices as *decent* items might start + -- at a couple of div while perfect items are worth hundreds of + -- divs. I think the best option here is weight - k * log10(price) + -- to prioritise good items while only slightly punishing high + -- prices. another option would be weight / log10(price), but it + -- still seems to overrate very cheap items that are bad + + -- 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 }) end table.sort(newTbl, function(a,b) return a.outputAttr > b.outputAttr end) elseif mode == self.sortModes.Price then @@ -927,6 +960,7 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro return end context.controls["priceButton"..context.row_idx].label = "Searching..." + self.lastQuery = query self.tradeQueryRequests:SearchWithQueryWeightAdjusted(self.pbRealm, self.pbLeague, query, function(items, errMsg) if errMsg then @@ -936,6 +970,25 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro else self:SetNotice(context.controls.pbNotice, "") end + + -- replace eldritch mods or enchants if the user requested + -- so in TradeQueryGenerator + if self.tradeQueryGenerator.lastCopyEldritch or + self.tradeQueryGenerator.lastCopyEnchantMode == "Copy Current" then + for i, _ in ipairs(items) do + local item = new("Item", items[i].item_string) + self.itemsTab:CopyAnointsAndEldritchImplicits(item, true, true, context.slotTbl.slotName) + items[i].item_string = item:BuildRaw() + end + elseif self.tradeQueryGenerator.lastCopyEnchantMode == "Remove" then + for i, _ in ipairs(items) do + local item = new("Item", items[i].item_string) + item.enchantModLines = {} + items[i].item_string = item:BuildRaw() + end + end + + self.resultTbl[context.row_idx] = items self:UpdateControlsWithItems(context.row_idx) context.controls["priceButton"..context.row_idx].label = "Price Item" @@ -983,11 +1036,12 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro controls["priceButton"..row_idx] = new("ButtonControl", { "TOPLEFT", controls["uri"..row_idx], "TOPRIGHT"}, {8, 0, 100, row_height}, "Price Item", function() controls["priceButton"..row_idx].label = "Searching..." - self.tradeQueryRequests:SearchWithURL(controls["uri"..row_idx].buf, function(items, errMsg) + self.tradeQueryRequests:SearchWithURL(controls["uri"..row_idx].buf, function(items, errMsg, query) if errMsg then self:SetNotice(controls.pbNotice, "Error: " .. errMsg) else self:SetNotice(controls.pbNotice, "") + self.lastQuery = query self.resultTbl[row_idx] = items self:UpdateControlsWithItems(row_idx) end @@ -1019,15 +1073,11 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro self.controls.fullPrice.label = "Total Price: " .. self:GetTotalPriceString() end) controls["changeButton"..row_idx].shown = function() return self.resultTbl[row_idx] end - local dropdownLabels = {} - for _, sortedResult in ipairs(self.sortedResultTbl[row_idx] or {}) do - local item = new("Item", self.resultTbl[row_idx][sortedResult.index].item_string) - table.insert(dropdownLabels, colorCodes[item.rarity]..item.name) - end - controls["resultDropdown"..row_idx] = new("DropDownControl", { "TOPLEFT", controls["changeButton"..row_idx], "TOPRIGHT"}, {8, 0, 325, row_height}, dropdownLabels, function(index) + 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]) end) + self:UpdateDropdownList(row_idx) local function addMegalomaniacCompareToTooltipIfApplicable(tooltip, result_index) if slotTbl.slotName ~= "Megalomaniac" then return @@ -1103,23 +1153,58 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro return self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].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, 185, row_height}, function() - return self.totalPrice[row_idx] and "Whisper for " .. self.totalPrice[row_idx].amount .. " " .. self.totalPrice[row_idx].currency or "Whisper" - end, function() - Copy(self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].whisper) - end) - controls["whisperButton"..row_idx].enabled = function() - return self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].whisper ~= nil - end - controls["whisperButton"..row_idx].tooltipFunc = function(tooltip) + 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]] + + if not itemResult then return "" end + + local price = self.totalPrice[row_idx] and + self.totalPrice[row_idx].amount .. " " .. self.totalPrice[row_idx].currency + + if itemResult.whisper then + return price and "Whisper for " .. price or "Whisper" + else + return price and "Search for " .. price or "Search" + end + + end, function() + local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + if itemResult.whisper then + Copy(itemResult.whisper) + else + local exactQuery = dkjson.decode(self.lastQuery) + -- use trade sum to get the specific item. both min and max + -- weight on site uses floats but only shows integer in the api + -- e.g. weight of 172.3 shows up as 172 in the api + exactQuery.query.stats[1].value = { min = floor(itemResult.weight, 1) - 1, max = round(itemResult.weight, 1) + 1 } + -- also apply trader name. this should make false positives + -- extremely unlikely. this doesn't seem to take up a filter slot + exactQuery.query.filters = exactQuery.query.filters or { } + exactQuery.query.filters.trade_filters = exactQuery.query.filters.trade_filters or { filters = { } } + exactQuery.query.filters.trade_filters.filters = exactQuery.query.filters.trade_filters.filters or { } + exactQuery.query.filters.trade_filters.filters.account = { input = itemResult.trader } + + local exactQueryStr = dkjson.encode(exactQuery) + + self.tradeQueryRequests:SearchWithQuery(self.pbRealm, self.pbLeague, exactQueryStr, function(_, _) + end, {callbackQueryId = function(queryId) + local url = self.hostName.."trade/search/"..self.pbLeague.."/"..queryId + Copy(url) + OpenURL(url) + end}) + end + end) + + controls["whisperButton" .. row_idx].tooltipFunc = function(tooltip) tooltip:Clear() - if self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]].item_string then - tooltip.center = true - tooltip:AddLine(16, "Copies the item purchase whisper to the clipboard") - end + tooltip.center = true + local itemResult = self.itemIndexTbl[row_idx] and self.resultTbl[row_idx][self.itemIndexTbl[row_idx]] + 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) end end - -- Method to update the Total Price string sum of all items function TradeQueryClass:GetTotalPriceString() local text = "" diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index f171a104f2..2e21816092 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -924,13 +924,36 @@ function TradeQueryGeneratorClass:ExecuteQuery() if self.calcContext.options.includeScourge then self:GenerateModWeights(self.modData["Scourge"]) end - if self.calcContext.options.includeEldritch then - self:GenerateModWeights(self.modData["Eater"]) - self:GenerateModWeights(self.modData["Exarch"]) - end - if self.calcContext.options.includeSynthesis then - self:GenerateModWeights(self.modData["Synthesis"]) + if self.calcContext.options.includeEldritch ~= "None" and + -- skip weights if we need an influenced item as they can produce really + -- bad results due to the filter limit + self.calcContext.options.influence1 == 1 and + self.calcContext.options.influence2 == 1 then + local omitConditional = self.calcContext.options.includeEldritch == "Omit While" + local eaterMods = self.modData["Eater"] + local exarchMods = self.modData["Exarch"] + if omitConditional then + local function filterMods(mods) + local filtered = {} + for name, mod in pairs(mods) do + -- the user might want to skip these because they're generally + -- not used much, but there are a lot of them and the higher + -- power causes them to take up a lot of filter slots + if not name:match(".*PinnaclePresence$") and not name:match(".*UniquePresence$") then + filtered[name] = mod + end + end + return filtered + end + eaterMods = filterMods(self.modData["Eater"]) + exarchMods = filterMods(self.modData["Exarch"]) + end + self:GenerateModWeights(eaterMods) + self:GenerateModWeights(exarchMods) end + -- if self.calcContext.options.includeSynthesis then + -- self:GenerateModWeights(self.modData["Synthesis"]) + -- end end function TradeQueryGeneratorClass:addMoreWEMods() @@ -1003,7 +1026,16 @@ function TradeQueryGeneratorClass:FinishQuery() -- This Stat diff value will generally be higher than the weighted sum of the same item, because the stats are all applied at once and can thus multiply off each other. -- So apply a modifier to get a reasonable min and hopefully approximate that the query will start out with small upgrades. local minWeight = megalomaniacSpecialMinWeight or currentStatDiff * 0.5 - + + -- what the trade site API uses for the above + self.tradeTypes = { + "securable", + "available", + "onlineleague", + "online", + "any", + } + local selectedTradeType = self.tradeTypes[self.tradeTypeIndex] -- Generate trade query str and open in browser local filters = 0 local queryTable = { @@ -1016,7 +1048,7 @@ function TradeQueryGeneratorClass:FinishQuery() } } }, - status = { option = "available" }, + status = { option = selectedTradeType }, stats = { { type = "weight", @@ -1104,6 +1136,10 @@ function TradeQueryGeneratorClass:FinishQuery() } end + if options.account then + queryTable.query.filters.trade_filters.filters.account = {input = options.account} + end + if options.maxLevel and options.maxLevel > 0 then queryTable.query.filters.req_filters = { disabled = false, @@ -1170,9 +1206,11 @@ function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callb local isJewelSlot = slot and slot.slotName:find("Jewel") ~= nil local isAbyssalJewelSlot = slot and slot.slotName:find("Abyssal") ~= nil local isAmuletSlot = slot and slot.slotName == "Amulet" + local isBeltSlot = slot and slot.slotName == "Belt" + local isWeaponSlot = slot and (slot.slotName == "Weapon 1" or slot.slotName == "Weapon 2") local isEldritchModSlot = slot and eldritchModSlots[slot.slotName] == true - controls.includeCorrupted = new("CheckBoxControl", {"TOP",nil,"TOP"}, {-40, 30, 18}, "Corrupted Mods:", function(state) end) + controls.includeCorrupted = new("CheckBoxControl", {"TOP",nil,"TOP"}, {-40, 30, 18}, "Corrupted Mods:", function(state) end, "Includes corruption implicit modifiers in the weighted sum.\nNote that there is a maximum search filter count which means this might cause other weights to not be included.") controls.includeCorrupted.state = not context.slotTbl.alreadyCorrupted and (self.lastIncludeCorrupted == nil or self.lastIncludeCorrupted == true) controls.includeCorrupted.enabled = not context.slotTbl.alreadyCorrupted @@ -1181,7 +1219,7 @@ function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callb --controls.includeSynthesis.state = (self.lastIncludeSynthesis == nil or self.lastIncludeSynthesis == true) local lastItemAnchor = controls.includeCorrupted - local includeScourge = self.queryTab.pbLeagueRealName == "Standard" or self.queryTab.pbLeagueRealName == "Hardcore" + local includeScourge = self.queryTab.pbLeague == "Standard" or self.queryTab.pbLeague == "Hardcore" local function updateLastAnchor(anchor, height) lastItemAnchor = anchor @@ -1194,7 +1232,7 @@ function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callb -- these unique items cannot be mirrored if not context.slotTbl.unique then - controls.includeMirrored = new("CheckBoxControl", {"TOPRIGHT",lastItemAnchor,"BOTTOMRIGHT"}, {0, 5, 18}, "Mirrored items:", function(state) end) + controls.includeMirrored = new("CheckBoxControl", {"TOPRIGHT",lastItemAnchor,"BOTTOMRIGHT"}, {0, 5, 18}, "Mirrored Items:", function(state) end) controls.includeMirrored.state = (self.lastIncludeMirrored == nil or self.lastIncludeMirrored == true) updateLastAnchor(controls.includeMirrored) end @@ -1211,25 +1249,71 @@ function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callb updateLastAnchor(controls.includeTalisman) end + -- Implicit mod and enchant behaviour in searching and sorting if isEldritchModSlot then - controls.includeEldritch = new("CheckBoxControl", {"TOPRIGHT",lastItemAnchor,"BOTTOMRIGHT"}, {0, 5, 18}, "Eldritch Mods:", function(state) end) - controls.includeEldritch.state = (self.lastIncludeEldritch == true) + local eldritchTooltip = [[Controls the inclusion of eldritch mod weights in the weighted sum. +None: no weights are generated. +All: weights are generated for all eldritch implicit modifiers. +Omit while: weights are generated, but conditional "While unique/atlas boss" modifiers are skipped. +It is often not recommended to use "All" as this includes a lot of high power modifiers, +which will cause other useful modifiers to be left out in the weighted sum.]] + controls.includeEldritch = new("DropDownControl", { "TOPLEFT", lastItemAnchor, "BOTTOMLEFT" }, { 0, 5, 92, 18 }, + { "None", "All", "Omit While" }, function(_state) end, eldritchTooltip) + controls.includeEldritchLabel = new("LabelControl", { "RIGHT", controls.includeEldritch, "LEFT" }, + { -4, 0, 80, 16 }, "Eldritch Mods:") + controls.includeEldritch:SetSel(self.lastIncludeEldritch or 1) updateLastAnchor(controls.includeEldritch) - end + local eldritchTooltip = "Replaces the eldritch modifiers on search results with the eldritch modifiers from your currently equipped item." + local labelText = "Copy Current Implicits:" + controls.copyEldritch = new("CheckBoxControl", + { "TOPLEFT", lastItemAnchor, "BOTTOMLEFT" }, + { 0, 5, 18, 18 }, + labelText, function(state) end, eldritchTooltip, false) + controls.copyEldritch.state = self.lastCopyEldritch or false + updateLastAnchor(controls.copyEldritch) + end + if isAmuletSlot or isBeltSlot or isWeaponSlot then + local term = isWeaponSlot and "enchants" or "anoints" + local enchantTooltip = s_format([[Keep: %s will be unchanged on the search results. +Copy Current: current %s will be applied to the search result items. +Remove: %s will be removed from the search results.]], term, term, term) + local copyEnchantList = { "Keep", "Copy Current", "Remove" } + controls.copyEnchantMode = new("DropDownControl", + { "TOPLEFT", lastItemAnchor, "BOTTOMLEFT" }, + { 0, 5, 120, 18 }, + copyEnchantList, function(state) end, enchantTooltip) + controls.copyEnchantMode.state = self.lastCopyEnchantMode or false + local labelText = isWeaponSlot and "^7Enchant Behaviour:" or "^7Anoint Behaviour:" + controls.copyEnchantModeLabel = new("LabelControl", { "RIGHT", controls.copyEnchantMode, "LEFT" }, + { -4, 0, 80, 16 }, labelText) + updateLastAnchor(controls.copyEnchantMode) + end if isJewelSlot and context.slotTbl.slotName ~= "Watcher's Eye" then controls.jewelType = new("DropDownControl", {"TOPLEFT",lastItemAnchor,"BOTTOMLEFT"}, {0, 5, 100, 18}, { "Any", "Base", "Abyss" }, function(index, value) end) controls.jewelType.selIndex = self.lastJewelType or 1 controls.jewelTypeLabel = new("LabelControl", {"RIGHT",controls.jewelType,"LEFT"}, {-5, 0, 0, 16}, "Jewel Type:") updateLastAnchor(controls.jewelType) elseif slot and not isAbyssalJewelSlot and context.slotTbl.slotName ~= "Watcher's Eye" then - controls.influence1 = new("DropDownControl", {"TOPLEFT",lastItemAnchor,"BOTTOMLEFT"}, {0, 5, 100, 18}, influenceDropdownNames, function(index, value) end) - controls.influence1.selIndex = self.lastInfluence1 or 1 - controls.influence1Label = new("LabelControl", {"RIGHT",controls.influence1,"LEFT"}, {-5, 0, 0, 16}, "Influence 1:") - - controls.influence2 = new("DropDownControl", {"TOPLEFT",controls.influence1,"BOTTOMLEFT"}, {0, 5, 100, 18}, influenceDropdownNames, function(index, value) end) - controls.influence2.selIndex = self.lastInfluence2 or 1 - controls.influence2Label = new("LabelControl", {"RIGHT",controls.influence2,"LEFT"}, {-5, 0, 0, 16}, "Influence 2:") + local selFunc = function() + -- influenced items can't have eldritch implicits + if controls.copyEldritch and isEldritchModSlot then + local hasInfluence1 = controls.influence1 and controls.influence1:GetSelValue() ~= "None" + local hasInfluence2 = controls.influence2 and controls.influence2:GetSelValue() ~= "None" + controls.copyEldritch.enabled = not hasInfluence1 and not hasInfluence2 + end + end + controls.influence1 = new("DropDownControl", { "TOPLEFT", lastItemAnchor, "BOTTOMLEFT" }, { 0, 5, 100, 18 }, + influenceDropdownNames, selFunc) + controls.influence1:SetSel(self.lastInfluence1 or 1) + controls.influence1Label = new("LabelControl", {"RIGHT",controls.influence1,"LEFT"}, {-5, 0, 0, 16}, "^7Influence 1:") + + controls.influence2 = new("DropDownControl", { "TOPLEFT", controls.influence1, "BOTTOMLEFT" }, { 0, 5, 100, 18 }, + influenceDropdownNames, selFunc) + controls.influence2:SetSel(self.lastInfluence2 or 1) + selFunc() + controls.influence2Label = new("LabelControl", { "RIGHT", controls.influence2, "LEFT" }, { -5, 0, 0, 16 }, + "^7Influence 2:") updateLastAnchor(controls.influence2, 46) elseif isAbyssalJewelSlot then controls.jewelType = new("DropDownControl", {"TOPLEFT",lastItemAnchor,"BOTTOMLEFT"}, {0, 5, 100, 18}, { "Abyss" }, nil) @@ -1237,7 +1321,6 @@ function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callb controls.jewelTypeLabel = new("LabelControl", {"RIGHT",controls.jewelType,"LEFT"}, {-5, 0, 0, 16}, "Jewel Type:") updateLastAnchor(controls.jewelType) end - -- Add max price limit selection dropbox local currencyDropdownNames = { } for _, currency in ipairs(currencyTable) do @@ -1259,12 +1342,12 @@ function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callb 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") controls.sockets.buf = self.lastSockets and tostring(self.lastSockets) or "" - controls.socketsLabel = new("LabelControl", {"RIGHT",controls.sockets,"LEFT"}, {-5, 0, 0, 16}, "# of Empty Sockets:") + controls.socketsLabel = new("LabelControl", {"RIGHT",controls.sockets,"LEFT"}, {-5, 0, 0, 16}, "^7# of Empty Sockets:") updateLastAnchor(controls.sockets) if not slot.slotName:find("Belt") and not slot.slotName:find("Ring") and not slot.slotName:find("Amulet") then controls.links = new("EditControl", {"TOPLEFT",lastItemAnchor,"BOTTOMLEFT"}, {0, 5, 70, 18}, nil, nil, "%D") - controls.linksLabel = new("LabelControl", {"RIGHT",controls.links,"LEFT"}, {-5, 0, 0, 16}, "# of Links:") + controls.linksLabel = new("LabelControl", {"RIGHT",controls.links,"LEFT"}, {-5, 0, 0, 16}, "^7# of Links:") updateLastAnchor(controls.links) end end @@ -1302,17 +1385,23 @@ function TradeQueryGeneratorClass:RequestQuery(slot, context, statWeights, callb controls.generateQuery = new("ButtonControl", { "BOTTOM", nil, "BOTTOM" }, {-45, -10, 80, 20}, "Execute", function() main:ClosePopup() + self.tradeTypeIndex = context.controls.tradeTypeSelection.selIndex + + self.lastCopyEldritch = controls.copyEldritch and controls.copyEldritch.state + self.lastCopyEnchantMode = controls.copyEnchantMode and controls.copyEnchantMode:GetSelValue() + if controls.includeMirrored then self.lastIncludeMirrored, options.includeMirrored = controls.includeMirrored.state, controls.includeMirrored.state end if controls.includeCorrupted then self.lastIncludeCorrupted, options.includeCorrupted = controls.includeCorrupted.state, controls.includeCorrupted.state end - if controls.includeSynthesis then - self.lastIncludeSynthesis, options.includeSynthesis = controls.includeSynthesis.state, controls.includeSynthesis.state - end + -- if controls.includeSynthesis then + -- self.lastIncludeSynthesis, options.includeSynthesis = controls.includeSynthesis.state, controls.includeSynthesis.state + -- end if controls.includeEldritch then - self.lastIncludeEldritch, options.includeEldritch = controls.includeEldritch.state, controls.includeEldritch.state + self.lastIncludeEldritch, options.includeEldritch = controls.includeEldritch.selIndex, + controls.includeEldritch:GetSelValue() end if controls.includeScourge then self.lastIncludeScourge, options.includeScourge = controls.includeScourge.state, controls.includeScourge.state diff --git a/src/Classes/TradeQueryRequests.lua b/src/Classes/TradeQueryRequests.lua index e656fb5657..95f8b6a5ad 100644 --- a/src/Classes/TradeQueryRequests.lua +++ b/src/Classes/TradeQueryRequests.lua @@ -285,8 +285,14 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) table.insert(items, { amount = trade_entry.listing.price.amount, currency = trade_entry.listing.price.currency, + -- note: using these to travel to the hideout or for a + -- direct whisper is not allowed, even if they are provided + -- right here + -- hideout_token = trade_entry.listing.hideout_token, + -- whisper_token = trade_entry.listing.whisper_token, item_string = common.base64.decode(trade_entry.item.extended.text), whisper = trade_entry.listing.whisper, + trader = trade_entry.listing.account.name, weight = trade_entry.item.pseudoMods and trade_entry.item.pseudoMods[1]:match("Sum: (.+)") or "0", id = trade_entry.id }) @@ -296,7 +302,7 @@ function TradeQueryRequestsClass:FetchResultBlock(url, callback) }) end ----@param callback fun(items:table, errMsg:string) +---@param callback fun(items:table, errMsg:string, query:string) function TradeQueryRequestsClass:SearchWithURL(url, callback) local subpath = url:match(self.hostName .. "trade/search/(.+)$") local paths = {} @@ -304,7 +310,7 @@ function TradeQueryRequestsClass:SearchWithURL(url, callback) table.insert(paths, path) end if #paths < 2 or #paths > 3 then - return callback(nil, "Invalid URL") + return callback(nil, "Invalid URL", nil) end local realm, league, queryId if #paths == 3 then @@ -314,9 +320,11 @@ function TradeQueryRequestsClass:SearchWithURL(url, callback) queryId = paths[#paths] self:FetchSearchQueryHTML(realm, league, queryId, function(query, errMsg) if errMsg then - return callback(nil, errMsg) + return callback(nil, errMsg, nil) end - self:SearchWithQuery(realm, league, query, callback) + self:SearchWithQuery(realm, league, query, function(items, searchErrMsg) + callback(items, searchErrMsg, query) + end) end) end diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index a856fd179d..d32323cfca 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -181,7 +181,8 @@ local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build) end, nil, nil, true) self.controls.treeSearch.tooltipText = "Uses Lua pattern matching for complex searches.\nPrefix your search with \"oil:\" to search by anoint recipe.\nTo search for multiple terms: (increased.fire.damage|increased.area.of.effect|etc)" - self.tradeLeaguesList = { } + -- table holding all realm/league pairs. (allLeagues[realm] = [league.id,...]) + self.tradeLeaguesList = {} -- Find Timeless Jewel Button self.controls.findTimelessJewel = new("ButtonControl", { "LEFT", self.controls.treeSearch, "RIGHT" }, { 8, 0, 150, 20 }, "Find Timeless Jewel", function() self:FindTimelessJewel() @@ -1496,8 +1497,12 @@ function TreeTabClass:FindTimelessJewel() end) controls.devotionSelect2.selIndex = timelessData.devotionVariant2 - controls.jewelSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 25, 0, 16}, "^7Jewel Type:") - controls.jewelSelect = new("DropDownControl", {"LEFT", controls.jewelSelectLabel, "RIGHT"}, {10, 0, 200, 18}, jewelTypes, function(index, value) + local rowSpacing = 6 + local rowHeight = 17 + local labelHeight = 16 + local labelSpacing = 4 + + controls.jewelSelect = new("DropDownControl", {"TOPLEFT", nil, "TOPLEFT"}, {380, 25, 200, rowHeight}, jewelTypes, function(index, value) timelessData.jewelType = value controls.devotionSelectLabel.shown = value.id == 4 -- Militant Faith controls.protectAllocatedLabel.shown = (value.id == 4 and controls.socketFilter.state) @@ -1510,13 +1515,15 @@ function TreeTabClass:FindTimelessJewel() updateSearchList("", true) end) controls.jewelSelect.selIndex = timelessData.jewelType.id + controls.jewelSelectLabel = new("LabelControl", {"RIGHT", controls.jewelSelect, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Jewel Type:") + - controls.conquerorSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 50, 0, 16}, "^7Conqueror:") - controls.conquerorSelect = new("DropDownControl", {"LEFT", controls.conquerorSelectLabel, "RIGHT"}, {10, 0, 200, 18}, conquerorTypes[timelessData.jewelType.id], function(index, value) + controls.conquerorSelect = new("DropDownControl", {"TOPLEFT", controls.jewelSelect, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, conquerorTypes[timelessData.jewelType.id], function(index, value) timelessData.conquerorType = value self.build.modFlag = true end) controls.conquerorSelect.selIndex = timelessData.conquerorType.id + controls.conquerorSelectLabel = new("LabelControl", {"RIGHT", controls.conquerorSelect, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Conqueror:") local allocatedNodes = { } local protectedNodes = { } @@ -1540,8 +1547,8 @@ function TreeTabClass:FindTimelessJewel() self.allocatedNodesInRadiusCount = #nodeNames end - controls.socketSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 75, 0, 16}, "^7Jewel Socket:") - controls.socketSelect = new("TimelessJewelSocketControl", {"LEFT", controls.socketSelectLabel, "RIGHT"}, {10, 0, 200, 18}, jewelSockets, function(index, value) + + controls.socketSelect = new("TimelessJewelSocketControl", {"TOPLEFT", controls.conquerorSelect, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, jewelSockets, function(index, value) timelessData.jewelSocket = value setAllocatedNodes() -- reset list when changing sockets self.build.modFlag = true @@ -1553,6 +1560,7 @@ function TreeTabClass:FindTimelessJewel() break end end + controls.socketSelectLabel = new("LabelControl", {"RIGHT", controls.socketSelect, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Jewel Socket:") local function clearProtected() -- clear all controls, nodes related to Militant Faith filtering protectedNodesCount = 0 @@ -1563,8 +1571,8 @@ function TreeTabClass:FindTimelessJewel() end end end - controls.socketFilterLabel = new("LabelControl", { "TOPRIGHT", nil, "TOPLEFT" }, { 405, 100, 0, 16 }, "^7Filter Nodes:") - controls.socketFilter = new("CheckBoxControl", { "LEFT", controls.socketFilterLabel, "RIGHT" }, { 10, 0, 18 }, nil, function(value) + + controls.socketFilter = new("CheckBoxControl", {"TOPLEFT", controls.socketSelect, "BOTTOMLEFT"}, {0, rowSpacing, rowHeight}, nil, function(value) timelessData.socketFilter = value self.build.modFlag = true controls.socketFilterAdditionalDistanceLabel.shown = value @@ -1578,6 +1586,7 @@ function TreeTabClass:FindTimelessJewel() clearProtected() end end) + controls.socketFilterLabel = new("LabelControl", {"RIGHT", controls.socketFilter, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Filter Nodes:") controls.socketFilter.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() tooltip:AddLine(16, "^7Enable this option to exclude nodes that you do not have allocated on your active passive skill tree.") @@ -1642,11 +1651,11 @@ function TreeTabClass:FindTimelessJewel() local scrollWheelSpeedTbl2 = { ["SHIFT"] = 0.2, ["CTRL"] = 0.002, ["DEFAULT"] = 0.02 } local nodeSliderStatLabel = "None" - controls.nodeSliderLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 125, 0, 16}, "^7Primary Node Weight:") - controls.nodeSlider = new("SliderControl", {"LEFT", controls.nodeSliderLabel, "RIGHT"}, {10, 0, 200, 16}, function(value) + controls.nodeSlider = new("SliderControl", {"TOPLEFT", controls.socketFilter, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, function(value) controls.nodeSliderValue.label = s_format("^7%.3f", value * 10) parseSearchList(1, controls.searchListFallback and controls.searchListFallback.shown or false) end, scrollWheelSpeedTbl) + controls.nodeSliderLabel = new("LabelControl", {"RIGHT", controls.nodeSlider, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Primary Node Weight:") controls.nodeSlider.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() if not controls.nodeSlider.dragging then @@ -1671,11 +1680,11 @@ function TreeTabClass:FindTimelessJewel() controls.nodeSlider:SetVal(0.1) local nodeSlider2StatLabel = "None" - controls.nodeSlider2Label = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 150, 0, 16}, "^7Secondary Node Weight:") - controls.nodeSlider2 = new("SliderControl", {"LEFT", controls.nodeSlider2Label, "RIGHT"}, {10, 0, 200, 16}, function(value) + controls.nodeSlider2 = new("SliderControl", {"TOPLEFT", controls.nodeSlider, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, function(value) controls.nodeSlider2Value.label = s_format("^7%.3f", value * 10) parseSearchList(1, controls.searchListFallback and controls.searchListFallback.shown or false) end, scrollWheelSpeedTbl) + controls.nodeSlider2Label = new("LabelControl", {"RIGHT", controls.nodeSlider2, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Secondary Node Weight:") controls.nodeSlider2.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() if not controls.nodeSlider2.dragging then @@ -1699,8 +1708,7 @@ function TreeTabClass:FindTimelessJewel() end controls.nodeSlider2:SetVal(0.1) - controls.nodeSlider3Label = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 175, 0, 16}, "^7Minimum Node Weight:") - controls.nodeSlider3 = new("SliderControl", {"LEFT", controls.nodeSlider3Label, "RIGHT"}, {10, 0, 200, 16}, function(value) + controls.nodeSlider3 = new("SliderControl", {"TOPLEFT", controls.nodeSlider2, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, function(value) if value == 1 then controls.nodeSlider3Value.label = "^7Required" else @@ -1708,6 +1716,7 @@ function TreeTabClass:FindTimelessJewel() end parseSearchList(1, controls.searchListFallback and controls.searchListFallback.shown or false) end, scrollWheelSpeedTbl2) + controls.nodeSlider3Label = new("LabelControl", {"RIGHT", controls.nodeSlider3, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Minimum Node Weight:") controls.nodeSlider3.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() if not controls.nodeSlider3.dragging then @@ -1753,8 +1762,7 @@ function TreeTabClass:FindTimelessJewel() end buildMods() - controls.nodeSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 200, 0, 16}, "^7Search for Node:") - controls.nodeSelect = new("DropDownControl", {"LEFT", controls.nodeSelectLabel, "RIGHT"}, {10, 0, 200, 18}, modData, function(index, value) + controls.nodeSelect = new("DropDownControl", {"TOPLEFT", controls.nodeSlider3, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, modData, function(index, value) nodeSliderStatLabel = "None" nodeSlider2StatLabel = "None" if value.id then @@ -1813,6 +1821,7 @@ function TreeTabClass:FindTimelessJewel() self.build.modFlag = true end end) + controls.nodeSelectLabel = new("LabelControl", {"RIGHT", controls.nodeSelect, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Search for Node:") controls.nodeSelect.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() if mode ~= "OUT" and value.descriptions then @@ -1987,7 +1996,6 @@ function TreeTabClass:FindTimelessJewel() updateSearchList(newList, true) end - controls.fallbackWeightsLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 225, 0, 16}, "^7Fallback Weight Mode:") local fallbackWeightsList = { } for id, stat in pairs(data.powerStatList) do if not stat.ignoreForItems and stat.label ~= "Name" then @@ -1998,9 +2006,10 @@ function TreeTabClass:FindTimelessJewel() }) end end - controls.fallbackWeightsList = new("DropDownControl", {"LEFT", controls.fallbackWeightsLabel, "RIGHT"}, {10, 0, 200, 18}, fallbackWeightsList, function(index) + controls.fallbackWeightsList = new("DropDownControl", {"TOPLEFT", controls.nodeSelect, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, fallbackWeightsList, function(index) timelessData.fallbackWeightMode.idx = index end) + controls.fallbackWeightsLabel = new("LabelControl", {"RIGHT", controls.fallbackWeightsList, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Fallback Weight Mode:") controls.fallbackWeightsList.selIndex = timelessData.fallbackWeightMode.idx or 1 controls.fallbackWeightsButton = new("ButtonControl", {"LEFT", controls.fallbackWeightsList, "RIGHT"}, {5, 0, 66, 18}, "Generate", function() setupFallbackWeights() @@ -2010,20 +2019,48 @@ function TreeTabClass:FindTimelessJewel() tooltip:Clear() tooltip:AddLine(16, "^7Click this button to generate new fallback node weights, replacing your old ones.") end - controls.totalMinimumWeightLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 250, 0, 16}, "^7Total Minimum Weight:") - controls.totalMinimumWeight = new("EditControl", {"LEFT", controls.totalMinimumWeightLabel, "RIGHT"}, {10, 0, 60, 18}, "", nil, "%D", nil, function(val) + controls.totalMinimumWeight = new("EditControl", {"TOPLEFT", controls.fallbackWeightsList, "BOTTOMLEFT"}, {0, rowSpacing, 200, rowHeight}, "", nil, "%D", nil, function(val) local num = tonumber(val) timelessData.totalMinimumWeight = num or nil self.build.modFlag = true end) + controls.totalMinimumWeightLabel = new("LabelControl", {"RIGHT", controls.totalMinimumWeight, "LEFT"}, {-labelSpacing, 0, 0, labelHeight}, "^7Total Minimum Weight:") controls.totalMinimumWeight.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() tooltip:AddLine(16, "^7Optional: Only show results where total weight meets or exceeds this value.") end + local listWidth = 440 + local listHeight = 200 + local buttonHeight = 20 + local edgePadding = 12 + local listYOffset = -(buttonHeight + edgePadding * 2) + controls.searchList = new("EditControl", { "BOTTOMLEFT", nil, "BOTTOMLEFT" }, + { edgePadding, listYOffset, listWidth, listHeight }, timelessData.searchList, nil, + "^%C\t\n", nil, function(value) + timelessData.searchList = value + parseSearchList(0, false) + self.build.modFlag = true + end, 16, true) + controls.searchList.shown = true + controls.searchList.enabled = true + controls.searchList:SetText(timelessData.searchList and timelessData.searchList or "") - controls.searchListButton = new("ButtonControl", {"TOPLEFT", nil, "TOPLEFT"}, {12, 250, 106, 20}, "^7Desired Nodes", function() - if controls.searchListFallback.shown then + controls.searchListFallback = new("EditControl", { "BOTTOMLEFT", nil, "BOTTOMLEFT" }, + { edgePadding, listYOffset, listWidth, listHeight }, + timelessData.searchListFallback, nil, "^%C\t\n", nil, function(value) + timelessData.searchListFallback = value + parseSearchList(0, true) + self.build.modFlag = true + end, 16, true) + controls.searchListFallback.shown = false + controls.searchListFallback.enabled = false + controls.searchListFallback:SetText(timelessData.searchListFallback and timelessData.searchListFallback or "") + + controls.searchListButton = new("ButtonControl", + { "BOTTOMLEFT", nil, "BOTTOMLEFT" }, + { edgePadding, listYOffset - listHeight - rowSpacing, 106, buttonHeight }, "^7Desired Nodes", function() + if controls.searchListFallback.shown then controls.searchListFallback.shown = false controls.searchListFallback.enabled = false controls.searchList.shown = true @@ -2032,11 +2069,14 @@ function TreeTabClass:FindTimelessJewel() end) controls.searchListButton.tooltipFunc = function(tooltip, mode, index, value) tooltip:Clear() - tooltip:AddLine(16, "^7This contains a list of your desired nodes along with their primary, secondary, and minimum weights.") - tooltip:AddLine(16, "^7This list can be updated manually or by selecting the node you want to update via the search dropdown list and then moving the node weight sliders.") + tooltip:AddLine(16, + "^7This contains a list of your desired nodes along with their primary, secondary, and minimum weights.") + tooltip:AddLine(16, + "^7This list can be updated manually or by selecting the node you want to update via the search dropdown list and then moving the node weight sliders.") end controls.searchListButton.locked = function() return controls.searchList.shown end - controls.searchListFallbackButton = new("ButtonControl", {"LEFT", controls.searchListButton, "RIGHT"}, {5, 0, 110, 20}, "^7Fallback Nodes", function() + + controls.searchListFallbackButton = new("ButtonControl", {"LEFT", controls.searchListButton, "RIGHT"}, {5, 0, 110, buttonHeight}, "^7Fallback Nodes", function() controls.searchList.shown = false controls.searchList.enabled = false controls.searchListFallback.shown = true @@ -2052,68 +2092,12 @@ function TreeTabClass:FindTimelessJewel() tooltip:AddLine(16, "^7Any manual changes made to your fallback nodes are lost when you click the generate button, as it completely replaces them.") end controls.searchListFallbackButton.locked = function() return controls.searchListFallback.shown end - controls.searchList = new("EditControl", {"TOPLEFT", nil, "TOPLEFT"}, {12, 275, 438, 200}, timelessData.searchList, nil, "^%C\t\n", nil, function(value) - timelessData.searchList = value - parseSearchList(0, false) - self.build.modFlag = true - end, 16, true) - controls.searchList.shown = true - controls.searchList.enabled = true - controls.searchList:SetText(timelessData.searchList and timelessData.searchList or "") - controls.searchListFallback = new("EditControl", {"TOPLEFT", nil, "TOPLEFT"}, {12, 275, 438, 200}, timelessData.searchListFallback, nil, "^%C\t\n", nil, function(value) - timelessData.searchListFallback = value - parseSearchList(0, true) - self.build.modFlag = true - end, 16, true) - controls.searchListFallback.shown = false - controls.searchListFallback.enabled = false - controls.searchListFallback:SetText(timelessData.searchListFallback and timelessData.searchListFallback or "") - controls.searchResultsLabel = new("LabelControl", { "TOPLEFT", nil, "TOPRIGHT" }, { -390, 250, 0, 16 }, "^7Results:") - controls.searchResults = new("TimelessJewelListControl", { "TOPLEFT", nil, "TOPRIGHT" }, { -450, 275, 438, 200 }, self.build) - controls.searchTradeLeagueSelect = new("DropDownControl", { "BOTTOMRIGHT", controls.searchResults, "TOPRIGHT" }, { -175, -5, 140, 20 }, nil, function(_, value) - self.timelessJewelLeagueSelect = value - end) + controls.searchResults = new("TimelessJewelListControl", { "BOTTOMLEFT", nil, "BOTTOMLEFT" }, + { edgePadding*2 + listWidth, -(buttonHeight + edgePadding * 2), listWidth, listHeight }, self.build) self.tradeQueryRequests = new("TradeQueryRequests") - controls.msg = new("LabelControl", nil, { -280, 5, 0, 16 }, "") - if #self.tradeLeaguesList > 0 then - controls.searchTradeLeagueSelect:SetList(self.tradeLeaguesList) - -- restore the last league selected - for i, league in ipairs(self.tradeLeaguesList) do - if league == self.timelessJewelLeagueSelect then - controls.searchTradeLeagueSelect:SetSel(i) - break - end - end - else - self.tradeQueryRequests:FetchLeagues("pc", function(leagues, errMsg) - if errMsg then - controls.msg.label = "^1Error fetching league list, default league will be used\n"..errMsg.."^7" - return - end - local tempLeagueTable = { } - for _, league in ipairs(leagues) do - if league ~= "Standard" and league ~= "Ruthless" and league ~= "Hardcore" and league ~= "Hardcore Ruthless" then - if not (league:find("Hardcore") or league:find("Ruthless")) then - -- set the dynamic, base league name to index 1 to sync league shown in dropdown on load with default/old behavior of copy trade url - t_insert(tempLeagueTable, league) - for _, val in ipairs(self.tradeLeaguesList) do - t_insert(tempLeagueTable, val) - end - self.tradeLeaguesList = copyTable(tempLeagueTable) - else - t_insert(self.tradeLeaguesList, league) - end - end - end - t_insert(self.tradeLeaguesList, "Standard") - t_insert(self.tradeLeaguesList, "Hardcore") - t_insert(self.tradeLeaguesList, "Ruthless") - t_insert(self.tradeLeaguesList, "Hardcore Ruthless") - controls.searchTradeLeagueSelect:SetList(self.tradeLeaguesList) - end) - end - controls.searchTradeButton = new("ButtonControl", { "BOTTOMRIGHT", controls.searchResults, "TOPRIGHT" }, { 0, -5, 170, 20 }, "Copy Trade URL", function() + controls.msg = new("LabelControl", nil, { -280, 5, 0, 20 }, "") + controls.searchTradeButton = new("ButtonControl", { "BOTTOMRIGHT", controls.searchResults, "TOPRIGHT" }, { 0, -rowSpacing, 170, buttonHeight }, "Copy Trade URL", function() local seedTrades = {} local startRow = controls.searchResults.selIndex or 1 local endRow = startRow + m_floor(10 / ((timelessData.sharedResults.conqueror.id == 1) and 3 or 1)) @@ -2159,10 +2143,17 @@ function TreeTabClass:FindTimelessJewel() end end + local tradeTypes = { + "securable", + "available", + "onlineleague", + "online", + "any" + } local search = { query = { status = { - option = "available" + option = tradeTypes[self.tradeTypeIndex] }, stats = { { @@ -2196,10 +2187,12 @@ function TreeTabClass:FindTimelessJewel() end -- if the league was not selected via dropdown, then default to the first league in the dropdown or "" if the leagues could not be read - self.timelessJewelLeagueSelect = self.timelessJewelLeagueSelect or (self.tradeLeaguesList and #self.tradeLeaguesList > 0 and self.tradeLeaguesList[1]) or "" + local selectedRealm = controls.realmSelection:GetSelValue():lower() - Copy("https://www.pathofexile.com/trade/search/"..(self.timelessJewelLeagueSelect).."/?q=" .. (s_gsub(dkjson.encode(search), "[^a-zA-Z0-9]", function(a) - return s_format("%%%02X", s_byte(a)) + local realmPath = selectedRealm == "pc" and "" or (selectedRealm .. "/") + Copy("https://www.pathofexile.com/trade/search/" .. realmPath .. + (controls.searchTradeLeagueSelect:GetSelValue()) .. "/?q=" .. (s_gsub(dkjson.encode(search), "[^a-zA-Z0-9]", function(a) + return s_format("%%%02X", s_byte(a)) end))) controls.searchTradeButton.label = "Copy Next Trade URL" @@ -2214,12 +2207,88 @@ function TreeTabClass:FindTimelessJewel() tooltip:AddLine(16, "^7After selecting a row You can also shift+click on another row to select a range of rows to search.") end - local width = 80 - local divider = 10 - local buttons = 3 - local totalWidth = m_floor(width * buttons + divider * (buttons - 1)) - local buttonX = -totalWidth / 2 + width / 2 - + controls.searchTradeLeagueSelect = new("DropDownControl", { "RIGHT", controls.searchTradeButton, "LEFT" }, + { -labelSpacing, 0, 140, buttonHeight }, nil, function(idx, val) + self.timelessJewelLeagueSelect = val + end) + controls.searchTradeLeagueLabel = new("LabelControl", { "TOPRIGHT", controls.searchTradeLeagueSelect, "TOPLEFT" }, + { -labelSpacing, 0, 0, labelHeight }, "^7League:") + -- Realm selection + self.realmList = { + "PC", "Sony", "Xbox" + } + controls.realmSelection = new("DropDownControl", { "BOTTOMLEFT", controls.searchTradeLeagueSelect, "TOPLEFT" }, + { 0, -rowSpacing, 80, buttonHeight }, self.realmList, nil) + local function updateLeagues() + local currentRealmId = controls.realmSelection:GetSelValue():lower() + if self.tradeLeaguesList[currentRealmId] == nil then self.tradeLeaguesList[currentRealmId] = {} end + local leagueList = self.tradeLeaguesList[currentRealmId] + if leagueList and #leagueList > 0 then + controls.searchTradeLeagueSelect:SetList(leagueList) + -- restore the last league selected + for i, league in ipairs(leagueList) do + if league == self.timelessJewelLeagueSelect then + controls.searchTradeLeagueSelect:SetSel(i) + break + end + end + else + self.tradeQueryRequests:FetchLeagues(currentRealmId, function(leagues, errMsg) + if errMsg then + controls.msg.label = "^1Error fetching league list, default league will be used\n" .. errMsg .. "^7" + return + end + local tempLeagueTable = {} + for _, league in ipairs(leagues) do + if league ~= "Standard" and league ~= "Ruthless" and league ~= "Hardcore" and league ~= "Hardcore Ruthless" then + if not (league:find("Hardcore") or league:find("Ruthless")) then + -- set the dynamic, base league name to index 1 to sync league shown in dropdown on load with default/old behavior of copy trade url + t_insert(tempLeagueTable, league) + for _, val in ipairs(leagueList) do + t_insert(tempLeagueTable, val) + end + leagueList = copyTable(tempLeagueTable) + self.tradeLeaguesList[currentRealmId] = leagueList + else + t_insert(leagueList, league) + end + end + end + t_insert(leagueList, "Standard") + t_insert(leagueList, "Hardcore") + t_insert(leagueList, "Ruthless") + t_insert(leagueList, "Hardcore Ruthless") + controls.searchTradeLeagueSelect:SetList(leagueList) + end) + end + end + controls.realmSelection.selFunc = function(idx, _) + self.selectedRealmIndex = idx + updateLeagues() + end + -- remember previous choice + controls.realmSelection:SetSel(self.selectedRealmIndex or 1) + -- manually call the function because when initialising, because the + -- function does not get called when the selection index stays the same + controls.realmSelection.selFunc(controls.realmSelection.selIndex) + controls.realmLabel = new("LabelControl", { "TOPRIGHT", controls.realmSelection, "TOPLEFT" }, + { -labelSpacing, 0, 0, labelHeight }, "^7Realm:") + + -- Buyout selection + local tradeTypes = { + "Instant buyout", + "Instant buyout and in person", + "In person (online in league)", + "In person (online)", + "Any (includes offline)" + } + controls.tradeTypeSelection = new("DropDownControl", { "LEFT", controls.realmSelection, "RIGHT" }, + { labelSpacing, 0, 205, buttonHeight }, tradeTypes, function(index, value) + self.tradeTypeIndex = index + end) + -- remember previous choice + self.tradeTypeIndex = self.tradeTypeIndex or 1 + controls.tradeTypeSelection:SetSel(self.tradeTypeIndex) -- Helper function to search a single socket local function searchSingleSocket(socketId, socketInfo) if not treeData.nodes[socketId] or not treeData.nodes[socketId].isJewelSocket then @@ -2535,7 +2604,21 @@ function TreeTabClass:FindTimelessJewel() return results end - controls.searchButton = new("ButtonControl", nil, {buttonX, 485, width, 20}, "Search", function() + local panelWidth = edgePadding * 3 + listWidth * 2 + local buttonDivider = 10 + local buttonWidth = 80 + -- reset button anchored to middle of panel and other buttons anchored to it + controls.resetButton = new("ButtonControl", {"BOTTOMLEFT", nil, "BOTTOMLEFT"}, {panelWidth / 2 - buttonWidth/2, -edgePadding, buttonWidth, buttonHeight}, "Reset", function() + updateSearchList("", true) + updateSearchList("", false) + wipeTable(timelessData.searchResults) + controls.searchTradeButton.enabled = false + clearProtected() + end) + controls.closeButton = new("ButtonControl", {"LEFT", controls.resetButton, "RIGHT"}, {buttonDivider, 0, buttonWidth, buttonHeight}, "Cancel", function() + main:ClosePopup() + end) + controls.searchButton = new("ButtonControl", {"RIGHT", controls.resetButton, "LEFT"}, {-buttonDivider, 0, buttonWidth, buttonHeight}, "Search", function() if timelessData.jewelSocket.id == -1 then wipeTable(timelessData.searchResults) wipeTable(timelessData.sharedResults) @@ -2601,16 +2684,7 @@ function TreeTabClass:FindTimelessJewel() end end end) - controls.resetButton = new("ButtonControl", nil, {buttonX + (width + divider), 485, width, 20}, "Reset", function() - updateSearchList("", true) - updateSearchList("", false) - wipeTable(timelessData.searchResults) - controls.searchTradeButton.enabled = false - clearProtected() - end) - controls.closeButton = new("ButtonControl", nil, {buttonX + (width + divider) * 2, 485, width, 20}, "Cancel", function() - main:ClosePopup() - end) - main:OpenPopup(910, 517, "Find a Timeless Jewel", controls) + local panelHeight = 565 + main:OpenPopup(panelWidth, panelHeight, "Find a Timeless Jewel", controls) end