From 0875a36f523002a2addb4272a45062286bd63aab Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:54:09 +0300 Subject: [PATCH 1/6] Add "Buy similar" button to display item controls --- src/Classes/ItemsTab.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 4a13dd2298..382935b296 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -14,6 +14,7 @@ local m_min = math.min local m_ceil = math.ceil local m_floor = math.floor local m_modf = math.modf +local buySimilar = LoadModule("Classes/CompareBuySimilar") local gemTooltip = LoadModule("Classes/GemTooltip") local rarityDropList = { @@ -410,6 +411,15 @@ holding Shift will put it in the second.]]) self:SetDisplayItem() end) + self.controls.displayItemBuySimilar = new("ButtonControl", + { "LEFT", self.controls.removeDisplayItem, "RIGHT", true }, + { 8, 0, 100, 20 }, "Buy similar", function() + local itemSlot = self:GetComparisonSlotNameForItem(self.displayItem) + buySimilar.openPopup(self.displayItem, itemSlot, self.build) + end) + self.controls.displayItemBuySimilar.shown = function() + return self.displayItem + end -- Section: Variant(s) self.controls.displayItemSectionVariant = new("Control", {"TOPLEFT",self.controls.addDisplayItem,"BOTTOMLEFT"}, {0, 8, 0, function() From d1d006cb9e3bc71c269aceff98d2dceb50f78ad4 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sat, 13 Jun 2026 16:22:58 +0300 Subject: [PATCH 2/6] Work around belt implicit matching --- src/Classes/TradeHelpers.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Classes/TradeHelpers.lua b/src/Classes/TradeHelpers.lua index 228f244e15..f112587d8a 100644 --- a/src/Classes/TradeHelpers.lua +++ b/src/Classes/TradeHelpers.lua @@ -168,6 +168,10 @@ end ---@return number? value returned if the mod is an option and uses values. e.g. timeless jewel function M.findTradeHash(item, modLine, modType, isDesecrated) local formattedLine = M.formatDatabaseText(modLine) + -- hack for belt implicits not matching. TODO: use stat_descriptions instead, which define what + -- description is the canonical form that is used on the trade site, either by assuming it's the + -- first one, or one with a marker called "canonical_line" + formattedLine = formattedLine:gsub("Has # Charm Slots", "Has # Charm Slot") -- the data export splits some mods into different parts, even though they -- are technically just one stat. we handle that here local isUnique = item.rarity == "UNIQUE" or item.rarity == "RELIC" From bfc28fb151c3955488a72c03ba55116ced8ec6e0 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 14 Jun 2026 00:14:55 +0300 Subject: [PATCH 3/6] Fix buy similar matching for emotion mods --- src/Classes/TradeHelpers.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Classes/TradeHelpers.lua b/src/Classes/TradeHelpers.lua index f112587d8a..a0961d38c4 100644 --- a/src/Classes/TradeHelpers.lua +++ b/src/Classes/TradeHelpers.lua @@ -256,8 +256,8 @@ function M.findTradeHash(item, modLine, modType, isDesecrated) end end end - -- essence mods don't seem to have spawn weights and are tested last - for _, dbMod in pairs(data.itemMods.Item) do + -- essence and emotion mods don't seem to have spawn weights and are tested last + for _, dbMod in pairs(item.affixes) do local tradeHashMaybe = findStat(dbMod, true) if tradeHashMaybe then return tradeHashMaybe From 422975d49189ddb9bbbdecd709727e9dc3a36e64 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:11:42 +0300 Subject: [PATCH 4/6] Refactor buy similar to match by stat descriptors --- src/Classes/CompareBuySimilar.lua | 88 ++++++--- src/Classes/TradeHelpers.lua | 267 ++++++++++++++-------------- src/Classes/TradeQueryGenerator.lua | 2 +- src/Data/QueryMods.lua | 38 ---- src/Data/TradeSiteStats.lua | 245 ++++++++++++++----------- src/Modules/Common.lua | 2 +- 6 files changed, 338 insertions(+), 304 deletions(-) diff --git a/src/Classes/CompareBuySimilar.lua b/src/Classes/CompareBuySimilar.lua index 3559161c67..3da0d4bdbd 100644 --- a/src/Classes/CompareBuySimilar.lua +++ b/src/Classes/CompareBuySimilar.lua @@ -7,6 +7,15 @@ local t_insert = table.insert local m_floor = math.floor local dkjson = require "dkjson" local tradeHelpers = LoadModule("Classes/TradeHelpers") +local tradeStats = tradeHelpers.getTradeStats() + +-- used to check what stats actually exist on the trade site. +local existingStats = {} +for _, cat in ipairs(tradeStats or {}) do + for _, entry in ipairs(cat.entries) do + existingStats[entry.id] = true + end +end local M = {} @@ -128,13 +137,12 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is -- Mod filters for i, entry in ipairs(modEntries) do local prefix = "mod" .. i - if entry.tradeId and controls[prefix .. "Check"] and controls[prefix .. "Check"].state then - local filter = { id = entry.tradeId } + local function getFilter(tradeId) + local filter = { id = tradeId } if entry.isOption then filter.value = { min = entry.value, max = entry.value } elseif entry.value then local minVal = tonumber(controls[prefix .. "Min"].buf) - local maxVal = tonumber(controls[prefix .. "Max"].buf) local value = {} if minVal then @@ -152,7 +160,20 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is filter.value = value end end - t_insert(queryTable.query.stats[1].filters, filter) + return filter + end + if controls[prefix .. "Check"] and controls[prefix .. "Check"].state then + if #entry.tradeIds == 1 then + -- 1 id entries are added to the stat filters section + t_insert(queryTable.query.stats[1].filters, getFilter(entry.tradeIds[1])) + elseif #entry.tradeIds > 1 then + -- ambiguous entries are added as a sparate count filter + local countFilter = { type = "count", value = { min = 1 }, filters = {} } + for _, tradeId in ipairs(entry.tradeIds) do + t_insert(countFilter.filters, getFilter(tradeId)) + end + t_insert(queryTable.query.stats, countFilter) + end end end @@ -202,16 +223,19 @@ function M.openPopup(item, slotName, primaryBuild) -- this adds a single aggregated entry for matching stats (e.g. transformed flat dmg mods) which avoids issues with confusing results. different types are not summed as e.g. implicit and explicit mods are separate in the search. options are also avoided as they don't represent values that can be added combined local function insertOrAddToExisting(entry) for _, existingFilter in ipairs(modEntries) do - if (not existingFilter.isOption) and entry.value - and existingFilter.tradeId and existingFilter.tradeId == entry.tradeId - and existingFilter.type == entry.type - then + -- check if all result trade ids are equal + local sameHashes = #entry.tradeIds > 0 and tableDeepEquals(entry.tradeIds, existingFilter.tradeIds) + if sameHashes and existingFilter.type == entry.type then + -- count of combined lines existingFilter.count = existingFilter.count + 1 - local value = (entry.invert ~= existingFilter.invert) and -entry.value or entry.value - existingFilter.value = (existingFilter.value or 0) + value + if entry.value then + local value = (entry.invert ~= existingFilter.invert) and -entry.value or entry.value or 0 + existingFilter.value = (existingFilter.value or 0) + value + end t_insert(existingFilter.formattedLines, entry.formattedLines[1]) return end + ::continue:: end t_insert(modEntries, entry) end @@ -228,24 +252,38 @@ function M.openPopup(item, slotName, primaryBuild) -- Use range-resolved text for matching local resolvedLine = (modLine.range and itemLib.applyRange(modLine.line, modLine.range, modLine.valueScalar)) or modLine.line - local tradeHash, identifier, value = tradeHelpers.findTradeHash(item, resolvedLine, source.type, modLine.desecrated) - local isOption = not not identifier - if not identifier then - identifier = tradeHash and string.format("%s.stat_%s", source.type, tradeHash) - value = tradeHelpers.modLineValue(resolvedLine) - end - local invert = (not isOption) and tradeHelpers.shouldBeInverted(identifier, resolvedLine, source.type) - insertOrAddToExisting({ + -- check option first, because even if we match a line via the descriptors, the trade id formatting is different for options. e.g.: explicit.stat_345345|33 + local tradeId, value = tradeHelpers.findTradeIdOption(resolvedLine, source.type) + + local entry = { -- this array will always start with one line, but if multiple mods are -- aggregated together it will contain the original mod lines for each - formattedLines = {formatted}, - tradeId = identifier, - value = value, - isOption = isOption, + formattedLines = { formatted }, type = source.type, - invert = invert, + isOption = true, + invert = false, count = 1, - }) + tradeIds = { tradeId }, + value = value, + } + + if not tradeId then + local resultHashes, value, invert = tradeHelpers.findTradeHash(resolvedLine) + -- convert hashes to string ids + local resultIds = {} + if resultHashes then + for idx = 1, #resultHashes do + local id = string.format("%s.stat_%s", source.type, resultHashes[idx]) + if existingStats[id] then + resultIds[idx] = id + end + end + end + entry.tradeIds = resultHashes + entry.value = value + entry.invert = invert + end + insertOrAddToExisting(entry) end end end @@ -407,7 +445,7 @@ function M.openPopup(item, slotName, primaryBuild) end prevType = entry.type local prefix = "mod" .. i - local canSearch = entry.tradeId ~= nil + local canSearch = #entry.tradeIds > 0 local rows = #entry.formattedLines diff --git a/src/Classes/TradeHelpers.lua b/src/Classes/TradeHelpers.lua index a0961d38c4..18ba5c4930 100644 --- a/src/Classes/TradeHelpers.lua +++ b/src/Classes/TradeHelpers.lua @@ -5,6 +5,30 @@ -- local m_floor = math.floor local dkjson = require "dkjson" +local statDescData = require("Data.StatDescriptions.stat_descriptions") + +-- precalculate patterns used for matching stat lines +local numberPattern = "%%d%+%%.%?%%d*" +for _, statDescEntry in ipairs(statDescData) do + for _, desc in ipairs(statDescEntry[1] or {}) do + desc.pat = desc.text + -- escape percentages + :gsub("%%", "%%%%") + -- and minus signs + :gsub("%-{", "%%%-{") + -- make plus signs optional and escape them. resistances for example have + in pob but + -- dont in the stat descriptors + :gsub("%+{", "%%%+%?{") + -- match # to # as one block since the trade site uses the midpoint + :gsub("{%d?:?%+?%-?d?} to {%d?:?%+?%-?d?}", string.format("(%s to %s)", numberPattern, numberPattern)) + + -- match negative number variables + :gsub("{%d?:%-d}", "%-(" .. numberPattern .. ")") + :gsub("{%d?:?%+?d?}", "%%%+%?(" .. numberPattern .. ")") + -- match basic number variables like {}, {0} or {:d} + :gsub("{%d?:?d?}", "(" .. numberPattern .. ")") + end +end local M = {} @@ -29,10 +53,13 @@ end -- Helper: extract the first number from a mod line for value comparison, or in the case of # to # -- mods, the midpoint of that range --- @param line string -function M.modLineValue(line) +--- @param onlyFromTo boolean whether we should only check for # to # matches +function M.modLineValue(line, onlyFromTo) local low, high = line:match("(%-?%d+%.?%d*) to (%-?%d+%.?%d*)") if low and high then return (tonumber(low) + tonumber(high)) / 2 + elseif onlyFromTo then + return nil end return tonumber(line:match("%-?[%d]+%.?[%d]*")) end @@ -63,7 +90,7 @@ local function getOptionTradeStatMap(tradeStats) if entry.text:match("#") then -- work around issue where pob splits timeless jewel -- mods into separate mod lines - newEntry.pattern = entry.text:gsub("\n.*", ""):gsub("(%+)", "%%+"):gsub("#", "(%%d%+)") + newEntry.pattern = entry.text:gsub("\n.*", ""):gsub("(%+)", "%%+"):gsub("#", "(%%d%+)"):lower() end table.insert(optionTradeStatMap[cat.id], newEntry) end @@ -108,161 +135,133 @@ function M.swapInverse(modLine) return modLine, inverseKey end --- checks if the mod should be inverted before query ---- @param tradeId string ---- @param modLine string ---- @param modType string -function M.shouldBeInverted(tradeId, modLine, modType) - local formattedLine = M.formatDatabaseText(M.formatDatabaseText(modLine)) - local invertedLine, inverseKey = M.swapInverse(formattedLine) - invertedLine = invertedLine:gsub("^%-", "") - if not inverseKey then - return false +-- used for calculating the hash field of a stat +local GGG_STAT_HASH32_SEED = 0xC58F1A7B +-- used for calculating the trade hash from stat hash fields +local GGG_TRADE_SEED = 0x02312233 +---@param stats string[] +---@param extraStat string extra stat for time-lost jewels +---@return integer +local function hashStats(stats, extraStat) + if extraStat then + stats = copyTable(stats) + table.insert(stats, extraStat) end - for _, category in ipairs(M.getTradeStats() or {}) do - if category.id == modType then - for _, stat in ipairs(category.entries) do - if tradeId == stat.id then - -- remove radius jewel extra text - local formattedTradeSiteText = M.formatDatabaseText(stat.text) - -- there are multiple stat variants on the trade site which are marked with e.g. (Local). None of these seem to be inverted, so we can check for those and return early - if formattedTradeSiteText:match(" %(%w+%)$") then - return false - end - - -- test for inverted mod - if inverseKey - and ((invertedLine == formattedTradeSiteText) - or (invertedLine:gsub("^%+", "") == formattedTradeSiteText)) then - return true - end - - -- otherwise it's probably not inverted - return false - end - end - end + local statHashes = "" + for _, statName in ipairs(stats) do + local newHash = intToBytes(murmurHash2(statName, GGG_STAT_HASH32_SEED)) + statHashes = statHashes .. newHash end - return false + return murmurHash2(statHashes, GGG_TRADE_SEED) end --- Helper: normalise data texts to # format ---- @param text string -function M.formatDatabaseText(text) - -- decimal -> integer - text = text:gsub("%d+%.%d+", "1") - -- (123-124) -> # - text = text:gsub("%(%d+%-%d+%)", "#") - text = text:gsub("%d+", "#") - return text -end - - --- Helper: find the trade stat ID for a mod line ----@param item table ----@param modLine string ----@param modType string ----@param isDesecrated boolean ----@return number? hash returned for most mods ----@return string? optionTradeId returned if the mod is an option. e.g. Allocates X ----@return number? value returned if the mod is an option and uses values. e.g. timeless jewel -function M.findTradeHash(item, modLine, modType, isDesecrated) - local formattedLine = M.formatDatabaseText(modLine) - -- hack for belt implicits not matching. TODO: use stat_descriptions instead, which define what - -- description is the canonical form that is used on the trade site, either by assuming it's the - -- first one, or one with a marker called "canonical_line" - formattedLine = formattedLine:gsub("Has # Charm Slots", "Has # Charm Slot") - -- the data export splits some mods into different parts, even though they - -- are technically just one stat. we handle that here - local isUnique = item.rarity == "UNIQUE" or item.rarity == "RELIC" - local function findStat(dbMod, ignoreWeights) - local excludeTags = (not isUnique) and { default = true } or nil - if not ignoreWeights and #(dbMod.weightKey or {}) > 0 - and not (item:GetModSpawnWeight(dbMod, nil, excludeTags) > 0) then - return nil - end - for tradeHash, description in pairs(dbMod.tradeHashes) do - local tradeLine = table.concat(description, "\n") - if formattedLine == M.formatDatabaseText(tradeLine) then - return tradeHash - end - - -- the mod line splitting between the stat export and item parsing - -- can be different. hence we test both a combined line and separate - -- lines - for _, descLine in ipairs(description) do - if formattedLine == M.formatDatabaseText(descLine) then - return tradeHash - end - end - end - end - +---@return string? tradeId +---@return number? value Only returned when applicable (primarily timeless jewels) +function M.findTradeIdOption(modLine, modType) + -- match stringify() behaviour + modLine = modLine:gsub("\n", " ") local tradeStats = M.getTradeStats() local optionTradeStatMap = getOptionTradeStatMap(tradeStats) if not tradeStats or not optionTradeStatMap then return end for _, v in ipairs(optionTradeStatMap[modType] or {}) do if v.pattern then - local match = modLine:match(v.pattern) + local match = modLine:lower():match(v.pattern) if match then - return nil, v.tradeId, tonumber(match) + return v.tradeId, tonumber(match) end - elseif v.text == modLine then - return nil, v.tradeId + elseif v.text:lower() == modLine:lower() then + return v.tradeId end end +end - -- desecrate-only mods - if isDesecrated then - for _, dbMod in pairs(data.itemMods.Desecrated) do - local tradeHashMaybe = findStat(dbMod, isUnique) - if tradeHashMaybe then - return tradeHashMaybe - end +-- Helper: find the trade stat ID for a mod line +---@param modLine string +---@return table[] results Can include more than one result if the results are ambiguous +---@return number? value Might be nil if the line has no sensible number value +---@return boolean shouldNegate whether the mod needs to be negated when given to the trade site +function M.findTradeHash(modLine) + local resultIds = {} + local value + local shouldNegate + local extraStat + -- time-lost jewels don't have proper stat descriptors and need to be handled separately + local timeLostJewelLines = { + ["^Notable Passive Skills in Radius also grant "] = "local_jewel_mod_stats_added_to_notable_passives", + ["^Small Passive Skills in Radius also grant "] = "local_jewel_mod_stats_added_to_small_passives", + } + for pat, stat in pairs(timeLostJewelLines) do + if modLine:match(pat) then + modLine = modLine:gsub(pat, "") + extraStat = stat + break end end - -- corruptions - if modType == "enchant" then - for _, dbMod in pairs(data.itemMods.Corruption) do - local tradeHashMaybe = findStat(dbMod) - if tradeHashMaybe then - return tradeHashMaybe - end + for _, statDescEntry in ipairs(statDescData) do + local statdescs = statDescEntry[1] + if not statdescs then + goto continue end - -- most implicit and explicit applicable to the type - elseif modType == "implicit" or modType == "explicit" then - for _, dbMod in pairs(item.affixes) do - local tradeHashMaybe = findStat(dbMod) - if tradeHashMaybe then - return tradeHashMaybe + -- by default, the trade site uses the first form listed in the stat descriptions, but there + -- can be a flag that says otherwise + -- local canonical_line = 1 + -- the stat descriptions default to using the first stat for the trade site, but this + -- flag can define it to be another one + local canonical_stat = 1 + local canonical_negated = false + for statDescIdx, statdesc in ipairs(statdescs) do + local negate = false + for desc_idx, flag in ipairs(statdesc) do + if (k == "negate" or k == "negate_and_double") and v == 1 then + negate = true + end + if k == "canonical_stat" then + canonical_stat = v + end + if desc_idx == 1 or k == "canonical_line" then + -- canonical_line = desc_idx + canonical_negated = negate + end end end - end - -- special mods with weird weights - for _, dbMod in pairs(data.itemMods.Exclusive) do - local tradeHashMaybe = findStat(dbMod, true) - if tradeHashMaybe then - return tradeHashMaybe - end - end - -- charm mods - if item.base and item.base.type == "Charm" then - for _, dbMod in pairs(data.itemMods.Charm) do - -- charms don't seem to have any spawn weights, so allow the default tag here - local tradeHashMaybe = findStat(dbMod, true) - if tradeHashMaybe then - return tradeHashMaybe + for _, statdesc in ipairs(statdescs) do + local negate = false + for desc_idx, flag in ipairs(statdesc) do + if (k == "negate" or k == "negate_and_double") and v == 1 then + negate = true + end + end + -- stat has no variables + if modLine == statdesc.text then + local tradeHash = hashStats(statDescEntry.stats, extraStat) + table.insert(resultIds, tradeHash) + shouldNegate = false + -- it's hard to know the correct value, but many stats have a form with no variables when the chance to do something is 100%. this should assign a value for those + value = tonumber(statdesc.limit[statDescIdx] and statdesc.limit[statDescIdx][1]) + goto continue + end + -- ensure no false positives by requiring a full line match. this is not possible in gmatch as it doesn't support ^ + if modLine:match("^" .. statdesc.pat .. "$") then + local idx = 1 + for match in modLine:gmatch(statdesc.pat) do + -- note that if the desired value isn't the first match and this is a # to #, + -- this will break as it contains two values. however, there is only a single + -- example where # to # are not the first two values currently + local number = tonumber(match) or M.modLineValue(match) + if number and idx == canonical_stat then + shouldNegate = negate ~= canonical_negated + local tradeHash = hashStats(statDescEntry.stats, extraStat) + table.insert(resultIds, tradeHash) + value = number + end + idx = idx + 1 + end end end + ::continue:: end - -- essence and emotion mods don't seem to have spawn weights and are tested last - for _, dbMod in pairs(item.affixes) do - local tradeHashMaybe = findStat(dbMod, true) - if tradeHashMaybe then - return tradeHashMaybe - end - end + return resultIds, value, shouldNegate end -- Map slot name + item type to (trade API category string, itemCategoryTags key). diff --git a/src/Classes/TradeQueryGenerator.lua b/src/Classes/TradeQueryGenerator.lua index 668826aef8..3f32892144 100644 --- a/src/Classes/TradeQueryGenerator.lua +++ b/src/Classes/TradeQueryGenerator.lua @@ -95,7 +95,7 @@ local function getStatEntries(modType) ["HeartOfTheWell"] = "explicit", ["AgainstTheDarkness"] = "explicit", } - if tradeStatCategoryIndices[modType] then + if tradeStatCategoryIndices[modType] or tradeStats[modType] then for i, cat in ipairs(tradeStats) do if cat.id == tradeStatCategoryIndices[modType] then return cat.entries diff --git a/src/Data/QueryMods.lua b/src/Data/QueryMods.lua index 3a5ec2a619..91d9eee8e1 100644 --- a/src/Data/QueryMods.lua +++ b/src/Data/QueryMods.lua @@ -27784,20 +27784,6 @@ return { ["type"] = "implicit", }, }, - ["3489782002"] = { - ["Amulet"] = { - ["max"] = 30, - ["min"] = 20, - }, - ["specialCaseData"] = { - }, - ["tradeMod"] = { - ["id"] = "implicit.stat_3489782002", - ["text"] = "# to maximum Energy Shield", - ["type"] = "implicit", - }, - ["usePositiveSign"] = true, - }, ["3544800472"] = { ["Chest"] = { ["max"] = 40, @@ -28544,14 +28530,6 @@ return { ["max"] = 1, ["min"] = 1, }, - ["2HWeapon"] = { - ["max"] = 1, - ["min"] = 1, - }, - ["Quarterstaff"] = { - ["max"] = 1, - ["min"] = 1, - }, ["specialCaseData"] = { }, ["tradeMod"] = { @@ -28726,18 +28704,10 @@ return { ["max"] = -10, ["min"] = -10, }, - ["2HWeapon"] = { - ["max"] = -10, - ["min"] = -10, - }, ["Helmet"] = { ["max"] = 4, ["min"] = 4, }, - ["Quarterstaff"] = { - ["max"] = -10, - ["min"] = -10, - }, ["Spear"] = { ["max"] = -10, ["min"] = -10, @@ -31410,18 +31380,10 @@ return { ["max"] = 4, ["min"] = 4, }, - ["2HWeapon"] = { - ["max"] = 4, - ["min"] = 4, - }, ["Helmet"] = { ["max"] = 1, ["min"] = 1, }, - ["Quarterstaff"] = { - ["max"] = 4, - ["min"] = 4, - }, ["Spear"] = { ["max"] = 4, ["min"] = 4, diff --git a/src/Data/TradeSiteStats.lua b/src/Data/TradeSiteStats.lua index 0ee8a17daf..fe17526e17 100644 --- a/src/Data/TradeSiteStats.lua +++ b/src/Data/TradeSiteStats.lua @@ -908,7 +908,7 @@ return { }, { ["id"] = "explicit.stat_3451259830", - ["text"] = "#% chance to gain Onslaught for 3 seconds when you kill an enemy affected by Abyssal Wasting", + ["text"] = "#% chance to gain Onslaught for 3 seconds when you kill an enemy affected by Abyssal Wasting", ["type"] = "explicit", }, { @@ -968,7 +968,7 @@ return { }, { ["id"] = "explicit.stat_19819865", - ["text"] = "#% chance to revive one of your Persistent Minions when you kill an enemy affected by Abyssal Wasting", + ["text"] = "#% chance to revive one of your Persistent Minions when you kill an enemy affected by Abyssal Wasting", ["type"] = "explicit", }, { @@ -978,7 +978,7 @@ return { }, { ["id"] = "explicit.stat_3927679277", - ["text"] = "#% chance when collecting an Elemental Infusion to gain anadditional Elemental Infusion of the same type", + ["text"] = "#% chance when collecting an Elemental Infusion to gain an additional Elemental Infusion of the same type", ["type"] = "explicit", }, { @@ -1648,12 +1648,12 @@ return { }, { ["id"] = "explicit.stat_461663422", - ["text"] = "#% increased Effect of Jewel Socket Passive Skillscontaining Corrupted Magic Jewels", + ["text"] = "#% increased Effect of Jewel Socket Passive Skills containing Corrupted Magic Jewels", ["type"] = "explicit", }, { ["id"] = "explicit.stat_3128077011", - ["text"] = "#% increased Effect of Jewel Socket Passive Skillscontaining Corrupted Rare Jewels", + ["text"] = "#% increased Effect of Jewel Socket Passive Skills containing Corrupted Rare Jewels", ["type"] = "explicit", }, { @@ -2498,12 +2498,12 @@ return { }, { ["id"] = "explicit.stat_1602191394", - ["text"] = "#% increased Rarity of Items foundYour other Modifiers to Rarity of Items found do not apply", + ["text"] = "#% increased Rarity of Items found Your other Modifiers to Rarity of Items found do not apply", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2261942307", - ["text"] = "#% increased Rarity of Items foundYour other Modifiers to Rarity of Items found do not apply", + ["text"] = "#% increased Rarity of Items found Your other Modifiers to Rarity of Items found do not apply", ["type"] = "explicit", }, { @@ -9583,7 +9583,7 @@ return { }, { ["id"] = "explicit.stat_258955603", - ["text"] = "Alternating every 5 seconds:Take #% more Damage from HitsTake #% more Damage over time", + ["text"] = "Alternating every 5 seconds: Take #% more Damage from Hits Take #% more Damage over time", ["type"] = "explicit", }, { @@ -10023,7 +10023,7 @@ return { }, { ["id"] = "explicit.stat_842299438", - ["text"] = "Bolts fired by Crossbow Attacks have #% chance to notexpend Ammunition if you've Reloaded Recently", + ["text"] = "Bolts fired by Crossbow Attacks have #% chance to not expend Ammunition if you've Reloaded Recently", ["type"] = "explicit", }, { @@ -10043,7 +10043,7 @@ return { }, { ["id"] = "explicit.stat_1217651243", - ["text"] = "Breaches expand to at least # metre in radiusBreaches remain open while there are alive Breach Monsters", + ["text"] = "Breaches expand to at least # metre in radius Breaches remain open while there are alive Breach Monsters", ["type"] = "explicit", }, { @@ -10123,7 +10123,7 @@ return { }, { ["id"] = "explicit.stat_1617268696", - ["text"] = "Burning Enemies you kill have a #% chance to Explode, dealing atenth of their maximum Life as Fire Damage", + ["text"] = "Burning Enemies you kill have a #% chance to Explode, dealing a tenth of their maximum Life as Fire Damage", ["type"] = "explicit", }, { @@ -10158,7 +10158,7 @@ return { }, { ["id"] = "explicit.stat_627896047", - ["text"] = "Can Attack as though using a One Handed Mace while both of your hand slots are emptyUnarmed Attacks that would use an Equipped One Hand Mace's damage use this Item's damage", + ["text"] = "Can Attack as though using a One Handed Mace while both of your hand slots are empty Unarmed Attacks that would use an Equipped One Hand Mace's damage use this Item's damage", ["type"] = "explicit", }, { @@ -10178,7 +10178,7 @@ return { }, { ["id"] = "explicit.stat_3418590244", - ["text"] = "Can only be applied to Precursor Tower MapsCompleting the Tower makes all nearby Maps accessible", + ["text"] = "Can only be applied to Precursor Tower Maps Completing the Tower makes all nearby Maps accessible", ["type"] = "explicit", }, { @@ -10263,7 +10263,7 @@ return { }, { ["id"] = "explicit.stat_1580426064", - ["text"] = "Cannot use Life FlasksNon-Unique Life Flasks apply their Effects constantlyRecovery from Life Flasks cannot be InstantRecovery from your Life Flasks cannot be applied to anything other than you", + ["text"] = "Cannot use Life Flasks Non-Unique Life Flasks apply their Effects constantly Recovery from Life Flasks cannot be Instant Recovery from your Life Flasks cannot be applied to anything other than you", ["type"] = "explicit", }, { @@ -10498,7 +10498,7 @@ return { }, { ["id"] = "explicit.stat_885925163", - ["text"] = "Copy a random Modifier from each enemy in your Presence whenyou Shapeshift to an Animal formModifiers gained this way are lost after # seconds or when you next Shapeshift", + ["text"] = "Copy a random Modifier from each enemy in your Presence when you Shapeshift to an Animal form Modifiers gained this way are lost after # seconds or when you next Shapeshift", ["type"] = "explicit", }, { @@ -10633,7 +10633,7 @@ return { }, { ["id"] = "explicit.stat_2894895028", - ["text"] = "Damage over Time bypasses your Energy ShieldWhile not on Full Life, Sacrifice #% of maximum Mana per Second to Recover that much Life", + ["text"] = "Damage over Time bypasses your Energy Shield While not on Full Life, Sacrifice #% of maximum Mana per Second to Recover that much Life", ["type"] = "explicit", }, { @@ -11063,7 +11063,7 @@ return { }, { ["id"] = "explicit.stat_1509533589", - ["text"] = "Enemies take #% increased Damage for each Elemental Ailment type amongyour Ailments on them", + ["text"] = "Enemies take #% increased Damage for each Elemental Ailment type among your Ailments on them", ["type"] = "explicit", }, { @@ -11083,7 +11083,7 @@ return { }, { ["id"] = "explicit.stat_2051332707", - ["text"] = "Enemies you kill while they are affected by Abyssal Wasting grant #% increased Flask Charges", + ["text"] = "Enemies you kill while they are affected by Abyssal Wasting grant #% increased Flask Charges", ["type"] = "explicit", }, { @@ -11183,7 +11183,7 @@ return { }, { ["id"] = "explicit.stat_2224139044", - ["text"] = "Every second Slam Skill you use while Shapeshifted is Ancestrally BoostedEvery second Strike Skill you use while Shapeshifted is Ancestrally Boosted", + ["text"] = "Every second Slam Skill you use while Shapeshifted is Ancestrally Boosted Every second Strike Skill you use while Shapeshifted is Ancestrally Boosted", ["type"] = "explicit", }, { @@ -11478,7 +11478,7 @@ return { }, { ["id"] = "explicit.stat_4128965096", - ["text"] = "Gain 1 Explosive Rhythm every # time you use a Grenade SkillRemove all Explosive Rhythm on reaching 10 to gain Explosive Fervour for 10 Seconds", + ["text"] = "Gain 1 Explosive Rhythm every # time you use a Grenade Skill Remove all Explosive Rhythm on reaching 10 to gain Explosive Fervour for 10 Seconds", ["type"] = "explicit", }, { @@ -11493,7 +11493,7 @@ return { }, { ["id"] = "explicit.stat_3492740640", - ["text"] = "Gain 1 Runic Binding on Hit with Spells, no more than once every 0.5 secondsLose all Runic Bindings when you Shapeshift to gain that much Unbound Potential", + ["text"] = "Gain 1 Runic Binding on Hit with Spells, no more than once every 0.5 seconds Lose all Runic Bindings when you Shapeshift to gain that much Unbound Potential", ["type"] = "explicit", }, { @@ -11618,27 +11618,27 @@ return { }, { ["id"] = "explicit.stat_3418580811|24", - ["text"] = "Glorifying the defilement of # souls in tribute to AmanamuPassives in radius are Conquered by the AbyssalsDesecration makes this item unstable", + ["text"] = "Glorifying the defilement of # souls in tribute to Amanamu Passives in radius are Conquered by the Abyssals Desecration makes this item unstable", ["type"] = "explicit", }, { ["id"] = "explicit.stat_3418580811|25", - ["text"] = "Glorifying the defilement of # souls in tribute to KulemakPassives in radius are Conquered by the AbyssalsDesecration makes this item unstable", + ["text"] = "Glorifying the defilement of # souls in tribute to Kulemak Passives in radius are Conquered by the Abyssals Desecration makes this item unstable", ["type"] = "explicit", }, { ["id"] = "explicit.stat_3418580811|26", - ["text"] = "Glorifying the defilement of # souls in tribute to KurgalPassives in radius are Conquered by the AbyssalsDesecration makes this item unstable", + ["text"] = "Glorifying the defilement of # souls in tribute to Kurgal Passives in radius are Conquered by the Abyssals Desecration makes this item unstable", ["type"] = "explicit", }, { ["id"] = "explicit.stat_3418580811|27", - ["text"] = "Glorifying the defilement of # souls in tribute to TecrodPassives in radius are Conquered by the AbyssalsDesecration makes this item unstable", + ["text"] = "Glorifying the defilement of # souls in tribute to Tecrod Passives in radius are Conquered by the Abyssals Desecration makes this item unstable", ["type"] = "explicit", }, { ["id"] = "explicit.stat_3418580811|28", - ["text"] = "Glorifying the defilement of # souls in tribute to UlamanPassives in radius are Conquered by the AbyssalsDesecration makes this item unstable", + ["text"] = "Glorifying the defilement of # souls in tribute to Ulaman Passives in radius are Conquered by the Abyssals Desecration makes this item unstable", ["type"] = "explicit", }, { @@ -11828,22 +11828,22 @@ return { }, { ["id"] = "explicit.stat_3881997959", - ["text"] = "Increases Movement Speed by 25%, plus 1% per # Evasion Rating, up to a maximum of 75%Other Modifiers to Movement Speed except for Sprinting do not apply", + ["text"] = "Increases Movement Speed by 25%, plus 1% per # Evasion Rating, up to a maximum of 75% Other Modifiers to Movement Speed except for Sprinting do not apply", ["type"] = "explicit", }, { ["id"] = "explicit.stat_895564377", - ["text"] = "Increases and Reductions to Cold and Fire Damage in Radius are transformed to apply to Lightning Damage", + ["text"] = "Increases and Reductions to Cold and Fire Damage in Radius are transformed to apply to Lightning Damage", ["type"] = "explicit", }, { ["id"] = "explicit.stat_1400313697", - ["text"] = "Increases and Reductions to Cold and Lightning Damage in Radius are transformed to apply to Fire Damage", + ["text"] = "Increases and Reductions to Cold and Lightning Damage in Radius are transformed to apply to Fire Damage", ["type"] = "explicit", }, { ["id"] = "explicit.stat_3368921525", - ["text"] = "Increases and Reductions to Fire and Lightning Damage in Radius are transformed to apply to Cold Damage", + ["text"] = "Increases and Reductions to Fire and Lightning Damage in Radius are transformed to apply to Cold Damage", ["type"] = "explicit", }, { @@ -11853,7 +11853,7 @@ return { }, { ["id"] = "explicit.stat_3407300125", - ["text"] = "Increases and Reductions to Mana Regeneration Rate alsoapply to Energy Shield Recharge Rate", + ["text"] = "Increases and Reductions to Mana Regeneration Rate also apply to Energy Shield Recharge Rate", ["type"] = "explicit", }, { @@ -11888,7 +11888,7 @@ return { }, { ["id"] = "explicit.stat_971590056", - ["text"] = "Inflict Anaemia on HitAnaemia allows # Corrupted Blood debuffs to be inflicted on enemies", + ["text"] = "Inflict Anaemia on Hit Anaemia allows # Corrupted Blood debuffs to be inflicted on enemies", ["type"] = "explicit", }, { @@ -11903,7 +11903,7 @@ return { }, { ["id"] = "explicit.stat_1695767482", - ["text"] = "Inflict Corrupted Blood for # second on Block, dealing #% ofyour maximum Life as Physical damage per second", + ["text"] = "Inflict Corrupted Blood for # second on Block, dealing #% of your maximum Life as Physical damage per second", ["type"] = "explicit", }, { @@ -11918,7 +11918,7 @@ return { }, { ["id"] = "explicit.stat_223138829", - ["text"] = "Inflict Elemental Exposure to Enemies 3 metres in front of youfor 4 seconds, every 0.25 seconds while raised", + ["text"] = "Inflict Elemental Exposure to Enemies 3 metres in front of you for 4 seconds, every 0.25 seconds while raised", ["type"] = "explicit", }, { @@ -12198,7 +12198,7 @@ return { }, { ["id"] = "explicit.stat_1777740627", - ["text"] = "Life that would be lost by taking Damage is instead Reserveduntil you take no Damage to Life for # second", + ["text"] = "Life that would be lost by taking Damage is instead Reserved until you take no Damage to Life for # second", ["type"] = "explicit", }, { @@ -12553,7 +12553,7 @@ return { }, { ["id"] = "explicit.stat_2889807051", - ["text"] = "Melee Hits count as Rampage KillsRampage", + ["text"] = "Melee Hits count as Rampage Kills Rampage", ["type"] = "explicit", }, { @@ -13518,7 +13518,7 @@ return { }, { ["id"] = "explicit.stat_3538915253", - ["text"] = "On Hitting an enemy, gains maximum added Lightning damage equal tothe enemy's Power for 20 seconds, up to a total of #", + ["text"] = "On Hitting an enemy, gains maximum added Lightning damage equal to the enemy's Power for 20 seconds, up to a total of #", ["type"] = "explicit", }, { @@ -13608,167 +13608,167 @@ return { }, { ["id"] = "explicit.stat_2422708892|45202", - ["text"] = "Passives in Radius of Ancestral Bond can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Ancestral Bond can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|18684", - ["text"] = "Passives in Radius of Avatar of Fire can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Avatar of Fire can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|42680", - ["text"] = "Passives in Radius of Blackflame Covenant can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Blackflame Covenant can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|51749", - ["text"] = "Passives in Radius of Blood Magic can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Blood Magic can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|56605", - ["text"] = "Passives in Radius of Bulwark can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Bulwark can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|56349", - ["text"] = "Passives in Radius of Chaos Inoculation can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Chaos Inoculation can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|33979", - ["text"] = "Passives in Radius of Conduit can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Conduit can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|9085", - ["text"] = "Passives in Radius of Crimson Assault can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Crimson Assault can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|14226", - ["text"] = "Passives in Radius of Dance with Death can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Dance with Death can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|57513", - ["text"] = "Passives in Radius of Eldritch Battery can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Eldritch Battery can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|46742", - ["text"] = "Passives in Radius of Elemental Equilibrium can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Elemental Equilibrium can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|33404", - ["text"] = "Passives in Radius of Eternal Youth can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Eternal Youth can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|32349", - ["text"] = "Passives in Radius of Giant's Blood can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Giant's Blood can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|19288", - ["text"] = "Passives in Radius of Glancing Blows can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Glancing Blows can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|34497", - ["text"] = "Passives in Radius of Heartstopper can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Heartstopper can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|64601", - ["text"] = "Passives in Radius of Hollow Palm Technique can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Hollow Palm Technique can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|28492", - ["text"] = "Passives in Radius of Iron Reflexes can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Iron Reflexes can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|61942", - ["text"] = "Passives in Radius of Lord of the Wilds can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Lord of the Wilds can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|45918", - ["text"] = "Passives in Radius of Mind Over Matter can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Mind Over Matter can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|39935", - ["text"] = "Passives in Radius of Necromantic Talisman can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Necromantic Talisman can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|25100", - ["text"] = "Passives in Radius of Oasis can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Oasis can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|55048", - ["text"] = "Passives in Radius of Pain Attunement can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Pain Attunement can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|37484", - ["text"] = "Passives in Radius of Primal Hunger can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Primal Hunger can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|44017", - ["text"] = "Passives in Radius of Resolute Technique can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Resolute Technique can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|25520", - ["text"] = "Passives in Radius of Resonance can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Resonance can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|11230", - ["text"] = "Passives in Radius of Ritual Cadence can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Ritual Cadence can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|49547", - ["text"] = "Passives in Radius of Scarred Faith can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Scarred Faith can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|41861", - ["text"] = "Passives in Radius of Trusted Kinship can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Trusted Kinship can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|14540", - ["text"] = "Passives in Radius of Unwavering Stance can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Unwavering Stance can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|33369", - ["text"] = "Passives in Radius of Vaal Pact can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Vaal Pact can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|47759", - ["text"] = "Passives in Radius of Whispers of Doom can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Whispers of Doom can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|49363", - ["text"] = "Passives in Radius of Wildsurge Incantation can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Wildsurge Incantation can be Allocated without being connected to your tree", ["type"] = "explicit", }, { ["id"] = "explicit.stat_2422708892|52", - ["text"] = "Passives in Radius of Zealot's Oath can be Allocatedwithout being connected to your tree", + ["text"] = "Passives in Radius of Zealot's Oath can be Allocated without being connected to your tree", ["type"] = "explicit", }, { @@ -13818,7 +13818,7 @@ return { }, { ["id"] = "explicit.stat_436406826", - ["text"] = "Players are Marked for Death for # secondsafter killing a Rare or Unique monster", + ["text"] = "Players are Marked for Death for # seconds after killing a Rare or Unique monster", ["type"] = "explicit", }, { @@ -14198,17 +14198,17 @@ return { }, { ["id"] = "explicit.stat_3418580811|22", - ["text"] = "Remembrancing # songworthy deeds by the line of MedvedPassives in radius are Conquered by the Kalguur", + ["text"] = "Remembrancing # songworthy deeds by the line of Medved Passives in radius are Conquered by the Kalguur", ["type"] = "explicit", }, { ["id"] = "explicit.stat_3418580811|23", - ["text"] = "Remembrancing # songworthy deeds by the line of OlrothPassives in radius are Conquered by the Kalguur", + ["text"] = "Remembrancing # songworthy deeds by the line of Olroth Passives in radius are Conquered by the Kalguur", ["type"] = "explicit", }, { ["id"] = "explicit.stat_3418580811|21", - ["text"] = "Remembrancing # songworthy deeds by the line of VoranaPassives in radius are Conquered by the Kalguur", + ["text"] = "Remembrancing # songworthy deeds by the line of Vorana Passives in radius are Conquered by the Kalguur", ["type"] = "explicit", }, { @@ -15078,7 +15078,7 @@ return { }, { ["id"] = "explicit.stat_1013492127", - ["text"] = "Spells fire # additional ProjectilesSpells fire Projectiles in a circle", + ["text"] = "Spells fire # additional Projectiles Spells fire Projectiles in a circle", ["type"] = "explicit", }, { @@ -15363,7 +15363,7 @@ return { }, { ["id"] = "explicit.stat_603021645", - ["text"] = "When a Party Member in your Presence Casts a Spell, youSacrifice #% of Mana and they Leech that Mana", + ["text"] = "When a Party Member in your Presence Casts a Spell, you Sacrifice #% of Mana and they Leech that Mana", ["type"] = "explicit", }, { @@ -15378,7 +15378,7 @@ return { }, { ["id"] = "explicit.stat_331648983", - ["text"] = "When you reload, triggers Gemini Surge to alternatelygain # Cold Surge or # Fire Surge", + ["text"] = "When you reload, triggers Gemini Surge to alternately gain # Cold Surge or # Fire Surge", ["type"] = "explicit", }, { @@ -15398,7 +15398,7 @@ return { }, { ["id"] = "explicit.stat_2626360934", - ["text"] = "Wind Skills which can be boosted by Elemental Ground Surfaces countas being boosted by Chilled Ground", + ["text"] = "Wind Skills which can be boosted by Elemental Ground Surfaces count as being boosted by Chilled Ground", ["type"] = "explicit", }, { @@ -16224,37 +16224,37 @@ return { }, { ["id"] = "implicit.stat_2369421690", - ["text"] = "Adds Abysses to a Map # use remaining", + ["text"] = "Adds Abysses to a Map # use remaining", ["type"] = "implicit", }, { ["id"] = "implicit.stat_4041853756", - ["text"] = "Adds Irradiated to a Map # use remaining", + ["text"] = "Adds Irradiated to a Map # use remaining", ["type"] = "implicit", }, { ["id"] = "implicit.stat_3166002380", - ["text"] = "Adds Ritual Altars to a Map # use remaining", + ["text"] = "Adds Ritual Altars to a Map # use remaining", ["type"] = "implicit", }, { ["id"] = "implicit.stat_3035440454", - ["text"] = "Adds Vaal Beacons to a Map # use remaining", + ["text"] = "Adds Vaal Beacons to a Map # use remaining", ["type"] = "implicit", }, { ["id"] = "implicit.stat_1714888636", - ["text"] = "Adds a Kalguuran Expedition to a Map # use remaining", + ["text"] = "Adds a Kalguuran Expedition to a Map # use remaining", ["type"] = "implicit", }, { ["id"] = "implicit.stat_3879011313", - ["text"] = "Adds a Mirror of Delirium to a Map # use remaining", + ["text"] = "Adds a Mirror of Delirium to a Map # use remaining", ["type"] = "implicit", }, { ["id"] = "implicit.stat_2219129443", - ["text"] = "Adds an Otherworldy Breach to a Map # use remaining", + ["text"] = "Adds an Otherworldy Breach to a Map # use remaining", ["type"] = "implicit", }, { @@ -16359,7 +16359,7 @@ return { }, { ["id"] = "implicit.stat_3376302538", - ["text"] = "Empowers the Map Boss of a Map # use remaining", + ["text"] = "Empowers the Map Boss of a Map # use remaining", ["type"] = "implicit", }, { @@ -18363,6 +18363,16 @@ return { ["text"] = "Allocates Bolstering Yell", ["type"] = "fractured", }, + { + ["id"] = "fractured.stat_2954116742|1448", + ["text"] = "Allocates Bond of the Cat", + ["type"] = "fractured", + }, + { + ["id"] = "fractured.stat_2954116742|52568", + ["text"] = "Allocates Bond of the Owl", + ["type"] = "fractured", + }, { ["id"] = "fractured.stat_2954116742|5191", ["text"] = "Allocates Bond of the Wolf", @@ -21355,12 +21365,12 @@ return { }, { ["id"] = "fractured.stat_3368921525", - ["text"] = "Increases and Reductions to Fire and Lightning Damage in Radius are transformed to apply to Cold Damage", + ["text"] = "Increases and Reductions to Fire and Lightning Damage in Radius are transformed to apply to Cold Damage", ["type"] = "fractured", }, { ["id"] = "fractured.stat_971590056", - ["text"] = "Inflict Anaemia on HitAnaemia allows # Corrupted Blood debuffs to be inflicted on enemies", + ["text"] = "Inflict Anaemia on Hit Anaemia allows # Corrupted Blood debuffs to be inflicted on enemies", ["type"] = "fractured", }, { @@ -21598,6 +21608,11 @@ return { ["text"] = "Monsters have #% increased Ailment Threshold", ["type"] = "fractured", }, + { + ["id"] = "fractured.stat_3909654181", + ["text"] = "Monsters have #% increased Attack, Cast and Movement Speed", + ["type"] = "fractured", + }, { ["id"] = "fractured.stat_2753083623", ["text"] = "Monsters have #% increased Critical Hit Chance", @@ -23544,6 +23559,11 @@ return { ["text"] = "Allocates Blazing Arms", ["type"] = "crafted", }, + { + ["id"] = "crafted.stat_2954116742|18308", + ["text"] = "Allocates Bleeding Out", + ["type"] = "crafted", + }, { ["id"] = "crafted.stat_2954116742|48925", ["text"] = "Allocates Blessing of the Moon", @@ -24629,6 +24649,11 @@ return { ["text"] = "Allocates Explosive Impact", ["type"] = "crafted", }, + { + ["id"] = "crafted.stat_2954116742|55835", + ["text"] = "Allocates Exposed to the Cosmos", + ["type"] = "crafted", + }, { ["id"] = "crafted.stat_2954116742|10423", ["text"] = "Allocates Exposed to the Inferno", @@ -27691,22 +27716,22 @@ return { }, { ["id"] = "crafted.stat_895564377", - ["text"] = "Increases and Reductions to Cold and Fire Damage in Radius are transformed to apply to Lightning Damage", + ["text"] = "Increases and Reductions to Cold and Fire Damage in Radius are transformed to apply to Lightning Damage", ["type"] = "crafted", }, { ["id"] = "crafted.stat_1400313697", - ["text"] = "Increases and Reductions to Cold and Lightning Damage in Radius are transformed to apply to Fire Damage", + ["text"] = "Increases and Reductions to Cold and Lightning Damage in Radius are transformed to apply to Fire Damage", ["type"] = "crafted", }, { ["id"] = "crafted.stat_3368921525", - ["text"] = "Increases and Reductions to Fire and Lightning Damage in Radius are transformed to apply to Cold Damage", + ["text"] = "Increases and Reductions to Fire and Lightning Damage in Radius are transformed to apply to Cold Damage", ["type"] = "crafted", }, { ["id"] = "crafted.stat_971590056", - ["text"] = "Inflict Anaemia on HitAnaemia allows # Corrupted Blood debuffs to be inflicted on enemies", + ["text"] = "Inflict Anaemia on Hit Anaemia allows # Corrupted Blood debuffs to be inflicted on enemies", ["type"] = "crafted", }, { @@ -33468,7 +33493,7 @@ return { }, { ["id"] = "rune.stat_2703838669", - ["text"] = "#% increased Movement Speed per 15 Spirit, up to a maximum of 40%Other Modifiers to Movement Speed except for Sprinting do not apply", + ["text"] = "#% increased Movement Speed per 15 Spirit, up to a maximum of 40% Other Modifiers to Movement Speed except for Sprinting do not apply", ["type"] = "augment", }, { @@ -33953,7 +33978,7 @@ return { }, { ["id"] = "rune.stat_258955603", - ["text"] = "Alternating every 5 seconds:Take #% more Damage from HitsTake #% more Damage over time", + ["text"] = "Alternating every 5 seconds: Take #% more Damage from Hits Take #% more Damage over time", ["type"] = "augment", }, { @@ -34118,7 +34143,7 @@ return { }, { ["id"] = "rune.stat_3909696841", - ["text"] = "Bonded: #% chance when collecting an Elemental Infusion to gain anadditional Elemental Infusion of the same type", + ["text"] = "Bonded: #% chance when collecting an Elemental Infusion to gain an additional Elemental Infusion of the same type", ["type"] = "augment", }, { @@ -34808,7 +34833,7 @@ return { }, { ["id"] = "rune.stat_2691854696", - ["text"] = "Bonded: Damage of Enemies Hitting you is Unlucky ifyour Runic Ward has been damaged Recently", + ["text"] = "Bonded: Damage of Enemies Hitting you is Unlucky if your Runic Ward has been damaged Recently", ["type"] = "augment", }, { @@ -34986,6 +35011,11 @@ return { ["text"] = "Bonded: Remnants you create have #% increased effect", ["type"] = "augment", }, + { + ["id"] = "rune.stat_2721126863", + ["text"] = "Bonded: Skills which Empower an Attack have #% chance to not count that Attack", + ["type"] = "augment", + }, { ["id"] = "rune.stat_3286003349", ["text"] = "Bonded: Storm Skills have +# to Limit", @@ -35183,7 +35213,7 @@ return { }, { ["id"] = "rune.stat_2241849004", - ["text"] = "Energy Shield Recharge starts after spending a total of 2000 Mana, no more than once every 2 seconds", + ["text"] = "Energy Shield Recharge starts after spending a total of 2000 Mana, no more than once every 2 seconds", ["type"] = "augment", }, { @@ -35268,7 +35298,7 @@ return { }, { ["id"] = "rune.stat_3557924960", - ["text"] = "Gain #% of Damage as Extra Damage of a random Element perRune Socketed in Equipped Items", + ["text"] = "Gain #% of Damage as Extra Damage of a random Element per Rune Socketed in Equipped Items", ["type"] = "augment", }, { @@ -35328,7 +35358,7 @@ return { }, { ["id"] = "rune.stat_2444976134", - ["text"] = "Gain Maximum Energy Shield equal to #% of totalStrength Requirements of Equipped Armour Items", + ["text"] = "Gain Maximum Energy Shield equal to #% of total Strength Requirements of Equipped Armour Items", ["type"] = "augment", }, { @@ -35403,7 +35433,7 @@ return { }, { ["id"] = "rune.stat_4282982513", - ["text"] = "Increases and Reductions to Movement Speed also apply to Energy Shield Recharge Rate", + ["text"] = "Increases and Reductions to Movement Speed also apply to Energy Shield Recharge Rate", ["type"] = "augment", }, { @@ -35431,6 +35461,11 @@ return { ["text"] = "Mana Recovery from Regeneration is also applied to Runic Ward", ["type"] = "augment", }, + { + ["id"] = "rune.stat_275498888", + ["text"] = "Maximum Quality is #%", + ["type"] = "augment", + }, { ["id"] = "rune.stat_4236566306", ["text"] = "Meta Skills gain #% increased Energy", @@ -35483,7 +35518,7 @@ return { }, { ["id"] = "rune.stat_3538915253", - ["text"] = "On Hitting an enemy, gains maximum added Lightning damage equal tothe enemy's Power for 20 seconds, up to a total of #", + ["text"] = "On Hitting an enemy, gains maximum added Lightning damage equal to the enemy's Power for 20 seconds, up to a total of #", ["type"] = "augment", }, { @@ -35513,7 +35548,7 @@ return { }, { ["id"] = "rune.stat_967155385", - ["text"] = "Prevent #% of Damage from Deflected Hits if you'veDeflected no Hits Recently", + ["text"] = "Prevent #% of Damage from Deflected Hits if you've Deflected no Hits Recently", ["type"] = "augment", }, { @@ -35999,7 +36034,7 @@ return { }, { ["id"] = "desecrated.stat_3927679277", - ["text"] = "#% chance when collecting an Elemental Infusion to gain anadditional Elemental Infusion of the same type", + ["text"] = "#% chance when collecting an Elemental Infusion to gain an additional Elemental Infusion of the same type", ["type"] = "desecrated", }, { @@ -38039,7 +38074,7 @@ return { }, { ["id"] = "desecrated.stat_971590056", - ["text"] = "Inflict Anaemia on HitAnaemia allows # Corrupted Blood debuffs to be inflicted on enemies", + ["text"] = "Inflict Anaemia on Hit Anaemia allows # Corrupted Blood debuffs to be inflicted on enemies", ["type"] = "desecrated", }, { @@ -38844,7 +38879,7 @@ return { }, { ["id"] = "desecrated.stat_436406826", - ["text"] = "Players are Marked for Death for # secondsafter killing a Rare or Unique monster", + ["text"] = "Players are Marked for Death for # seconds after killing a Rare or Unique monster", ["type"] = "desecrated", }, { diff --git a/src/Modules/Common.lua b/src/Modules/Common.lua index 3ba68b6853..8e9560a6c1 100644 --- a/src/Modules/Common.lua +++ b/src/Modules/Common.lua @@ -893,7 +893,7 @@ end ---@return string function stringify(thing) if type(thing) == 'string' then - local s = thing:gsub("\n", "") + local s = thing:gsub("\n", " ") return s elseif type(thing) == 'number' then return ""..thing; From e1cb33974bd82818b906d0228702a4523f140d91 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:23:53 +0300 Subject: [PATCH 5/6] Dedupe stat hash code --- src/Classes/TradeHelpers.lua | 23 ++--------------------- src/Export/Scripts/mods.lua | 22 ++++------------------ src/Modules/Common.lua | 21 ++++++++++++++++++++- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/Classes/TradeHelpers.lua b/src/Classes/TradeHelpers.lua index 18ba5c4930..a7626ca286 100644 --- a/src/Classes/TradeHelpers.lua +++ b/src/Classes/TradeHelpers.lua @@ -135,25 +135,6 @@ function M.swapInverse(modLine) return modLine, inverseKey end --- used for calculating the hash field of a stat -local GGG_STAT_HASH32_SEED = 0xC58F1A7B --- used for calculating the trade hash from stat hash fields -local GGG_TRADE_SEED = 0x02312233 ----@param stats string[] ----@param extraStat string extra stat for time-lost jewels ----@return integer -local function hashStats(stats, extraStat) - if extraStat then - stats = copyTable(stats) - table.insert(stats, extraStat) - end - local statHashes = "" - for _, statName in ipairs(stats) do - local newHash = intToBytes(murmurHash2(statName, GGG_STAT_HASH32_SEED)) - statHashes = statHashes .. newHash - end - return murmurHash2(statHashes, GGG_TRADE_SEED) -end ---@return string? tradeId ---@return number? value Only returned when applicable (primarily timeless jewels) @@ -234,7 +215,7 @@ function M.findTradeHash(modLine) end -- stat has no variables if modLine == statdesc.text then - local tradeHash = hashStats(statDescEntry.stats, extraStat) + local tradeHash = HashStats(statDescEntry.stats, extraStat) table.insert(resultIds, tradeHash) shouldNegate = false -- it's hard to know the correct value, but many stats have a form with no variables when the chance to do something is 100%. this should assign a value for those @@ -251,7 +232,7 @@ function M.findTradeHash(modLine) local number = tonumber(match) or M.modLineValue(match) if number and idx == canonical_stat then shouldNegate = negate ~= canonical_negated - local tradeHash = hashStats(statDescEntry.stats, extraStat) + local tradeHash = HashStats(statDescEntry.stats, extraStat) table.insert(resultIds, tradeHash) value = number end diff --git a/src/Export/Scripts/mods.lua b/src/Export/Scripts/mods.lua index 7d2e62302b..ff07ed84a9 100644 --- a/src/Export/Scripts/mods.lua +++ b/src/Export/Scripts/mods.lua @@ -32,21 +32,6 @@ function table.containsId(table, element) return false end --- used for calculating the hash field of a stat -local GGG_STAT_HASH32_SEED = 0xC58F1A7B --- used for calculating the trade hash from stat hash fields -local GGG_TRADE_SEED = 0x02312233 ----@param stats string[] ----@return integer -local function hashStats(stats) - local statHashes = "" - for _, statName in ipairs(stats) do - local newHash = intToBytes(murmurHash2(statName, GGG_STAT_HASH32_SEED)) - statHashes = statHashes..newHash - end - return murmurHash2(statHashes, GGG_TRADE_SEED) -end - local whiteListStat = { ["dummy_stat_display_nothing"] = true, } @@ -197,14 +182,15 @@ local function writeMods(outName, condFunc) local stats = copyTable(statEntry.stats) -- radius jewels lack a proper stat descriptor and so we add it manually local prefix + local extraStat -- radius jewel mods: -- notable if mod.NodeType == 2 then - table.insert(stats, "local_jewel_mod_stats_added_to_notable_passives") + extraStat = "local_jewel_mod_stats_added_to_notable_passives" prefix = "Notable Passive Skills in Radius also grant " -- small elseif mod.NodeType and mod.NodeType == 1 then - table.insert(stats, "local_jewel_mod_stats_added_to_small_passives") + extraStat = "local_jewel_mod_stats_added_to_small_passives" prefix = "Small Passive Skills in Radius also grant " end @@ -217,7 +203,7 @@ local function writeMods(outName, condFunc) end end - local tradeHash = hashStats(stats) + local tradeHash = HashStats(stats, extraStat) tradeHashes[tradeHash] = description ::innerContinue:: end diff --git a/src/Modules/Common.lua b/src/Modules/Common.lua index 8e9560a6c1..bc1cdf3d69 100644 --- a/src/Modules/Common.lua +++ b/src/Modules/Common.lua @@ -1056,4 +1056,23 @@ function GetVirtualScreenSize() height = math.floor(height / scale) end return width, height -end \ No newline at end of file +end +-- used for calculating the hash field of a stat +local GGG_STAT_HASH32_SEED = 0xC58F1A7B +-- used for calculating the trade hash from stat hash fields +local GGG_TRADE_SEED = 0x02312233 +---@param stats string[] +---@param extraStat string extra stat for time-lost jewels +---@return integer +function HashStats(stats, extraStat) + if extraStat then + stats = copyTable(stats) + table.insert(stats, extraStat) + end + local statHashes = "" + for _, statName in ipairs(stats) do + local newHash = intToBytes(murmurHash2(statName, GGG_STAT_HASH32_SEED)) + statHashes = statHashes .. newHash + end + return murmurHash2(statHashes, GGG_TRADE_SEED) +end From 794ee964d4ddb627eedf6f140d8c21b9ee80b7e0 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:30:38 +0300 Subject: [PATCH 6/6] Defeat cspell --- src/Classes/CompareBuySimilar.lua | 2 +- src/Classes/TradeHelpers.lua | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Classes/CompareBuySimilar.lua b/src/Classes/CompareBuySimilar.lua index 3da0d4bdbd..642ab6bf7c 100644 --- a/src/Classes/CompareBuySimilar.lua +++ b/src/Classes/CompareBuySimilar.lua @@ -167,7 +167,7 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is -- 1 id entries are added to the stat filters section t_insert(queryTable.query.stats[1].filters, getFilter(entry.tradeIds[1])) elseif #entry.tradeIds > 1 then - -- ambiguous entries are added as a sparate count filter + -- ambiguous entries are added as a separate count filter local countFilter = { type = "count", value = { min = 1 }, filters = {} } for _, tradeId in ipairs(entry.tradeIds) do t_insert(countFilter.filters, getFilter(tradeId)) diff --git a/src/Classes/TradeHelpers.lua b/src/Classes/TradeHelpers.lua index a7626ca286..7f364540fd 100644 --- a/src/Classes/TradeHelpers.lua +++ b/src/Classes/TradeHelpers.lua @@ -17,7 +17,7 @@ for _, statDescEntry in ipairs(statDescData) do -- and minus signs :gsub("%-{", "%%%-{") -- make plus signs optional and escape them. resistances for example have + in pob but - -- dont in the stat descriptors + -- don't in the stat descriptors :gsub("%+{", "%%%+%?{") -- match # to # as one block since the trade site uses the midpoint :gsub("{%d?:?%+?%-?d?} to {%d?:?%+?%-?d?}", string.format("(%s to %s)", numberPattern, numberPattern)) @@ -180,8 +180,8 @@ function M.findTradeHash(modLine) end end for _, statDescEntry in ipairs(statDescData) do - local statdescs = statDescEntry[1] - if not statdescs then + local statDescriptions = statDescEntry[1] + if not statDescriptions then goto continue end -- by default, the trade site uses the first form listed in the stat descriptions, but there @@ -191,7 +191,7 @@ function M.findTradeHash(modLine) -- flag can define it to be another one local canonical_stat = 1 local canonical_negated = false - for statDescIdx, statdesc in ipairs(statdescs) do + for statDescIdx, statdesc in ipairs(statDescriptions) do local negate = false for desc_idx, flag in ipairs(statdesc) do if (k == "negate" or k == "negate_and_double") and v == 1 then @@ -206,7 +206,7 @@ function M.findTradeHash(modLine) end end end - for _, statdesc in ipairs(statdescs) do + for _, statdesc in ipairs(statDescriptions) do local negate = false for desc_idx, flag in ipairs(statdesc) do if (k == "negate" or k == "negate_and_double") and v == 1 then