From 219a1a44a5010976ae3b9f139137cfa77e9c357b Mon Sep 17 00:00:00 2001 From: William Archbell Date: Sun, 25 Oct 2026 13:54:56 -0400 Subject: [PATCH] Fix defence conversion allocation with several targets Two problems in the per-source conversion step of CalcDefence: - When a source's ConvertTo* rates totalled more than 100%, the loop meant to scale them down used ipairs over a table keyed by target name, so it never ran. Global and Life/Mana conversions then converted more than 100% of the source. - The per-slot path reduced the source's slot base inside the target loop. Every target after the first with a non-zero rate took its share from an already reduced base, and the source was reduced once per target. This also happened below 100% total, and when one target came only from a GainAs rate. Iterate the rates with pairs, and reduce each slot base once after all targets have taken their share. --- spec/System/TestDefence_spec.lua | 43 ++++++++++++++++++++++++++++++++ src/Modules/CalcDefence.lua | 7 ++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/spec/System/TestDefence_spec.lua b/spec/System/TestDefence_spec.lua index 6d9c1a3f79..d96e6bea24 100644 --- a/spec/System/TestDefence_spec.lua +++ b/spec/System/TestDefence_spec.lua @@ -49,6 +49,49 @@ describe("TestDefence", function() assert.are.equals(manaWithoutTotalEnergyShield + 100, player.output.Mana) end) + it("scales conversion rates down proportionally when they total more than 100%", function() + -- 50% to Energy Shield plus 75% to Armour (with 75% Chaos Resistance) is 125%, so both scale by 100/125 + build.configTab.input.customMods = "\z + 50% of Maximum Life Converted to Energy Shield\n\z + Convert 1% of maximum Life to twice as much Armour per 1% Chaos Resistance above 0%\n\z + +75% to Chaos Resistance\n\z + " + build.configTab:BuildModList() + runCallback("OnFrame") + + local lifeBase = build.calcsTab.mainEnv.player.modDB:Sum("BASE", nil, "Life") + local factor = 100 / 125 + assert.are.equals(round(lifeBase * 50 * factor / 100), build.calcsTab.mainOutput.EnergyShield) + assert.are.equals(round(lifeBase * (75 * factor + 75) / 100), build.calcsTab.mainOutput.Armour) + end) + + it("takes each conversion target's share from the unconverted slot base", function() + local function energyShieldBodyArmourWith(customMods) + newBuild() + build.itemsTab:CreateDisplayItemFromRaw([[ + New Item + Tattered Robe + +100 to maximum Energy Shield + ]]) + build.itemsTab:AddDisplayItem() + build.configTab.input.customMods = customMods + build.configTab:BuildModList() + runCallback("OnFrame") + return build.calcsTab.mainOutput + end + local toMana = "Convert 40% of maximum Energy Shield to maximum Mana" + local asArmour = "Gain 20% of maximum Energy Shield as extra Armour" + + local manaOnly = energyShieldBodyArmourWith(toMana) + local armourOnly = energyShieldBodyArmourWith(asArmour) + local both = energyShieldBodyArmourWith(toMana .. "\n" .. asArmour) + + -- Gaining Armour from Energy Shield must not reduce the Energy Shield kept or the Mana converted + assert.are.equals(manaOnly.EnergyShield, both.EnergyShield) + assert.are.equals(manaOnly.Mana, both.Mana) + assert.are.equals(armourOnly.Armour, both.Armour) + end) + it("Geofri's Sanctuary mods work", function() build.itemsTab:CreateDisplayItemFromRaw([[ Geofri's Sanctuary diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index de8d8996b5..e5aa785b59 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -1372,7 +1372,7 @@ function calcs.defence(env, actor) if totalConversion > 100 then local factor = 100 / totalConversion totalConversion = 100 - for name, val in ipairs(source.conversionRate) do + for name, val in pairs(source.conversionRate) do source.conversionRate[name] = val * factor end end @@ -1410,7 +1410,6 @@ function calcs.defence(env, actor) if breakdown then breakdown.slot(slot, source.name .. " to " .. target.name .. " conversion", { slotName = slot }, targetBase, nil, unpack(target.mods)) end - source.basePerSlot[slot] = source.basePerSlot[slot] * (100 - source.totalConversion) / 100 end end local targetBase = globalBase * rate / 100 @@ -1437,6 +1436,10 @@ function calcs.defence(env, actor) end end if source.defence then + -- Every target takes its share from the unconverted base, so reduce the source once, after all targets + for _, slot in pairs({ "Helmet", "Gloves", "Boots", "Body Armour", "Weapon 2", "Weapon 3" }) do + source.basePerSlot[slot] = source.basePerSlot[slot] * (100 - source.totalConversion) / 100 + end source.globalBase = globalBase * (100 - source.totalConversion) / 100 source.totalBase = totalBase * (100 - source.totalConversion) / 100 end