From ec31c6acbc4577867cf7078f54c6ea4634369beb Mon Sep 17 00:00:00 2001 From: unrealdreamz <132005717+unrealdreamz@users.noreply.github.com> Date: Mon, 18 May 2026 19:19:34 -0400 Subject: [PATCH 1/3] Allow fractional skill counts --- spec/System/TestSkills_spec.lua | 21 ++++++++++++++++++++- src/Classes/SkillsTab.lua | 25 ++++++++++++++++--------- src/Modules/Build.lua | 18 ++++++++++++------ 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index 3afa41ce06..b6287e3e68 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -72,6 +72,25 @@ describe("TestSkills", function() assert.True(math.abs(finalCost - 8.67) < 0.1) -- floor(9 * 1.5) / 1.5 end) + it("Fractional skill count scales Full DPS", function() + build.skillsTab:PasteSocketGroup("Ball Lightning 20/0 1") + build.skillsTab.socketGroupList[1].includeInFullDPS = true + runCallback("OnFrame") + + local fullDPS = build.calcsTab.mainOutput.FullDPS + assert.truthy(fullDPS) + assert.True(fullDPS > 0) + + newBuild() + + build.skillsTab:PasteSocketGroup("Ball Lightning 20/0 0.5") + build.skillsTab.socketGroupList[1].includeInFullDPS = true + runCallback("OnFrame") + + assert.are.equals(0.5, build.skillsTab.socketGroupList[1].gemList[1].count) + assert.True(math.abs(build.calcsTab.mainOutput.FullDPS - fullDPS * 0.5) < fullDPS * 0.001) + end) + it("Test mana cost efficiency with support gems", function() -- Test interaction between cost efficiency and cost multipliers build.skillsTab:PasteSocketGroup("Contagion 6/0 1\nMagnified Area I 1/0 1") @@ -245,4 +264,4 @@ describe("TestSkills", function() assert.True(build.calcsTab.calcsOutput.Cooldown == 10) end) -end) \ No newline at end of file +end) diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index c3cc16115d..95cfc4e4fd 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -70,6 +70,14 @@ local sortGemTypeList = { { label = "Effective Hit Pool", type = "TotalEHP" }, } +local function parseSkillCount(buf) + return tonumber(buf) or 1 +end + +local function formatSkillCount(count) + return string.format("%g", count or 1) +end + local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Control", function(self, build) self.UndoHandler() self.ControlHost() @@ -193,8 +201,8 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont self.controls.groupCountLabel.shown = function() return self.displayGroup.source ~= nil end - self.controls.groupCount = new("EditControl", { "LEFT", self.controls.groupCountLabel, "RIGHT" }, { 4, 0, 60, 20 }, nil, nil, "%D", 2, function(buf) - self.displayGroup.groupCount = tonumber(buf) or 1 + self.controls.groupCount = new("EditControl", { "LEFT", self.controls.groupCountLabel, "RIGHT" }, { 4, 0, 60, 20 }, nil, nil, "^%d.", 6, function(buf) + self.displayGroup.groupCount = parseSkillCount(buf) self:AddUndoState() self.build.buildFlag = true end) @@ -568,7 +576,7 @@ function SkillsTabClass:CopySocketGroup(socketGroup) skillText = skillText .. "Slot: " .. socketGroup.slot .. "\r\n" end for _, gemInstance in ipairs(socketGroup.gemList) do - skillText = skillText .. string.format("%s %d/%d %s %d\r\n", gemInstance.nameSpec, gemInstance.level, gemInstance.quality, gemInstance.enabled and "" or "DISABLED", gemInstance.count or 1) + skillText = skillText .. string.format("%s %d/%d %s %s\r\n", gemInstance.nameSpec, gemInstance.level, gemInstance.quality, gemInstance.enabled and "" or "DISABLED", formatSkillCount(gemInstance.count)) end Copy(skillText) end @@ -585,13 +593,13 @@ function SkillsTabClass:PasteSocketGroup(testInput) if slot then newGroup.slot = slot end - for nameSpec, level, quality, state, count in skillText:gmatch("([ %a']+) (%d+)/(%d+) ?(%a*) (%d+)") do + for nameSpec, level, quality, state, count in skillText:gmatch("([ %a']+) (%d+)/(%d+) ?(%a*) ([%d%.]+)") do t_insert(newGroup.gemList, { nameSpec = nameSpec, level = tonumber(level) or 20, quality = tonumber(quality) or 0, enabled = state ~= "DISABLED", - count = tonumber(count) or 1, + count = parseSkillCount(count), enableGlobal1 = true, enableGlobal2 = true }) @@ -853,7 +861,7 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."Enable"] = slot.enabled -- Count gem - slot.count = new("EditControl", {"LEFT",slot.enabled,"RIGHT"}, {18, 0, 60, 20}, nil, nil, "%D", 2, function(buf) + slot.count = new("EditControl", {"LEFT",slot.enabled,"RIGHT"}, {18, 0, 60, 20}, nil, nil, "^%d.", 6, function(buf) local gemInstance = self.displayGroup.gemList[index] if not gemInstance then gemInstance = { nameSpec = "", level = self.defaultGemLevel or 20, quality = self.defaultGemQuality or 0, enabled = true, enableGlobal1 = true, count = 1, new = true } @@ -863,8 +871,7 @@ function SkillsTabClass:CreateGemSlot(index) slot.enabled.state = true slot.enableGlobal1.state = true end - gemInstance.count = tonumber(buf) or 1 - slot.count.buf = tostring(gemInstance.count) + gemInstance.count = parseSkillCount(buf) self:ProcessSocketGroup(self.displayGroup) self:AddUndoState() self.build.buildFlag = true @@ -883,7 +890,7 @@ function SkillsTabClass:CreateGemSlot(index) end slot.count.tooltipFunc = function(tooltip) if tooltip:CheckForUpdate(self.build.outputRevision, self.displayGroup) then - tooltip:AddLine(16, "^8Note: `count` integer value scales the DPS of associated skill by a scalar.") + tooltip:AddLine(16, "^8Note: `count` numeric value scales the DPS of associated skill by a scalar.") tooltip:AddLine(16, "^8To be used with totems, minions, shot-gunning of projectiles (e.g., VD, magma-orbs),") tooltip:AddLine(16, "^8multi-hit projectiles (e.g. ball-lightning), traps, mines.") end diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index fca8789b4f..54d3733e02 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -26,6 +26,10 @@ local function InsertIfNew(t, val) table.insert(t, val) end +local function formatSkillCount(count) + return s_format("%g", count or 1) +end + function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLink) self.dbFileName = dbFileName self.buildName = buildName @@ -1007,11 +1011,12 @@ function buildMode:Save(xml) if skillData.trigger and skillData.trigger ~= "" then triggerStr = skillData.trigger end + local skillCount = skillData.count or 1 local lhsString = skillData.name - if skillData.count >= 2 then - lhsString = tostring(skillData.count).."x "..skillData.name + if skillCount ~= 1 then + lhsString = formatSkillCount(skillCount).."x "..skillData.name end - t_insert(xml, { elem = "FullDPSSkill", attrib = { stat = lhsString, value = tostring(skillData.dps * skillData.count), skillPart = skillData.skillPart or "", source = skillData.source or skillData.trigger or "" } }) + t_insert(xml, { elem = "FullDPSSkill", attrib = { stat = lhsString, value = tostring(skillData.dps * skillCount), skillPart = skillData.skillPart or "", source = skillData.source or skillData.trigger or "" } }) end addedStatNames[statName] = true else @@ -1924,14 +1929,15 @@ function buildMode:AddDisplayStatList(statList, actor) if skillData.trigger and skillData.trigger ~= "" then triggerStr = colorCodes.WARNING.." ("..skillData.trigger..")"..labelColor end + local skillCount = skillData.count or 1 local lhsString = labelColor..skillData.name..triggerStr..":" - if skillData.count >= 2 then - lhsString = labelColor..tostring(skillData.count).."x "..skillData.name..triggerStr..":" + if skillCount ~= 1 then + lhsString = labelColor..formatSkillCount(skillCount).."x "..skillData.name..triggerStr..":" end t_insert(statBoxList, { height = 16, lhsString, - self:FormatStat({fmt = "1.f"}, skillData.dps * skillData.count, overCapStatVal), + self:FormatStat({fmt = "1.f"}, skillData.dps * skillCount, overCapStatVal), }) if skillData.skillPart then t_insert(statBoxList, { From 8bf165e846dbc2a8713a4198cd2165df3d01eee9 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 22 May 2026 17:35:28 +1000 Subject: [PATCH 2/3] Fix box to be wider and use existing +- buttons --- src/Classes/EditControl.lua | 2 +- src/Classes/SkillsTab.lua | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index 2964057adf..dd14221001 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -61,7 +61,7 @@ local EditClass = newClass("EditControl", "ControlHost", "Control", "UndoHandler local _, height = self:GetSize() return height - 4 end - if self.filter == "%D" or self.filter == "^%-%d" then + if self.filter == "%D" or self.filter == "^%-%d" or self.filter == "^%d." then -- Add +/- buttons for integer number edits self.isNumeric = true self.controls.buttonDown = new("ButtonControl", {"RIGHT",self,"RIGHT"}, {-2, 0, buttonSize, buttonSize}, "-", function() diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 95cfc4e4fd..9aa42aaf73 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -201,7 +201,7 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont self.controls.groupCountLabel.shown = function() return self.displayGroup.source ~= nil end - self.controls.groupCount = new("EditControl", { "LEFT", self.controls.groupCountLabel, "RIGHT" }, { 4, 0, 60, 20 }, nil, nil, "^%d.", 6, function(buf) + self.controls.groupCount = new("EditControl", { "LEFT", self.controls.groupCountLabel, "RIGHT" }, { 4, 0, 80, 20 }, nil, nil, "^%d.", 6, function(buf) self.displayGroup.groupCount = parseSkillCount(buf) self:AddUndoState() self.build.buildFlag = true @@ -262,7 +262,7 @@ will automatically apply to the skill.]] self.controls.gemLevelHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].level, "TOPLEFT"}, {0, -2, 0, 16}, "^7Level:") self.controls.gemQualityHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].quality, "TOPLEFT"}, {0, -2, 0, 16}, "^7Quality:") self.controls.gemEnableHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].enabled, "TOPLEFT"}, {-16, -2, 0, 16}, "^7Enabled:") - self.controls.gemCountHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].count, "TOPLEFT"}, {8, -2, 0, 16}, "^7Count:") + self.controls.gemCountHeader = new("LabelControl", {"BOTTOMLEFT", self.gemSlots[1].count, "TOPLEFT"}, {18, -2, 0, 16}, "^7Count:") end) function SkillsTabClass:LoadSkill(node, skillSetId) @@ -514,7 +514,7 @@ function SkillsTabClass:Draw(viewPort, inputEvents) self.controls.scrollBarH.y = viewPort.y + viewPort.height - 18 do - local maxX = self.controls.gemCountHeader:GetPos() + self.controls.gemCountHeader:GetSize() + 15 + local maxX = self.controls.gemCountHeader:GetPos() + self.controls.gemCountHeader:GetSize() + 25 local contentWidth = maxX - self.x self.controls.scrollBarH:SetContentDimension(contentWidth, viewPort.width) end @@ -861,7 +861,7 @@ function SkillsTabClass:CreateGemSlot(index) self.controls["gemSlot"..index.."Enable"] = slot.enabled -- Count gem - slot.count = new("EditControl", {"LEFT",slot.enabled,"RIGHT"}, {18, 0, 60, 20}, nil, nil, "^%d.", 6, function(buf) + slot.count = new("EditControl", {"LEFT",slot.enabled,"RIGHT"}, {18, 0, 80, 20}, nil, nil, "^%d.", 5, function(buf) local gemInstance = self.displayGroup.gemList[index] if not gemInstance then gemInstance = { nameSpec = "", level = self.defaultGemLevel or 20, quality = self.defaultGemQuality or 0, enabled = true, enableGlobal1 = true, count = 1, new = true } From ece14470c4b98768812a0120efa244bf05fe2516 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 22 May 2026 20:20:49 +1000 Subject: [PATCH 3/3] Reduce diff + fix value below 0 --- src/Classes/EditControl.lua | 8 ++++++-- src/Classes/SkillsTab.lua | 16 ++++------------ src/Modules/Build.lua | 8 ++------ 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index dd14221001..ca597de711 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -686,8 +686,12 @@ function EditClass:OnKeyUp(key) end end elseif key == "WHEELDOWN" or key == "DOWN" then - if cur and (self.filter ~= "%D" or cur > 0)then - self:SetText(tostring(cur - (self.numberInc or 1)), true) + if cur then + local value = cur - (self.numberInc or 1) + if self.filter == "%D" or self.filter == "^%d." then + value = m_max(value, 0) + end + self:SetText(tostring(value), true) else if self.placeholder then self:SetText(tostring((tonumber(self.placeholder) or 0) - (self.numberInc or 1)), true) diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 022697b6a4..8425f00d95 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -70,14 +70,6 @@ local sortGemTypeList = { { label = "Effective Hit Pool", type = "TotalEHP" }, } -local function parseSkillCount(buf) - return tonumber(buf) or 1 -end - -local function formatSkillCount(count) - return string.format("%g", count or 1) -end - local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Control", function(self, build) self.UndoHandler() self.ControlHost() @@ -202,7 +194,7 @@ local SkillsTabClass = newClass("SkillsTab", "UndoHandler", "ControlHost", "Cont return self.displayGroup.source ~= nil end self.controls.groupCount = new("EditControl", { "LEFT", self.controls.groupCountLabel, "RIGHT" }, { 4, 0, 80, 20 }, nil, nil, "^%d.", 6, function(buf) - self.displayGroup.groupCount = parseSkillCount(buf) + self.displayGroup.groupCount = tonumber(buf) or 1 self:AddUndoState() self.build.buildFlag = true end) @@ -576,7 +568,7 @@ function SkillsTabClass:CopySocketGroup(socketGroup) skillText = skillText .. "Slot: " .. socketGroup.slot .. "\r\n" end for _, gemInstance in ipairs(socketGroup.gemList) do - skillText = skillText .. string.format("%s %d/%d %s %s\r\n", gemInstance.nameSpec, gemInstance.level, gemInstance.quality, gemInstance.enabled and "" or "DISABLED", formatSkillCount(gemInstance.count)) + skillText = skillText .. string.format("%s %d/%d %s %s\r\n", gemInstance.nameSpec, gemInstance.level, gemInstance.quality, gemInstance.enabled and "" or "DISABLED", string.format("%g", gemInstance.count or 1)) end Copy(skillText) end @@ -599,7 +591,7 @@ function SkillsTabClass:PasteSocketGroup(testInput) level = tonumber(level) or 20, quality = tonumber(quality) or 0, enabled = state ~= "DISABLED", - count = parseSkillCount(count), + count = tonumber(count) or 1, enableGlobal1 = true, enableGlobal2 = true }) @@ -871,7 +863,7 @@ function SkillsTabClass:CreateGemSlot(index) slot.enabled.state = true slot.enableGlobal1.state = true end - gemInstance.count = parseSkillCount(buf) + gemInstance.count = tonumber(buf) or 1 self:ProcessSocketGroup(self.displayGroup) self:AddUndoState() self.build.buildFlag = true diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 9a259f24a5..8ab26d85e0 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -26,10 +26,6 @@ local function InsertIfNew(t, val) table.insert(t, val) end -local function formatSkillCount(count) - return s_format("%g", count or 1) -end - function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLink) self.dbFileName = dbFileName self.buildName = buildName @@ -1014,7 +1010,7 @@ function buildMode:Save(xml) local skillCount = skillData.count or 1 local lhsString = skillData.name if skillCount ~= 1 then - lhsString = formatSkillCount(skillCount).."x "..skillData.name + lhsString = s_format("%g", skillCount).."x "..skillData.name end t_insert(xml, { elem = "FullDPSSkill", attrib = { stat = lhsString, value = tostring(skillData.dps * skillCount), skillPart = skillData.skillPart or "", source = skillData.source or skillData.trigger or "" } }) end @@ -1936,7 +1932,7 @@ function buildMode:AddDisplayStatList(statList, actor) local skillCount = skillData.count or 1 local lhsString = labelColor..skillData.name..triggerStr..":" if skillCount ~= 1 then - lhsString = labelColor..formatSkillCount(skillCount).."x "..skillData.name..triggerStr..":" + lhsString = labelColor..s_format("%g", skillCount).."x "..skillData.name..triggerStr..":" end t_insert(statBoxList, { height = 16,