From 95518a80c716d623038ec6d01e5c6f1f0c47609f Mon Sep 17 00:00:00 2001 From: LocalIdentity <31035929+LocalIdentity@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:24:55 +1000 Subject: [PATCH 1/2] Fix rounding of scaled conversion percentages The game rounds every scaled conversion destination to a whole percentage point before summing the converted amount. e.g. Three 40% conversions become 33%, 33%, and 33% instead of the 33.33% PoB currently uses --- spec/System/TestOffence_spec.lua | 17 +++++++++++++++++ src/Modules/CalcOffence.lua | 10 +++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index d93047e8647..4cbc4846afc 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -50,6 +50,23 @@ describe("TestOffence", function() assert.are.equals(damageWithoutArrowMod, build.calcsTab.mainOutput.AverageDamage) end) + it("rounds each scaled damage conversion to a whole percent", function() + build.skillsTab:PasteSocketGroup("Fireball 20/0 1") + build.configTab.input.customMods = [[ + 40% of Physical Damage Converted to Lightning Damage + 40% of Physical Damage Converted to Cold Damage + 40% of Physical Damage Converted to Fire Damage + ]] + build.configTab:BuildModList() + runCallback("OnFrame") + + local conversion = build.calcsTab.mainEnv.player.mainSkill.conversionTable.Physical + assert.are.equals(0.33, conversion.conversion.Lightning) + assert.are.equals(0.33, conversion.conversion.Cold) + assert.are.equals(0.33, conversion.conversion.Fire) + assert.is_true(math.abs(conversion.mult - 0.01) < 0.000001) + end) + it("parses more/less/increased/reduced minimum and maximum damage of every type", function() build.itemsTab:CreateDisplayItemFromRaw([[ New Item diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 3fd3f028e3a..90057bffb31 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -1911,12 +1911,16 @@ function calcs.offence(env, actor, activeSkill) globalTotal = globalTotal * factor end local dmgTable = { conversion = { }, gain = { } } + local convertedTotal = 0 for type in pairs(globalConv) do - dmgTable.conversion[type] = (globalConv[type] + skillConv[type]) / 100 + -- The game stores each scaled conversion destination as a whole percent. + local conversion = round(globalConv[type] + skillConv[type]) + dmgTable.conversion[type] = conversion / 100 dmgTable.gain[type] = add[type] / 100 - dmgTable[type] = (globalConv[type] + skillConv[type] + add[type]) / 100 + dmgTable[type] = (conversion + add[type]) / 100 + convertedTotal = convertedTotal + conversion end - dmgTable.mult = 1 - m_min((globalTotal + skillTotal) / 100, 1) + dmgTable.mult = 1 - m_min(convertedTotal / 100, 1) conversionTable[damageType] = dmgTable end conversionTable["Chaos"] = { mult = 1 } From 46484a99b6a46427e52dad24645256bfcfd5115f Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Fri, 21 Aug 2026 18:00:32 +1000 Subject: [PATCH 2/2] Fix rounding for damage conversion values Only round converted damage at the end instead of the start of the calculation path. This prevents each recursive conversion step from creating or losing damage values. --- spec/System/TestOffence_spec.lua | 19 +++++++++++++++++++ src/Modules/CalcOffence.lua | 27 +++++++++++++++------------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index 4cbc4846afc..63a08da3f16 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -67,6 +67,25 @@ describe("TestOffence", function() assert.is_true(math.abs(conversion.mult - 0.01) < 0.000001) end) + it("rounds damage after the final step of a conversion path", function() + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Imbued Wand + Adds 2 to 2 Physical Damage to Spells + ]]) + build.itemsTab:AddDisplayItem() + build.skillsTab:PasteSocketGroup("Fireball 20/0 1") + build.configTab.input.customMods = [[ + 50% of Physical Damage Converted to Lightning Damage + 50% of Lightning Damage Converted to Cold Damage + ]] + build.configTab:BuildModList() + runCallback("OnFrame") + + assert.are.equals(9.6, build.calcsTab.calcsOutput.PhysicalMinBase) + assert.are.equals(1, build.calcsTab.calcsOutput.ColdMin) + end) + it("parses more/less/increased/reduced minimum and maximum damage of every type", function() build.itemsTab:CreateDisplayItemFromRaw([[ New Item diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 90057bffb31..b85bbcabd56 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -87,11 +87,6 @@ local function calcDamage(activeSkill, output, cfg, breakdown, damageType, typeF addMax = addMax + max * convMult end end - if addMin ~= 0 and addMax ~= 0 then - addMin = round(addMin) - addMax = round(addMax) - end - local baseMin = output[damageType.."MinBase"] local baseMax = output[damageType.."MaxBase"] if baseMin == 0 and baseMax == 0 then @@ -99,13 +94,16 @@ local function calcDamage(activeSkill, output, cfg, breakdown, damageType, typeF if breakdown and (addMin ~= 0 or addMax ~= 0) then t_insert(breakdown.damageTypes, { source = damageType, - convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax), - total = addMin .. " to " .. addMax, + convSrc = (addMin ~= 0 or addMax ~= 0) and (round(addMin) .. " to " .. round(addMax)), + total = round(addMin) .. " to " .. round(addMax), convDst = convDst and s_format("%d%% to %s", conversionTable[damageType].conversion[convDst] * 100, convDst), gainDst = convDst and s_format("%d%% gained as %s", conversionTable[damageType].gain[convDst] * 100, convDst), }) end - return addMin, addMax + if convDst then + return addMin, addMax + end + return round(addMin), round(addMax) end -- Combine modifiers @@ -125,15 +123,20 @@ local function calcDamage(activeSkill, output, cfg, breakdown, damageType, typeF base = baseMin .. " to " .. baseMax, inc = (inc ~= 1 and "x "..inc), more = (more ~= 1 and "x "..more), - convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax), - total = (round(baseMin * inc * more) + addMin) .. " to " .. (round(baseMax * inc * more) + addMax), + convSrc = (addMin ~= 0 or addMax ~= 0) and (round(addMin) .. " to " .. round(addMax)), + total = round(baseMin * inc * more + addMin) .. " to " .. round(baseMax * inc * more + addMax), convDst = convDst and conversionTable[damageType].conversion[convDst] > 0 and s_format("%d%% to %s", conversionTable[damageType].conversion[convDst] * 100, convDst), gainDst = convDst and conversionTable[damageType].gain[convDst] > 0 and s_format("%d%% gained as %s", conversionTable[damageType].gain[convDst] * 100, convDst), }) end - return round(((baseMin * inc * more) * genericMoreMinDamage + addMin) * moreMinDamage * incMinDamage), - round(((baseMax * inc * more) * genericMoreMaxDamage + addMax) * moreMaxDamage * incMaxDamage) + local min = ((baseMin * inc * more) * genericMoreMinDamage + addMin) * moreMinDamage * incMinDamage + local max = ((baseMax * inc * more) * genericMoreMaxDamage + addMax) * moreMaxDamage * incMaxDamage + if convDst then + -- Converted paths stay fractional until they reach their final damage type. + return min, max + end + return round(min), round(max) end local function calcAilmentSourceDamage(activeSkill, output, cfg, breakdown, damageType, typeFlags)