From 6a838ea6d76fb7529573dcfecbb170ecf5f3a0f5 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:42:18 +0300 Subject: [PATCH 01/11] Calculate quality dynamically based on equipped pob item --- src/Classes/ImportTab.lua | 9 ++----- src/Classes/SkillsTab.lua | 43 ++++++++++++++++++++++++++++++++- src/Modules/Build.lua | 3 +++ src/Modules/CalcActiveSkill.lua | 5 ++++ src/Modules/CalcPerform.lua | 3 +++ src/Modules/CalcSetup.lua | 7 ++++-- src/Modules/Data.lua | 1 + 7 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 21e7d2adfaf..8f958f21040 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -1839,12 +1839,7 @@ function ImportTabClass:ImportSocketedItems(item, socketedItems, slotName) itemSocketGroupList[groupID] = { label = "", enabled = true, gemList = { }, slot = slotName } end local socketGroup = itemSocketGroupList[groupID] - if not socketedItem.support and socketGroup.gemList[1] and socketGroup.gemList[1].support and not (item.title and item.title:match("Dialla's Malefaction")) then - -- If the first gemInstance is a support gemInstance, put the first active gemInstance before it - t_insert(socketGroup.gemList, 1, gemInstance) - else - t_insert(socketGroup.gemList, gemInstance) - end + t_insert(socketGroup.gemList, gemInstance) if socketedItem.builtInSupport then socketGroup.imbuedSupport = socketedItem.builtInSupport:gsub("Supported by Level 1 ", "") self.build.skillsTab.controls.imbuedSupport.gemChangeFunc(data.gems[data.gemForBaseName[socketGroup.imbuedSupport:lower().." support"]], nil, nil, true, slotName) @@ -1882,7 +1877,7 @@ function ImportTabClass:ImportSocketedItems(item, socketedItems, slotName) else t_insert(self.build.skillsTab.socketGroupList, itemSocketGroup) end - self.build.skillsTab:ProcessSocketGroup(itemSocketGroup) + self.build.skillsTab:ProcessSocketGroup(itemSocketGroup, item) end end diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 8c4644c701e..661e991eea2 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -772,6 +772,15 @@ function SkillsTabClass:CreateGemSlot(index) self:AddUndoState() self.build.buildFlag = true end) + slot.quality.borderFunc = function() + local gemInstance = self.displayGroup.gemList[index] + -- draw colourful colours for sockets with matching sockets + if gemInstance and gemInstance.matchesSocket and gemInstance.color then + return unpack(hexToRGB(gemInstance.color:gsub("%^", "0")) or { 0.5, 0.5, 0.5 }) + else + return 0.5, 0.5, 0.5 + end + end slot.quality.tooltipFunc = function(tooltip) if tooltip:CheckForUpdate(self.build.outputRevision, self.displayGroup) then -- Get the gem instance from the skills @@ -1043,8 +1052,33 @@ function SkillsTabClass:ProcessGemLevel(gemData, imbued) end end +---@param socketGroup table[] +---@param item table? +function SkillsTabClass:CheckSocketGroupSockets(socketGroup, item) + for i, gemInstance in ipairs(socketGroup.gemList) do + gemInstance.matchesSocket = false + -- add quality for matching sockets by looking up linked item + if socketGroup.slot and (gemInstance.grantedEffect or gemInstance.gemData) then + local grantedEffect = gemInstance.grantedEffect or gemInstance.gemData.grantedEffect + local slot = self.build.itemsTab.slots[socketGroup.slot] + local colours = { "R", "G", "B" } + if slot then + -- during import the item this socket group is imported from is + -- provided, but otherwise it will be equipped in the build + if not item then + item = self.build.itemsTab.items[slot.selItemId] + end + if item and item.sockets and item.sockets[i].color == (grantedEffect.color and colours[grantedEffect.color]) then + gemInstance.matchesSocket = true + end + end + end + end +end -- Processes the given socket group, filling in information that will be used for display or calculations -function SkillsTabClass:ProcessSocketGroup(socketGroup) +---@param socketGroup table[] +---@param item table? +function SkillsTabClass:ProcessSocketGroup(socketGroup, item) -- Loop through the skill gem list local data = self.build.data for _, gemInstance in ipairs(socketGroup.gemList) do @@ -1117,8 +1151,15 @@ function SkillsTabClass:ProcessSocketGroup(socketGroup) end end end + self:CheckSocketGroupSockets(socketGroup, item) end +-- reprocess socket groups on rebuild +function SkillsTabClass:UpdateSocketGroups() + for _, socketGroup in ipairs(self.skillSets[self.activeSkillSetId].socketGroupList) do + self:CheckSocketGroupSockets(socketGroup) + end +end -- Set the skill to be displayed/edited function SkillsTabClass:SetDisplayGroup(socketGroup) self.displayGroup = socketGroup diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 69398829e83..2157fe4f91b 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -669,6 +669,8 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.configTab:ImportCalcSettings() end + -- reprocess socket groups as they might depend on items which don't necessarily load first. + self.skillsTab:UpdateSocketGroups() -- Build calculation output tables wipeGlobalCache() self.outputRevision = 1 @@ -1242,6 +1244,7 @@ function buildMode:OnFrame(inputEvents) self.buildFlag = false self.calcsTab:BuildOutput() self:RefreshStatList() + self.skillsTab:UpdateSocketGroups() self.configTab.calcFunc, self.configTab.calcBase = self.calcsTab:GetMiscCalculator(self) end if main.showThousandsSeparators ~= self.lastShowThousandsSeparators then diff --git a/src/Modules/CalcActiveSkill.lua b/src/Modules/CalcActiveSkill.lua index cf9fa5e04f0..9e8d7c23400 100644 --- a/src/Modules/CalcActiveSkill.lua +++ b/src/Modules/CalcActiveSkill.lua @@ -532,6 +532,11 @@ function calcs.buildActiveSkillModList(env, activeSkill) -- Apply gem/quality modifiers from support gems skillModList:NewMod("GemLevel", "BASE", activeSkill.activeEffect.srcInstance and activeSkill.activeEffect.srcInstance.level or activeSkill.activeEffect.level, "Max Level") skillModList:NewMod("GemQuality", "BASE", activeSkill.activeEffect.srcInstance and activeSkill.activeEffect.srcInstance.quality or activeSkill.activeEffect.quality, "Max Quality") + -- matching socket bonus + local socketMatches = activeSkill.activeEffect.srcInstance and activeSkill.activeEffect.srcInstance.matchesSocket or activeSkill.activeEffect.matchesSocket + if socketMatches then + skillModList:NewMod("GemQuality", "BASE", data.misc.MatchingSocketQualityBonus, "Socket Quality") + end for _, supportProperty in ipairs(skillModList:Tabulate("LIST", activeSkill.skillCfg, "SupportedGemProperty")) do local value = supportProperty.value if value.keyword == "grants_active_skill" and activeSkill.activeEffect.gemData and not activeSkill.activeEffect.gemData.tags.support then diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 6c4f13dec26..e90db39f7b4 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -3903,6 +3903,9 @@ function calcs.perform(env, skipEHP) if totalItemQuality > 0 then t_insert(env.player.breakdown.GemQuality, s_format("+ %d ^8(quality from items)", totalItemQuality)) end + if mainSkill.activeEffect.matchesSocket then + t_insert(env.player.breakdown.GemQuality, s_format("+ %d ^8(quality from socket)", data.misc.MatchingSocketQualityBonus)) + end t_insert(env.player.breakdown.GemQuality, s_format("= %d", output.GemQuality)) end end diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 5cadb660dec..3ca6b9e2e3e 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -1558,10 +1558,11 @@ function calcs.initEnv(build, mode, override, specEnv) if not grantedEffect or not grantedEffect.support then return end + local actualQuality = gemInstance.quality + (gemInstance.matchesSocket and data.misc.MatchingSocketQualityBonus or 0) local supportEffect = { grantedEffect = grantedEffect, level = gemInstance.level, - quality = gemInstance.quality, + quality = actualQuality, srcInstance = gemInstance, gemData = gemInstance.gemData, superseded = false, @@ -1629,10 +1630,12 @@ function calcs.initEnv(build, mode, override, specEnv) for index, grantedEffect in ipairs(grantedEffectList) do if not grantedEffect.support and not grantedEffect.unsupported and (not grantedEffect.hasGlobalEffect or gemInstance["enableGlobal"..index]) then slotHasActiveSkill = true + local actualQuality = gemInstance.quality + (gemInstance.matchesSocket and data.misc.MatchingSocketQualityBonus or 0) local activeEffect = { grantedEffect = grantedEffect, level = gemInstance.level, - quality = gemInstance.quality, + quality = actualQuality, + matchesSocket = gemInstance.matchesSocket, srcInstance = gemInstance, gemData = gemInstance.gemData, } diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index ed3ab9af5eb..f6947eba965 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -292,6 +292,7 @@ data.misc = { -- magic numbers PvpElemental2 = 150, PvpNonElemental1 = 0.57, PvpNonElemental2 = 90, + MatchingSocketQualityBonus = 10, } data.skillColorMap = { colorCodes.STRENGTH, colorCodes.DEXTERITY, colorCodes.INTELLIGENCE, colorCodes.NORMAL } From 6b63b1bc162106060eb99ce11d94e944567c6ab1 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:22:13 +0300 Subject: [PATCH 02/11] Support dialla's malefaction quality bonus line --- src/Classes/Item.lua | 5 ++++- src/Classes/SkillsTab.lua | 10 ++++++++-- src/Data/ModCache.lua | 3 +-- src/Modules/ModParser.lua | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index ef2c2603b9c..cee2dc7831e 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -1741,7 +1741,7 @@ function ItemClass:BuildRaw() end if self.sockets and #self.sockets > 0 then local line = "Sockets: " - for i, socket in pairs(self.sockets) do + for i, socket in ipairs(self.sockets) do line = line .. socket.color if self.sockets[i+1] then line = line .. (socket.group == self.sockets[i+1].group and "-" or " ") @@ -2380,6 +2380,9 @@ function ItemClass:BuildModList() end self.sockets = newSockets end + if calcLocal(baseList, "SocketAlwaysMatches", "FLAG", 0) then + self.sockets.colourAlwaysMatches = true + end self.socketedJewelEffectModifier = 1 + calcLocal(baseList, "SocketedJewelEffect", "INC", 0) / 100 if self.base.weapon or self.type == "Ring" then self.slotModList = { } diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 661e991eea2..4604ea0f767 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -1068,8 +1068,14 @@ function SkillsTabClass:CheckSocketGroupSockets(socketGroup, item) if not item then item = self.build.itemsTab.items[slot.selItemId] end - if item and item.sockets and item.sockets[i].color == (grantedEffect.color and colours[grantedEffect.color]) then - gemInstance.matchesSocket = true + if item and item.sockets then + -- e.g. dialla's malefaction + if item.sockets.colourAlwaysMatches then + gemInstance.matchesSocket = true + else + local gemColour = grantedEffect.color and colours[grantedEffect.color] + gemInstance.matchesSocket = item.sockets[i] and (item.sockets[i].color == gemColour) + end end end end diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 4544bc04a84..7be42d7187d 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -9217,8 +9217,7 @@ c["Gains no Charges during Effect Creates a Smoke Cloud on Use"]={nil,"Gains no c["Gains no Charges during Effect Taunts nearby Enemies on use"]={nil,"Gains no Charges during Effect Taunts nearby Enemies on use "} c["Gains no Charges during Effect of any Overflowing Chalice Flask"]={nil,"Gains no Charges during Effect of any Overflowing Chalice Flask "} c["Gains no Charges during Effect of any Soul Ripper Flask"]={nil,"Gains no Charges during Effect of any Soul Ripper Flask "} -c["Gems Socketed always have the Quality bonus from Socket Colour"]={nil,"Gems Socketed always have the Quality bonus from Socket Colour "} -c["Gems Socketed always have the Quality bonus from Socket Colour Has no Attribute Requirements"]={nil,"Gems Socketed always have the Quality bonus from Socket Colour Has no Attribute Requirements "} +c["Gems Socketed always have the Quality bonus from Socket Colour"]={{[1]={flags=0,keywordFlags=0,name="SocketAlwaysMatches",type="FLAG",value=true}},nil} c["Gems Socketed in Blue Sockets gain 100% increased Experience"]={nil,"Gems Socketed in Blue Sockets gain 100% increased Experience "} c["Gems Socketed in Blue Sockets gain 100% increased Experience Gems Socketed always have the Quality bonus from Socket Colour"]={nil,"Gems Socketed in Blue Sockets gain 100% increased Experience Gems Socketed always have the Quality bonus from Socket Colour "} c["Gems Socketed in Blue Sockets gain 25% increased Experience"]={nil,"Gems Socketed in Blue Sockets gain 25% increased Experience "} diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 751b2ada447..47163dc8459 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -3037,6 +3037,7 @@ local specialModList = { ["drop brine ground while moving, lasting 4 seconds"] = { flag("CanCreateBrineGround"), }, -- Item local modifiers ["has no sockets"] = { flag("NoSockets") }, + ["gems socketed always have the quality bonus from socket colour"] = { flag("SocketAlwaysMatches") }, ["reflects your o[tp][hp][eo][rs]i?t?e? ring"] = { -- Display only. For Kalandra's Touch. }, From 98724fadb9b12ddb9dedd11b74a306eb900e348c Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 15:56:52 +0300 Subject: [PATCH 03/11] Add line showing sockets --- src/Classes/ItemsTab.lua | 56 +++++++++++++++++++++------------------ src/Classes/SkillsTab.lua | 23 ++++++++++++---- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index b92cec6abfb..568a7767c11 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -4064,6 +4064,35 @@ local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementIte return spec end +function ItemsTabClass:GetSocketDescriptionLine(item) + -- Sockets/links + local group = 0 + local line = "" + for i, socket in ipairs(item.sockets) do + if i > 1 then + if socket.group == group then + line = line .. "^7=" + else + line = line .. " " + end + group = socket.group + end + local code + if socket.color == "R" then + code = colorCodes.STRENGTH + elseif socket.color == "G" then + code = colorCodes.DEXTERITY + elseif socket.color == "B" then + code = colorCodes.INTELLIGENCE + elseif socket.color == "W" then + code = colorCodes.SCION + elseif socket.color == "A" then + code = "^xB0B0B0" + end + line = line .. code .. socket.color + end + return line +end function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth) local fontSizeSmall = main.showFlavourText and 16 or 14 local fontSizeBig = main.showFlavourText and 18 or 16 @@ -4282,32 +4311,7 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth) end if #item.sockets > 0 then - -- Sockets/links - local group = 0 - local line = "" - for i, socket in ipairs(item.sockets) do - if i > 1 then - if socket.group == group then - line = line .. "^7=" - else - line = line .. " " - end - group = socket.group - end - local code - if socket.color == "R" then - code = colorCodes.STRENGTH - elseif socket.color == "G" then - code = colorCodes.DEXTERITY - elseif socket.color == "B" then - code = colorCodes.INTELLIGENCE - elseif socket.color == "W" then - code = colorCodes.SCION - elseif socket.color == "A" then - code = "^xB0B0B0" - end - line = line .. code .. socket.color - end + local line = self:GetSocketDescriptionLine(item) tooltip:AddLine(fontSizeBig, "^x7F7F7FSockets: "..line, "FONTIN SC") end tooltip:AddSeparator(10) diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 4604ea0f767..5e45730e4ad 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -211,13 +211,26 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont self.build.buildFlag = true end) + self.controls.socketsLabel = new("LabelControl", { "TOPLEFT", self.controls.groupSlotLabel, "BOTTOMLEFT" }, { 0, 6, 0, 16 }, function() + local groupSlot = self.controls.groupSlot:GetSelValue() + if groupSlot.slotName then + local slot = self.build.itemsTab.slots[groupSlot.slotName] + if slot then + local item = self.build.itemsTab.items[slot.selItemId] + if item then + return "^7Item sockets: " .. self.build.itemsTab:GetSocketDescriptionLine(item) + end + end + end + return "" + end) -- self.imbuedSupportBySlot is used by CalcSetup to add an ExtraSupport mod of the selected gem -- Each displayGroup has its own "imbuedSupport" and is saved to the xml to load when changing sockets or loading a build -- "slotName" is used on import, which uses builtInSupport to get the gemData and pass in here -- buildFlag to true triggers the reload/run the CalcSetup to add on the support -- the last var in the GemSelectControl init, the true, sets imbuedSelect to true which sets the level to 1 and support filtering self.imbuedSupportBySlot = { } - self.controls.imbuedSupportLabel = new("LabelControl", { "LEFT", self.controls.groupSlotLabel, "LEFT" }, { 86, 28, 0, 16 }, colorCodes.CRAFTED.."Imbued Support:") + self.controls.imbuedSupportLabel = new("LabelControl", { "TOPLEFT", self.controls.socketsLabel, "BOTTOMLEFT" }, { 0, 6, 0, 16 }, colorCodes.CRAFTED .. "Imbued Support:") self.controls.imbuedSupport = new("GemSelectControl", { "LEFT", self.controls.imbuedSupportLabel, "RIGHT" }, { 8, 0, 250, 20 }, self, 1, function(gemData, _, _, gemMatch, slotName) local targetSlot = slotName or (self.displayGroup and self.displayGroup.slot) if not targetSlot then @@ -266,7 +279,7 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont end self.controls.imbuedSupportClear.tooltipText = "Remove this imbued support." - self.controls.groupCountLabel = new("LabelControl", { "LEFT", self.controls.includeInFullDPS, "RIGHT" }, { 16, 0, 0, 16 }, "Count:") + self.controls.groupCountLabel = new("LabelControl", { "LEFT", self.controls.includeInFullDPS, "RIGHT" }, { 16, 0, 0, 16 }, "^7Count:") self.controls.groupCountLabel.shown = function() return self.displayGroup.source ~= nil end @@ -278,7 +291,7 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont self.controls.groupCount.shown = function() return self.displayGroup.source ~= nil end - self.controls.sourceNote = new("LabelControl", { "TOPLEFT", self.controls.groupSlotLabel, "TOPLEFT" }, { 0, 30, 0, 16 }) + self.controls.sourceNote = new("LabelControl", { "TOPLEFT", self.controls.socketsLabel, "TOPLEFT" }, { 0, 30, 0, 16 }) self.controls.sourceNote.shown = function() return self.displayGroup.source ~= nil end @@ -324,8 +337,8 @@ will automatically apply to the skill.]] self:SetActiveSkillSet(1) -- Skill gem slots - self.anchorGemSlots = new("Control", {"TOPLEFT",self.anchorGroupDetail,"TOPLEFT"}, {0, 28 + 28 + 16 + 28, 0, 0}) - self.gemSlots = { } + self.anchorGemSlots = new("Control", { "TOPLEFT", self.controls.imbuedSupportLabel, "BOTTOMLEFT" }, { 0, 30, 0, 0 }) + self.gemSlots = {} self:CreateGemSlot(1) self.controls.gemNameHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].nameSpec, "TOPLEFT"}, {0, -2, 0, 16}, "^7Gem name:") self.controls.gemLevelHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].level, "TOPLEFT"}, {0, -2, 0, 16}, "^7Level:") From 37a9453f2d422f00bee248c506bf65f35907a487 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 16:16:06 +0300 Subject: [PATCH 04/11] Deal with split groups --- src/Classes/ImportTab.lua | 3 +- src/Classes/SkillsTab.lua | 63 +++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 8f958f21040..a1a9f164bb9 100644 --- a/src/Classes/ImportTab.lua +++ b/src/Classes/ImportTab.lua @@ -1472,6 +1472,7 @@ function ImportTabClass:ImportItemsAndSkills(charData, clearItems, clearSkills, end self.build.itemsTab:PopulateSlots() self.build.itemsTab:AddUndoState() + self.build.skillsTab:UpdateSocketGroups() self.build.skillsTab:AddUndoState() self.build.characterLevel = charData.level self.build.configTab:UpdateLevel() @@ -1877,7 +1878,7 @@ function ImportTabClass:ImportSocketedItems(item, socketedItems, slotName) else t_insert(self.build.skillsTab.socketGroupList, itemSocketGroup) end - self.build.skillsTab:ProcessSocketGroup(itemSocketGroup, item) + self.build.skillsTab:ProcessSocketGroup(itemSocketGroup) end end diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 5e45730e4ad..dcd2cc649e2 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -1065,39 +1065,9 @@ function SkillsTabClass:ProcessGemLevel(gemData, imbued) end end ----@param socketGroup table[] ----@param item table? -function SkillsTabClass:CheckSocketGroupSockets(socketGroup, item) - for i, gemInstance in ipairs(socketGroup.gemList) do - gemInstance.matchesSocket = false - -- add quality for matching sockets by looking up linked item - if socketGroup.slot and (gemInstance.grantedEffect or gemInstance.gemData) then - local grantedEffect = gemInstance.grantedEffect or gemInstance.gemData.grantedEffect - local slot = self.build.itemsTab.slots[socketGroup.slot] - local colours = { "R", "G", "B" } - if slot then - -- during import the item this socket group is imported from is - -- provided, but otherwise it will be equipped in the build - if not item then - item = self.build.itemsTab.items[slot.selItemId] - end - if item and item.sockets then - -- e.g. dialla's malefaction - if item.sockets.colourAlwaysMatches then - gemInstance.matchesSocket = true - else - local gemColour = grantedEffect.color and colours[grantedEffect.color] - gemInstance.matchesSocket = item.sockets[i] and (item.sockets[i].color == gemColour) - end - end - end - end - end -end -- Processes the given socket group, filling in information that will be used for display or calculations ---@param socketGroup table[] ----@param item table? -function SkillsTabClass:ProcessSocketGroup(socketGroup, item) +function SkillsTabClass:ProcessSocketGroup(socketGroup) -- Loop through the skill gem list local data = self.build.data for _, gemInstance in ipairs(socketGroup.gemList) do @@ -1170,13 +1140,40 @@ function SkillsTabClass:ProcessSocketGroup(socketGroup, item) end end end - self:CheckSocketGroupSockets(socketGroup, item) end -- reprocess socket groups on rebuild function SkillsTabClass:UpdateSocketGroups() + local slotSocketedCounts = {} for _, socketGroup in ipairs(self.skillSets[self.activeSkillSetId].socketGroupList) do - self:CheckSocketGroupSockets(socketGroup) + for i, gemInstance in ipairs(socketGroup.gemList) do + gemInstance.matchesSocket = false + -- add quality for matching sockets by looking up linked item + if socketGroup.slot and (gemInstance.grantedEffect or gemInstance.gemData) then + local grantedEffect = gemInstance.grantedEffect or gemInstance.gemData.grantedEffect + local slot = self.build.itemsTab.slots[socketGroup.slot] + -- since PoB processes split links on an item as separate + -- groups, we can assume that we continue from where the last + -- socket group with the slot ended at + local gemIdx = i + (slotSocketedCounts[socketGroup.slot] or 0) + local colours = { "R", "G", "B" } + if slot then + -- during import the item this socket group is imported from is + -- provided, but otherwise it will be equipped in the build + local item = self.build.itemsTab.items[slot.selItemId] + if item and item.sockets then + -- e.g. dialla's malefaction + if item.sockets.colourAlwaysMatches then + gemInstance.matchesSocket = true + else + local gemColour = grantedEffect.color and colours[grantedEffect.color] + gemInstance.matchesSocket = item.sockets[gemIdx] and (item.sockets[gemIdx].color == gemColour) + end + end + end + slotSocketedCounts[socketGroup.slot] = (slotSocketedCounts[socketGroup.slot] or 0) + 1 + end + end end end -- Set the skill to be displayed/edited From da7f4b06b377cf667ff4a3a99b02f3fd748f8a76 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:48:43 +0300 Subject: [PATCH 05/11] Add button to make item colours match gems --- src/Classes/SkillsTab.lua | 121 ++++++++++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 30 deletions(-) diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index dcd2cc649e2..5e51c2272b8 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -211,26 +211,84 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont self.build.buildFlag = true end) - self.controls.socketsLabel = new("LabelControl", { "TOPLEFT", self.controls.groupSlotLabel, "BOTTOMLEFT" }, { 0, 6, 0, 16 }, function() + local function getSelectedItem() + local item local groupSlot = self.controls.groupSlot:GetSelValue() if groupSlot.slotName then local slot = self.build.itemsTab.slots[groupSlot.slotName] if slot then - local item = self.build.itemsTab.items[slot.selItemId] - if item then - return "^7Item sockets: " .. self.build.itemsTab:GetSocketDescriptionLine(item) + item = self.build.itemsTab.items[slot.selItemId] + if not item then + return end end end - return "" + return item, groupSlot + end + self.controls.socketsLabel = new("LabelControl", { "TOPLEFT", self.controls.groupSlotLabel, "BOTTOMLEFT" }, { 0, 8, 0, 16 }, function() + local item = getSelectedItem() + local socketLine = "" + if item and item.base and not item.base.socketLimit then + socketLine = "This item cannot have sockets" + elseif item then + socketLine = self.build.itemsTab:GetSocketDescriptionLine(item) + end + return "^7Item sockets: " .. socketLine + end) + self.controls.socketsLabel.shown = function() + local item = getSelectedItem() + return not not item + end + self.controls.optimiseSockets = new("ButtonControl", { "LEFT", self.controls.socketsLabel, "RIGHT" }, { 4, 0, 120, 18 }, "^7Optimise Sockets", function() + local item, groupSlot = getSelectedItem() + if not item or not groupSlot or not item.base then + return + end + + + -- save count of abyssal sockets + local abyssalSocketCount = 0 + for _, socket in ipairs(item.sockets) do + if socket.color == "A" then + abyssalSocketCount = abyssalSocketCount + 1 + end + end + + local groupCount = 0 + item.sockets = {} + local maxSockets = (item.base.socketLimit or 0) - abyssalSocketCount + for _, group in ipairs(self.socketGroupList) do + local colours = { "R", "G", "B" } + if maxSockets > 0 and group.slot == groupSlot.slotName then + for _, gem in ipairs(group.gemList) do + local grantedEffect = gem.grantedEffect or gem.gemData.grantedEffect + local gemColour = grantedEffect.color and colours[grantedEffect.color] or "W" + table.insert(item.sockets, { color = gemColour, group = groupCount }) + maxSockets = maxSockets - 1 + end + groupCount = groupCount + 1 + end + end + + for _ = 0, abyssalSocketCount - 1 do + groupCount = groupCount + 1 + table.insert(item.sockets, { color = "A", group = groupCount }) + end + item:BuildAndParseRaw() + self:UpdateSocketGroups() end) + self.controls.optimiseSockets.shown = function() + local item = getSelectedItem() + return item and item.base.socketLimit + end + self.controls.optimiseSockets.tooltipText = "Rebuild the item's sockets to match the groups assigned to it." -- self.imbuedSupportBySlot is used by CalcSetup to add an ExtraSupport mod of the selected gem -- Each displayGroup has its own "imbuedSupport" and is saved to the xml to load when changing sockets or loading a build -- "slotName" is used on import, which uses builtInSupport to get the gemData and pass in here -- buildFlag to true triggers the reload/run the CalcSetup to add on the support -- the last var in the GemSelectControl init, the true, sets imbuedSelect to true which sets the level to 1 and support filtering self.imbuedSupportBySlot = { } - self.controls.imbuedSupportLabel = new("LabelControl", { "TOPLEFT", self.controls.socketsLabel, "BOTTOMLEFT" }, { 0, 6, 0, 16 }, colorCodes.CRAFTED .. "Imbued Support:") + self.controls.imbuedSupportLabel = new("LabelControl", { "TOPLEFT", self.controls.socketsLabel, "BOTTOMLEFT", true }, { 0, 8, 0, 16 }, colorCodes.CRAFTED .. "Imbued Support:") self.controls.imbuedSupport = new("GemSelectControl", { "LEFT", self.controls.imbuedSupportLabel, "RIGHT" }, { 8, 0, 250, 20 }, self, 1, function(gemData, _, _, gemMatch, slotName) local targetSlot = slotName or (self.displayGroup and self.displayGroup.slot) if not targetSlot then @@ -267,7 +325,8 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont return isImbuedEnabled() end self.controls.imbuedSupportLabel.shown = function() -- don't show imbued for skills from items - return not self.displayGroup.source + return self.displayGroup and not + self.displayGroup.source end self.controls.imbuedSupportClear = new("ButtonControl", { "LEFT", self.controls.imbuedSupportLabel, "RIGHT" }, { 260, 0, 20, 20}, "x", function() self.controls.imbuedSupport.gemId = nil @@ -667,6 +726,7 @@ function SkillsTabClass:CreateGemSlot(index) self.gemSlots[index2].enableGlobal2.state = gemInstance.enableGlobal2 self.gemSlots[index2].count:SetText(gemInstance.count or 1) end + self:UpdateSocketGroups() self:AddUndoState() self.build.buildFlag = true end @@ -1146,33 +1206,34 @@ end function SkillsTabClass:UpdateSocketGroups() local slotSocketedCounts = {} for _, socketGroup in ipairs(self.skillSets[self.activeSkillSetId].socketGroupList) do - for i, gemInstance in ipairs(socketGroup.gemList) do - gemInstance.matchesSocket = false - -- add quality for matching sockets by looking up linked item - if socketGroup.slot and (gemInstance.grantedEffect or gemInstance.gemData) then - local grantedEffect = gemInstance.grantedEffect or gemInstance.gemData.grantedEffect - local slot = self.build.itemsTab.slots[socketGroup.slot] - -- since PoB processes split links on an item as separate - -- groups, we can assume that we continue from where the last - -- socket group with the slot ended at - local gemIdx = i + (slotSocketedCounts[socketGroup.slot] or 0) - local colours = { "R", "G", "B" } - if slot then - -- during import the item this socket group is imported from is - -- provided, but otherwise it will be equipped in the build - local item = self.build.itemsTab.items[slot.selItemId] - if item and item.sockets then - -- e.g. dialla's malefaction - if item.sockets.colourAlwaysMatches then - gemInstance.matchesSocket = true - else - local gemColour = grantedEffect.color and colours[grantedEffect.color] - gemInstance.matchesSocket = item.sockets[gemIdx] and (item.sockets[gemIdx].color == gemColour) + if socketGroup.slot then + local gemOffset = (slotSocketedCounts[socketGroup.slot] or 0) + for i, gemInstance in ipairs(socketGroup.gemList) do + gemInstance.matchesSocket = false + -- add quality for matching sockets by looking up linked item + if (gemInstance.grantedEffect or gemInstance.gemData) then + local grantedEffect = gemInstance.grantedEffect or gemInstance.gemData.grantedEffect + local slot = self.build.itemsTab.slots[socketGroup.slot] + -- since PoB processes split links on an item as separate + -- groups, we can assume that we continue from where the last + -- socket group with the slot ended at + local colours = { "R", "G", "B" } + local gemIdx = gemOffset + i + if slot then + local item = self.build.itemsTab.items[slot.selItemId] + if item and item.sockets then + -- e.g. dialla's malefaction + if item.sockets.colourAlwaysMatches then + gemInstance.matchesSocket = true + else + local gemColour = grantedEffect.color and colours[grantedEffect.color] + gemInstance.matchesSocket = item.sockets[gemIdx] and (item.sockets[gemIdx].color == gemColour) + end end end end - slotSocketedCounts[socketGroup.slot] = (slotSocketedCounts[socketGroup.slot] or 0) + 1 end + slotSocketedCounts[socketGroup.slot] = gemOffset + #socketGroup.gemList end end end From ded89270c284ccdd93d97a5a3c10734f12b26ce0 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:55:01 +0300 Subject: [PATCH 06/11] Add quality to tooltips as separate lines --- src/Classes/GemTooltip.lua | 25 +++++++++++++++++++------ src/Modules/CalcActiveSkill.lua | 4 ++++ src/Modules/CalcSetup.lua | 24 +++++++++++++++++++++++- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/Classes/GemTooltip.lua b/src/Classes/GemTooltip.lua index 5100615c21f..8fc24cdf33b 100644 --- a/src/Classes/GemTooltip.lua +++ b/src/Classes/GemTooltip.lua @@ -137,12 +137,25 @@ local function addCommonGemInfo(tooltip, build, gemInstance, grantedEffect, addL grantedEffectLevel.damageEffectiveness * 100), "FONTIN SC") end end - if addReq and displayInstance.quality > 0 then - tooltip:AddLine(fontSizeBig, string.format("^x7F7F7FQuality: " .. colorCodes.MAGIC .. "+%d%%^7%s", - gemInstance.quality, - (displayInstance.quality > gemInstance.quality) and - " (" .. colorCodes.MAGIC .. "+" .. (displayInstance.quality - gemInstance.quality) .. "^7)" or "" - ), "FONTIN SC") + if addReq then + if gemInstance.quality > 0 then + tooltip:AddLine(fontSizeBig, colorCodes.MAGIC .. string.format("^x7F7F7FQuality: +%s%d%%", colorCodes.MAGIC, gemInstance.quality), "FONTIN SC") + end + local function formatQuality(number, suffix) + return colorCodes.MAGIC .. string.format("+%d%% Quality from %s", number, suffix) + end + if (displayInstance.itemQuality or 0) > 0 then + tooltip:AddLine(fontSizeBig, formatQuality(displayInstance.itemQuality, "Item"), "FONTIN SC") + end + if (displayInstance.supportQuality or 0) > 0 then + tooltip:AddLine(fontSizeBig, formatQuality(displayInstance.supportQuality, "Support"), "FONTIN SC") + end + if (displayInstance.globalQuality or 0) > 0 then + tooltip:AddLine(fontSizeBig, formatQuality(displayInstance.globalQuality, "Global Modifiers"), "FONTIN SC") + end + if (displayInstance.socketQuality or 0) > 0 then + tooltip:AddLine(fontSizeBig, formatQuality(displayInstance.socketQuality, "Socket Colour"), "FONTIN SC") + end end tooltip:AddSeparator(10) if addReq then diff --git a/src/Modules/CalcActiveSkill.lua b/src/Modules/CalcActiveSkill.lua index 9e8d7c23400..c036a5fe026 100644 --- a/src/Modules/CalcActiveSkill.lua +++ b/src/Modules/CalcActiveSkill.lua @@ -540,6 +540,10 @@ function calcs.buildActiveSkillModList(env, activeSkill) for _, supportProperty in ipairs(skillModList:Tabulate("LIST", activeSkill.skillCfg, "SupportedGemProperty")) do local value = supportProperty.value if value.keyword == "grants_active_skill" and activeSkill.activeEffect.gemData and not activeSkill.activeEffect.gemData.tags.support then + -- save quality increases for use in tooltips + if value.key == "quality" then + activeEffect.supportQuality = (activeEffect.supportQuality or 0) + value.value + end activeEffect[value.key] = activeEffect[value.key] + value.value skillModList:NewMod("GemSupport".. value.key:gsub("^%l", string.upper), "BASE", value.value, supportProperty.mod.source, #supportProperty.mod > 0 and supportProperty.mod[1] or nil) end diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 3ca6b9e2e3e..22133da7f4d 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -304,6 +304,21 @@ local function applyGemMods(effect, modList) match = false end if match then + -- save quality increases for use in tooltips + if value.key == "quality" then + local isSocketed = false + for _, tag in ipairs(mod.mod) do + if tag.type == "SocketedIn" then + isSocketed = true + break + end + end + if isSocketed then + effect.itemQuality = (effect.itemQuality or 0) + value.value + else + effect.globalQuality = (effect.globalQuality or 0) + value.value + end + end effect[value.key] = (effect[value.key] or 0) + value.value effect.gemPropertyInfo = effect.gemPropertyInfo or {} t_insert(effect.gemPropertyInfo, mod) @@ -1563,6 +1578,10 @@ function calcs.initEnv(build, mode, override, specEnv) grantedEffect = grantedEffect, level = gemInstance.level, quality = actualQuality, + globalQuality = 0, + itemQuality = 0, + supportQuality = 0, + socketQuality = gemInstance.matchesSocket and data.misc.MatchingSocketQualityBonus or 0, srcInstance = gemInstance, gemData = gemInstance.gemData, superseded = false, @@ -1635,7 +1654,10 @@ function calcs.initEnv(build, mode, override, specEnv) grantedEffect = grantedEffect, level = gemInstance.level, quality = actualQuality, - matchesSocket = gemInstance.matchesSocket, + globalQuality = 0, + itemQuality = 0, + supportQuality = 0, + socketQuality = gemInstance.matchesSocket and data.misc.MatchingSocketQualityBonus or 0, srcInstance = gemInstance, gemData = gemInstance.gemData, } From 44e78a1fb52cf96b15e1e7f215964bc065e3d9eb Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:42:40 +0300 Subject: [PATCH 07/11] Fix breakdown issues and order of operations --- src/Modules/Build.lua | 2 +- src/Modules/CalcActiveSkill.lua | 2 +- src/Modules/CalcPerform.lua | 7 ++++--- src/Modules/CalcSections.lua | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 2157fe4f91b..6ad4078eba7 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -1242,9 +1242,9 @@ function buildMode:OnFrame(inputEvents) -- Rebuild calculation output tables self.outputRevision = self.outputRevision + 1 self.buildFlag = false + self.skillsTab:UpdateSocketGroups() self.calcsTab:BuildOutput() self:RefreshStatList() - self.skillsTab:UpdateSocketGroups() self.configTab.calcFunc, self.configTab.calcBase = self.calcsTab:GetMiscCalculator(self) end if main.showThousandsSeparators ~= self.lastShowThousandsSeparators then diff --git a/src/Modules/CalcActiveSkill.lua b/src/Modules/CalcActiveSkill.lua index c036a5fe026..543bc6c6161 100644 --- a/src/Modules/CalcActiveSkill.lua +++ b/src/Modules/CalcActiveSkill.lua @@ -535,7 +535,7 @@ function calcs.buildActiveSkillModList(env, activeSkill) -- matching socket bonus local socketMatches = activeSkill.activeEffect.srcInstance and activeSkill.activeEffect.srcInstance.matchesSocket or activeSkill.activeEffect.matchesSocket if socketMatches then - skillModList:NewMod("GemQuality", "BASE", data.misc.MatchingSocketQualityBonus, "Socket Quality") + skillModList:NewMod("GemSocketQuality", "BASE", data.misc.MatchingSocketQualityBonus, "Socket Quality") end for _, supportProperty in ipairs(skillModList:Tabulate("LIST", activeSkill.skillCfg, "SupportedGemProperty")) do local value = supportProperty.value diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index e90db39f7b4..6025a7c6495 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -3890,9 +3890,10 @@ function calcs.perform(env, skipEHP) local baseQuality = mainSkill.skillModList:Sum("BASE", mainSkill.skillCfg, "GemQuality") local totalItemQuality = mainSkill.skillModList:Sum("BASE", mainSkill.skillCfg, "GemItemQuality") local totalSupportQuality = mainSkill.skillModList:Sum("BASE", mainSkill.skillCfg, "GemSupportQuality") + local socketQuality = mainSkill.skillModList:Sum("BASE", mainSkill.skillCfg, "GemSocketQuality") output.GemHasQuality = true - output.GemQuality = baseQuality + totalSupportQuality + totalItemQuality + output.GemQuality = baseQuality + totalSupportQuality + totalItemQuality + socketQuality if env.player.breakdown then env.player.breakdown.GemQuality = {} @@ -3903,8 +3904,8 @@ function calcs.perform(env, skipEHP) if totalItemQuality > 0 then t_insert(env.player.breakdown.GemQuality, s_format("+ %d ^8(quality from items)", totalItemQuality)) end - if mainSkill.activeEffect.matchesSocket then - t_insert(env.player.breakdown.GemQuality, s_format("+ %d ^8(quality from socket)", data.misc.MatchingSocketQualityBonus)) + if socketQuality > 0 then + t_insert(env.player.breakdown.GemQuality, s_format("+ %d ^8(quality from socket)", socketQuality)) end t_insert(env.player.breakdown.GemQuality, s_format("= %d", output.GemQuality)) end diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index fcc1f55f165..54f10fba550 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -630,7 +630,7 @@ return { } }, { 1, "SkillTypeStats", 1, colorCodes.OFFENCE, {{ defaultCollapsed = false, label = "Skill type-specific Stats", data = { { label = "Gem Level", haveOutput = "GemHasLevel", { format = "{0:output:GemLevel}", { breakdown = "GemLevel" }, { modName = { "GemLevel" }, cfg = "skill" },{ modName = { "GemSupportLevel" }, cfg = "skill" }, { modName = { "GemItemLevel" }, cfg = "skill" }, }, }, - { label = "Gem Quality", haveOutput = "GemHasQuality", { format = "{0:output:GemQuality}", { breakdown = "GemQuality" }, { modName = { "GemQuality" }, cfg = "skill" },{ modName = { "GemSupportQuality" }, cfg = "skill" }, { modName = { "GemItemQuality" }, cfg = "skill" }, }, }, + { label = "Gem Quality", haveOutput = "GemHasQuality", { format = "{0:output:GemQuality}", { breakdown = "GemQuality" }, { modName = { "GemQuality", "GemSupportQuality", "GemItemQuality", "GemSocketQuality" }, cfg = "skill" }, }, }, { label = "Mana Cost", color = colorCodes.MANA, haveOutput = "ManaHasCost", { format = "{0:output:ManaCost}", { breakdown = "ManaCost" }, { modName = manaCost, cfg = "skill" }, }, }, { label = "Mana % Cost", color = colorCodes.MANA, haveOutput = "ManaPercentHasCost", { format = "{0:output:ManaPercentCost}", { breakdown = "ManaPercentCost" }, { modName = manaCost, cfg = "skill" }, }, }, { label = "Mana per second", color = colorCodes.MANA, haveOutput = "ManaPerSecondHasCost", { format = "{2:output:ManaPerSecondCost}", { breakdown = "ManaPerSecondCost" }, { modName = manaCost, cfg = "skill" }, }, }, From 23ffe0dea1d0c0a71e9eef62014ad0f32bc7529c Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:52:58 +0300 Subject: [PATCH 08/11] Cleanup --- src/Classes/GemTooltip.lua | 2 +- src/Classes/Item.lua | 2 +- src/Classes/SkillsTab.lua | 21 +++++++++++---------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Classes/GemTooltip.lua b/src/Classes/GemTooltip.lua index 8fc24cdf33b..4709a99827b 100644 --- a/src/Classes/GemTooltip.lua +++ b/src/Classes/GemTooltip.lua @@ -139,7 +139,7 @@ local function addCommonGemInfo(tooltip, build, gemInstance, grantedEffect, addL end if addReq then if gemInstance.quality > 0 then - tooltip:AddLine(fontSizeBig, colorCodes.MAGIC .. string.format("^x7F7F7FQuality: +%s%d%%", colorCodes.MAGIC, gemInstance.quality), "FONTIN SC") + tooltip:AddLine(fontSizeBig, string.format("^x7F7F7FQuality: +%s%d%%", colorCodes.MAGIC, gemInstance.quality), "FONTIN SC") end local function formatQuality(number, suffix) return colorCodes.MAGIC .. string.format("+%d%% Quality from %s", number, suffix) diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index cee2dc7831e..1511469c22f 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -2380,7 +2380,7 @@ function ItemClass:BuildModList() end self.sockets = newSockets end - if calcLocal(baseList, "SocketAlwaysMatches", "FLAG", 0) then + if self.sockets and calcLocal(baseList, "SocketAlwaysMatches", "FLAG", 0) then self.sockets.colourAlwaysMatches = true end self.socketedJewelEffectModifier = 1 + calcLocal(baseList, "SocketedJewelEffect", "INC", 0) / 100 diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 5e51c2272b8..822b548e46e 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -214,7 +214,7 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont local function getSelectedItem() local item local groupSlot = self.controls.groupSlot:GetSelValue() - if groupSlot.slotName then + if groupSlot and groupSlot.slotName then local slot = self.build.itemsTab.slots[groupSlot.slotName] if slot then item = self.build.itemsTab.items[slot.selItemId] @@ -245,7 +245,6 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont return end - -- save count of abyssal sockets local abyssalSocketCount = 0 for _, socket in ipairs(item.sockets) do @@ -259,12 +258,14 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont local maxSockets = (item.base.socketLimit or 0) - abyssalSocketCount for _, group in ipairs(self.socketGroupList) do local colours = { "R", "G", "B" } - if maxSockets > 0 and group.slot == groupSlot.slotName then + if group.slot == groupSlot.slotName then for _, gem in ipairs(group.gemList) do - local grantedEffect = gem.grantedEffect or gem.gemData.grantedEffect - local gemColour = grantedEffect.color and colours[grantedEffect.color] or "W" - table.insert(item.sockets, { color = gemColour, group = groupCount }) - maxSockets = maxSockets - 1 + local grantedEffect = gem.grantedEffect or (gem.gemData and gem.gemData.grantedEffect) + if grantedEffect and maxSockets > 0 then + local gemColour = grantedEffect.color and colours[grantedEffect.color] or "W" + table.insert(item.sockets, { color = gemColour, group = groupCount }) + maxSockets = maxSockets - 1 + end end groupCount = groupCount + 1 end @@ -279,7 +280,7 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont end) self.controls.optimiseSockets.shown = function() local item = getSelectedItem() - return item and item.base.socketLimit + return item and (item.base.socketLimit ~= nil) end self.controls.optimiseSockets.tooltipText = "Rebuild the item's sockets to match the groups assigned to it." -- self.imbuedSupportBySlot is used by CalcSetup to add an ExtraSupport mod of the selected gem @@ -1126,7 +1127,7 @@ function SkillsTabClass:ProcessGemLevel(gemData, imbued) end -- Processes the given socket group, filling in information that will be used for display or calculations ----@param socketGroup table[] +---@param socketGroup table function SkillsTabClass:ProcessSocketGroup(socketGroup) -- Loop through the skill gem list local data = self.build.data @@ -1205,7 +1206,7 @@ end -- reprocess socket groups on rebuild function SkillsTabClass:UpdateSocketGroups() local slotSocketedCounts = {} - for _, socketGroup in ipairs(self.skillSets[self.activeSkillSetId].socketGroupList) do + for _, socketGroup in ipairs(self.socketGroupList) do if socketGroup.slot then local gemOffset = (slotSocketedCounts[socketGroup.slot] or 0) for i, gemInstance in ipairs(socketGroup.gemList) do From 373ee790015b3b01be02bd578b523bc18e28ba89 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:46:30 +0300 Subject: [PATCH 09/11] Add a bunch of tests --- spec/System/TestGemSocketQuality_spec.lua | 229 ++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 spec/System/TestGemSocketQuality_spec.lua diff --git a/spec/System/TestGemSocketQuality_spec.lua b/spec/System/TestGemSocketQuality_spec.lua new file mode 100644 index 00000000000..b01efb266f2 --- /dev/null +++ b/spec/System/TestGemSocketQuality_spec.lua @@ -0,0 +1,229 @@ +describe("TestGemSocketQuality", function() + before_each(function() + newBuild() + end) + + -- 3 blue socket body armour + local function equipBody(sockets, extraLines) + build.itemsTab:CreateDisplayItemFromRaw( + "Rarity: RARE\nTest Robe\nSage's Robe\nQuality: 0\nSockets: " .. sockets .. "\nImplicits: 0\n" .. (extraLines or "")) + build.itemsTab:AddDisplayItem() + end + + -- get specific socketgroup linked to a slot + local function groupForSlot(slotName, index) + local seen = 0 + for _, group in ipairs(build.skillsTab.socketGroupList) do + if group.slot == slotName then + seen = seen + 1 + if seen == (index or 1) then + return group + end + end + end + end + + it("grants +10% quality to a gem in a matching colour socket", function() + equipBody("B-B-B") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\n") + runCallback("OnFrame") + + local group = groupForSlot("Body Armour") + assert.is_true(group.gemList[1].matchesSocket) + assert.are.equals(10, build.calcsTab.mainOutput.GemQuality) + -- The bonus must reach the calculations, not just the displayed number: + -- activeEffect.quality is the value fed to buildSkillInstanceStats. + assert.are.equals(10, build.calcsTab.mainEnv.player.mainSkill.activeEffect.quality) + end) + + it("adds the socket bonus on top of the gem's own quality", function() + equipBody("B-B-B") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/5 1\n") + runCallback("OnFrame") + + assert.are.equals(15, build.calcsTab.mainOutput.GemQuality) + end) + + it("grants no bonus to a gem in a mismatched colour socket", function() + equipBody("R-W-R") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\nFireball 20/0 1\n") + runCallback("OnFrame") + + local group = groupForSlot("Body Armour") + assert.is_false(group.gemList[1].matchesSocket) + assert.is_false(group.gemList[2].matchesSocket) + assert.are.equals(0, build.calcsTab.mainOutput.GemQuality) + assert.are.equals(0, build.calcsTab.mainEnv.player.mainSkill.activeEffect.quality) + end) + + it("applies the socket quality bonus to the skill's quality-scaled stats", function() + equipBody("B-B-B") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\n") + -- Fireball aoe explosion + build.calcsTab.input.skillPart = 2 + runCallback("OnFrame") + assert.are.equals(10, build.calcsTab.mainEnv.player.mainSkill.activeEffect.quality) + local matchedRadius = build.calcsTab.mainOutput.AreaOfEffectRadius + + newBuild() + equipBody("R-R-R") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\n") + build.calcsTab.input.skillPart = 2 + runCallback("OnFrame") + assert.are.equals(0, build.calcsTab.mainEnv.player.mainSkill.activeEffect.quality) + local unmatchedRadius = build.calcsTab.mainOutput.AreaOfEffectRadius + + assert.is_true(matchedRadius > unmatchedRadius) + end) + + it("grants no bonus to a white/colourless gem", function() + equipBody("W-W-W") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nPortal 20/0 1\n") + runCallback("OnFrame") + + local group = groupForSlot("Body Armour") + assert.is_false(group.gemList[1].matchesSocket) + end) + + it("adds quality when an item is equipped and removes it when unequipped", function() + equipBody("B-B-B") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\n") + runCallback("OnFrame") + assert.are.equals(10, build.calcsTab.mainOutput.GemQuality) + + -- Unequip the body armour + build.itemsTab.slots["Body Armour"]:SetSelItemId(0) + build.buildFlag = true + runCallback("OnFrame") + + local group = groupForSlot("Body Armour") + assert.is_false(group.gemList[1].matchesSocket) + assert.are.equals(0, build.calcsTab.mainOutput.GemQuality) + end) + + it("always grants the bonus with Dialla's 'always matches' mod", function() + -- Blue gem in a red socket + equipBody("R-R-R", "Gems Socketed always have the Quality bonus from Socket Colour\n") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\n") + runCallback("OnFrame") + + local item = build.itemsTab.items[build.itemsTab.slots["Body Armour"].selItemId] + assert.is_true(item.sockets.colourAlwaysMatches) + + local group = groupForSlot("Body Armour") + assert.is_true(group.gemList[1].matchesSocket) + assert.are.equals(10, build.calcsTab.mainOutput.GemQuality) + assert.are.equals(10, build.calcsTab.mainEnv.player.mainSkill.activeEffect.quality) + end) + + it("respects socket order across split groups on one item", function() + -- Sockets are Blue then Red. The first group's gem lines up with the + -- blue socket, the second group's gem continues at the red socket + equipBody("B-R") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\n") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nAbsolution 20/0 1\n") + runCallback("OnFrame") + + local blueGroup = groupForSlot("Body Armour", 1) + local redGroup = groupForSlot("Body Armour", 2) + -- Fireball (blue) matches socket 1 + assert.is_true(blueGroup.gemList[1].matchesSocket) + -- Absolution (red) only matches if the offset continues at socket 2 + assert.is_true(redGroup.gemList[1].matchesSocket) + end) + + it("does not grant the bonus when split order breaks the match", function() + -- Both sockets blue: the red gem in the second group should not match + equipBody("R-B") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\n") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nAbsolution 20/0 1\n") + runCallback("OnFrame") + + assert.is_false(groupForSlot("Body Armour", 1).gemList[1].matchesSocket) + assert.is_false(groupForSlot("Body Armour", 2).gemList[1].matchesSocket) + end) + + it("Optimise Sockets recolours an item to match its assigned gems", function() + equipBody("R-R-R-R") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\nAbsolution 20/0 1\n") + runCallback("OnFrame") + + build.skillsTab.controls.optimiseSockets.onClick() + runCallback("OnFrame") + + local item = build.itemsTab.items[build.itemsTab.slots["Body Armour"].selItemId] + assert.are.equals(2, #item.sockets) + assert.are.equals("B", item.sockets[1].color) + assert.are.equals("R", item.sockets[2].color) + + local group = groupForSlot("Body Armour") + assert.is_true(group.gemList[1].matchesSocket) + assert.is_true(group.gemList[2].matchesSocket) + end) + + it("Optimise Sockets does not exceed the item's socket limit", function() + equipBody("R") + -- Assign more gems than the base's 6 socket limit + build.skillsTab:PasteSocketGroup("Slot: Body Armour\n" .. + ("Fireball 20/0 1\n"):rep(8)) + runCallback("OnFrame") + + build.skillsTab.controls.optimiseSockets.onClick() + runCallback("OnFrame") + + local item = build.itemsTab.items[build.itemsTab.slots["Body Armour"].selItemId] + assert.is_true(#item.sockets <= item.base.socketLimit) + assert.are.equals(6, #item.sockets) + end) + it("Optimise Sockets preserves abyssal sockets and stays within the limit", function() + equipBody("A-R-R") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\nAbsolution 20/0 1\n") + runCallback("OnFrame") + + build.skillsTab.controls.optimiseSockets.onClick() + runCallback("OnFrame") + + local item = build.itemsTab.items[build.itemsTab.slots["Body Armour"].selItemId] + local abyssal = 0 + for _, socket in ipairs(item.sockets) do + if socket.color == "A" then + abyssal = abyssal + 1 + end + end + assert.are.equals(1, abyssal) + assert.is_true(#item.sockets <= item.base.socketLimit) + end) + + -- find controlled destruction increased damage mod + local function supportQualityDamageMod() + local mainSkill = build.calcsTab.mainEnv.player.mainSkill + for _, entry in ipairs(mainSkill.skillModList:Tabulate("INC", mainSkill.skillCfg, "Damage")) do + if entry.mod.source == "Skill:SupportControlledDestruction" then + return entry.mod + end + end + end + + it("applies the socket bonus to a support gem's effective quality", function() + equipBody("B-B") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\nControlled Destruction 20/0 1\n") + runCallback("OnFrame") + + local group = groupForSlot("Body Armour") + assert.is_true(group.gemList[2].matchesSocket) + + -- check that the actual 5% damage mod exists + local mod = supportQualityDamageMod() + assert.is_not_nil(mod) + assert.are.equals(5, mod.value) + + -- and check that the mod is gone without the socket quality bonus + newBuild() + equipBody("R-R") + build.skillsTab:PasteSocketGroup("Slot: Body Armour\nFireball 20/0 1\nControlled Destruction 20/0 1\n") + runCallback("OnFrame") + + assert.is_false(groupForSlot("Body Armour").gemList[2].matchesSocket) + assert.is_nil(supportQualityDamageMod()) + end) +end) From 39978fdbc6597da75588dddf7a4e2c46405983c2 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:22:22 +0300 Subject: [PATCH 10/11] Fix typo --- spec/System/TestGemSocketQuality_spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/System/TestGemSocketQuality_spec.lua b/spec/System/TestGemSocketQuality_spec.lua index b01efb266f2..719004b1767 100644 --- a/spec/System/TestGemSocketQuality_spec.lua +++ b/spec/System/TestGemSocketQuality_spec.lua @@ -10,7 +10,7 @@ describe("TestGemSocketQuality", function() build.itemsTab:AddDisplayItem() end - -- get specific socketgroup linked to a slot + -- get specific socket group linked to a slot local function groupForSlot(slotName, index) local seen = 0 for _, group in ipairs(build.skillsTab.socketGroupList) do From d218aa297d1041adf42fbfe3804280851aa459b0 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Tue, 4 Aug 2026 16:17:46 +1000 Subject: [PATCH 11/11] Minor fixes Clicking optimise socket now creates an undo state Optimise now calls buildflag so the stats update on gems when clicking Now resets the socket colours when changing the item the sockets use --- src/Classes/SkillsTab.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 822b548e46e..5793c11ca24 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -245,6 +245,8 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont return end + self.build.itemsTab:AddUndoState() + -- save count of abyssal sockets local abyssalSocketCount = 0 for _, socket in ipairs(item.sockets) do @@ -277,6 +279,7 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont end item:BuildAndParseRaw() self:UpdateSocketGroups() + self.build.buildFlag = true end) self.controls.optimiseSockets.shown = function() local item = getSelectedItem() @@ -1207,10 +1210,13 @@ end function SkillsTabClass:UpdateSocketGroups() local slotSocketedCounts = {} for _, socketGroup in ipairs(self.socketGroupList) do + -- Clear stale matches when a group is no longer assigned to an item. + for _, gemInstance in ipairs(socketGroup.gemList) do + gemInstance.matchesSocket = false + end if socketGroup.slot then local gemOffset = (slotSocketedCounts[socketGroup.slot] or 0) for i, gemInstance in ipairs(socketGroup.gemList) do - gemInstance.matchesSocket = false -- add quality for matching sockets by looking up linked item if (gemInstance.grantedEffect or gemInstance.gemData) then local grantedEffect = gemInstance.grantedEffect or gemInstance.gemData.grantedEffect