diff --git a/spec/System/TestGemSocketQuality_spec.lua b/spec/System/TestGemSocketQuality_spec.lua new file mode 100644 index 00000000000..719004b1767 --- /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 socket group 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) diff --git a/src/Classes/GemTooltip.lua b/src/Classes/GemTooltip.lua index 5100615c21f..4709a99827b 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, 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/Classes/ImportTab.lua b/src/Classes/ImportTab.lua index 21e7d2adfaf..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() @@ -1839,12 +1840,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) diff --git a/src/Classes/Item.lua b/src/Classes/Item.lua index 1274c4a401c..4b2f6645c76 100644 --- a/src/Classes/Item.lua +++ b/src/Classes/Item.lua @@ -1923,7 +1923,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 " ") @@ -2586,6 +2586,9 @@ function ItemClass:BuildModList() end self.sockets = newSockets end + if self.sockets and 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/ItemsTab.lua b/src/Classes/ItemsTab.lua index 2633489f738..972f6443c46 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -4198,6 +4198,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 @@ -4430,32 +4459,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 8c4644c701e..5793c11ca24 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -211,13 +211,88 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont self.build.buildFlag = true end) + local function getSelectedItem() + local item + local groupSlot = self.controls.groupSlot:GetSelValue() + if groupSlot and groupSlot.slotName then + local slot = self.build.itemsTab.slots[groupSlot.slotName] + if slot then + item = self.build.itemsTab.items[slot.selItemId] + if not item then + return + end + end + end + 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 + + self.build.itemsTab:AddUndoState() + + -- 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 group.slot == groupSlot.slotName then + for _, gem in ipairs(group.gemList) do + 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 + end + + for _ = 0, abyssalSocketCount - 1 do + groupCount = groupCount + 1 + table.insert(item.sockets, { color = "A", group = groupCount }) + end + item:BuildAndParseRaw() + self:UpdateSocketGroups() + self.build.buildFlag = true + end) + self.controls.optimiseSockets.shown = function() + local item = getSelectedItem() + 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 -- 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", 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 @@ -254,7 +329,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 @@ -266,7 +342,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 +354,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 +400,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:") @@ -654,6 +730,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 @@ -772,6 +849,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 @@ -1044,6 +1130,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 function SkillsTabClass:ProcessSocketGroup(socketGroup) -- Loop through the skill gem list local data = self.build.data @@ -1119,6 +1206,44 @@ function SkillsTabClass:ProcessSocketGroup(socketGroup) end end +-- reprocess socket groups on rebuild +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 + -- 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 + end + slotSocketedCounts[socketGroup.slot] = gemOffset + #socketGroup.gemList + end + end +end -- Set the skill to be displayed/edited function SkillsTabClass:SetDisplayGroup(socketGroup) self.displayGroup = socketGroup diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index d26e46498e3..f53e9f7cd71 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -9250,8 +9250,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/Build.lua b/src/Modules/Build.lua index 69398829e83..6ad4078eba7 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 @@ -1240,6 +1242,7 @@ 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.configTab.calcFunc, self.configTab.calcBase = self.calcsTab:GetMiscCalculator(self) diff --git a/src/Modules/CalcActiveSkill.lua b/src/Modules/CalcActiveSkill.lua index cf9fa5e04f0..543bc6c6161 100644 --- a/src/Modules/CalcActiveSkill.lua +++ b/src/Modules/CalcActiveSkill.lua @@ -532,9 +532,18 @@ 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("GemSocketQuality", "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 + -- 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/CalcPerform.lua b/src/Modules/CalcPerform.lua index 73025efe922..9217e3b8d76 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -3891,9 +3891,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 = {} @@ -3904,6 +3905,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 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 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" }, }, }, diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 24e28a4d115..4c7561e616d 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) @@ -1560,10 +1575,15 @@ 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, + globalQuality = 0, + itemQuality = 0, + supportQuality = 0, + socketQuality = gemInstance.matchesSocket and data.misc.MatchingSocketQualityBonus or 0, srcInstance = gemInstance, gemData = gemInstance.gemData, superseded = false, @@ -1631,10 +1651,15 @@ 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, + globalQuality = 0, + itemQuality = 0, + supportQuality = 0, + socketQuality = gemInstance.matchesSocket and data.misc.MatchingSocketQualityBonus or 0, srcInstance = gemInstance, gemData = gemInstance.gemData, } diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index 10459584341..8b0dd2259a3 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -294,6 +294,7 @@ data.misc = { -- magic numbers PvpElemental2 = 150, PvpNonElemental1 = 0.57, PvpNonElemental2 = 90, + MatchingSocketQualityBonus = 10, } data.skillColorMap = { colorCodes.STRENGTH, colorCodes.DEXTERITY, colorCodes.INTELLIGENCE, colorCodes.NORMAL } diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index c3135807d27..5a15ec5054c 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -3083,6 +3083,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. },