From b04d7551e1718effc0669c0a6d954a81c9e37494 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:09:33 +0200 Subject: [PATCH 1/8] Refactor leech calculation Intent was to standardize mod phrasing and logic, as well as reducing repetition of code. --- src/Modules/CalcOffence.lua | 42 +++++++++++++++++++++++-------------- src/Modules/ModParser.lua | 10 +++++++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index d7a745415f..f8ae570048 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -3693,25 +3693,35 @@ function calcs.offence(env, actor, activeSkill) local lifeLeech = 0 local energyShieldLeech = 0 local manaLeech = 0 + + -- Determine base leech value according to resource (using function to avoid repetition) + ---@param resource string "Life" | "Mana" | "EnergyShield" + ---@return number + local function getBaseLeech(resource) + local leech = 0 + if (not skillModList:Flag(nil, "No" .. resource .. "LeechFrom" .. damageType .. "Damage" )) then + -- Check if converted phys leech + if skillModList:Flag(nil, resource .. "LeechBasedOn" .. damageType .. "Damage") then + local modSource = "" + for i, result in ipairs(actor.modDB:Tabulate("FLAG", nil, resource .. "LeechBasedOn" .. damageType .. "Damage")) do + if result.mod.value then + modSource = result.mod.source + break + end + end + skillModList:ReplaceMod(damageType .. "Damage" .. resource .. "Leech", "BASE", skillModList:Sum("BASE", cfg, "PhysicalDamage" .. resource .. "Leech"), modSource) + end + leech = skillModList:Sum("BASE", cfg, "Damage" .. resource .. "Leech", damageType.."Damage" .. resource .. "Leech", isElemental[damageType] and "ElementalDamage" .. resource .. "Leech" or nil) + enemyDB:Sum("BASE", cfg, "SelfDamage" .. resource .. "Leech") / 100 + end + return leech and leech or 0 + end + if skillFlags.mine or skillFlags.trap or skillFlags.totem then lifeLeech = skillModList:Sum("BASE", cfg, "DamageLifeLeechToPlayer") else - if skillModList:Flag(nil, "LifeLeechBasedOnChaosDamage") then - if damageType == "Chaos" then - lifeLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", "PhysicalDamageLifeLeech", "LightningDamageLifeLeech", "ColdDamageLifeLeech", "FireDamageLifeLeech", "ChaosDamageLifeLeech", "ElementalDamageLifeLeech") + enemyDB:Sum("BASE", cfg, "SelfDamageLifeLeech") / 100 - end - else - if pass == 1 and damageType == "Physical" and skillModList:Flag(nil, "PhysicalAsElementalDamageLifeLeech") then - skillModList:NewMod("ElementalDamageLifeLeech", "BASE", skillModList:Sum("BASE", cfg, "PhysicalDamageLifeLeech"), "Mystic Harvest") - end - lifeLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", damageType.."DamageLifeLeech", isElemental[damageType] and "ElementalDamageLifeLeech" or nil) + enemyDB:Sum("BASE", cfg, "SelfDamageLifeLeech") / 100 - end - energyShieldLeech = skillModList:Sum("BASE", cfg, "DamageEnergyShieldLeech", damageType.."DamageEnergyShieldLeech", isElemental[damageType] and "ElementalDamageEnergyShieldLeech" or nil) + enemyDB:Sum("BASE", cfg, "SelfDamageEnergyShieldLeech") / 100 - if pass == 1 and damageType == "Physical" and skillModList:Flag(nil, "PhysicalAsAllDamageManaLeech") then - skillModList:NewMod("ElementalDamageManaLeech", "BASE", skillModList:Sum("BASE", cfg, "PhysicalDamageLifeLeech"), "Ravenous Doubts") - skillModList:NewMod("ChaosDamageManaLeech", "BASE", skillModList:Sum("BASE", cfg, "PhysicalDamageLifeLeech"), "Ravenous Doubts") - end - manaLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageManaLeech", damageType.."DamageManaLeech", isElemental[damageType] and "ElementalDamageManaLeech" or nil) + enemyDB:Sum("BASE", cfg, "SelfDamageManaLeech") / 100 + lifeLeech = getBaseLeech("Life") + energyShieldLeech = getBaseLeech("EnergyShield") + manaLeech = getBaseLeech("Mana") end if ghostReaver and not noLifeLeech then diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 70353de369..588a9eec24 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -2468,7 +2468,10 @@ local specialModList = { mod(firstToUpper(dmgType) .. "Min", "BASE", 1, nil, ModFlag.Attack, { type = "PercentStat", stat = "AccuracyOnWeapon 2", percent = num }, { type = "SkillType", skillType = SkillType.NonWeaponAttack, neg = true } , { type = "Condition", var = "OffHandAttack" }), mod(firstToUpper(dmgType) .. "Max", "BASE", 1, nil, ModFlag.Attack, { type = "PercentStat", stat = "AccuracyOnWeapon 2", percent = num }, { type = "SkillType", skillType = SkillType.NonWeaponAttack, neg = true } , { type = "Condition", var = "OffHandAttack" }), } end, - ["life leech recovers based on your elemental damage as well as physical damage"] = { flag("PhysicalAsElementalDamageLifeLeech"), }, + ["life leech recovers based on your elemental damage as well as physical damage"] = { + flag("LifeLeechBasedOnColdDamage"), + flag("LifeLeechBasedOnFireDamage"), + flag("LifeLeechBasedOnLightningDamage"), }, ["evasion rating from equipped helmet, gloves and boots is doubled"] = { mod("Evasion", "MORE", 100, { type = "SlotName", slotNameList = { "Helmet", "Boots", "Gloves" } }) }, ["evasion rating from equipped body armour is halved"] = { mod("Evasion", "MORE", -50, { type = "SlotName", slotName = "Body Armour" }) }, -- Ascendant @@ -4459,7 +4462,10 @@ local specialModList = { mod("InstantLifeLeech", "BASE", 100, { type = "Condition", var = "CriticalStrike" }), mod("InstantManaLeech", "BASE", 100, { type = "Condition", var = "CriticalStrike" }) }, - ["with 5 corrupted items equipped: life leech recovers based on your chaos damage instead"] = { flag("LifeLeechBasedOnChaosDamage", { type = "MultiplierThreshold", var = "CorruptedItem", threshold = 5 }) }, + ["with 5 corrupted items equipped: life leech recovers based on your chaos damage instead"] = { + flag("LifeLeechBasedOnChaosDamage", { type = "MultiplierThreshold", var = "CorruptedItem", threshold = 5 }), + flag("NoLifeLeechFromPhysicalDamage", { type = "MultiplierThreshold", var = "CorruptedItem", threshold = 5 }), + }, ["you have vaal pact if you've dealt a critical hit recently"] = { mod("Keystone", "LIST", "Vaal Pact", { type = "Condition", var = "CritRecently" }) }, ["you have vaal pact while at maximum endurance charges"] = { mod("Keystone", "LIST", "Vaal Pact", { type = "StatThreshold", stat = "EnduranceCharges", thresholdStat = "EnduranceChargesMax" }) }, ["you have vaal pact while focus?sed"] = { mod("Keystone", "LIST", "Vaal Pact", { type = "Condition", var = "Focused" }) }, From 8ff183e7acf28f31f08e81e00871f27f3f78fee7 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:35:01 +0200 Subject: [PATCH 2/8] Add `hasTag` function to `modLib` in `ModTools.lua` There was no easy existing way to check if a specific tag is already present on a on mod (Or I couldn't find it) So I added a function for it, which makes it easy to check for, add, or replace/modify specific tags --- src/Modules/ModTools.lua | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Modules/ModTools.lua b/src/Modules/ModTools.lua index 27f73271fc..049e9d58e5 100644 --- a/src/Modules/ModTools.lua +++ b/src/Modules/ModTools.lua @@ -233,4 +233,36 @@ function modLib.setSource(mod, source) mod.value.mod.source = source end return mod +end + +-- Check if a mod contains a specific tag already +-- Note: All keys AND values need to be matched exactly +---@param mod table individual mod that is to be checked +---@param searchTag table exact tag you're searching for, e.g. { type = "Condition", var = "Shocked" } +---@return boolean @returns `true` if tag is found, otherwise `false` +---@return number @returns position as `number` if tag exists or `0` if not +function modLib.hasTag(mod, searchTag) + local numSearchKeys = 0 -- Apparently Lua doesn't have a built in functionality to return the number of keys in a table(?) so have to determine manually + for _, __ in pairs(searchTag) do + numSearchKeys = numSearchKeys + 1 + end + + for i, tag in ipairs(mod) do + local numTagKeys = 0 -- Total number of keys in tag + local numMatchedKeys = 0 -- Number of keys matching with searchTag + if type(tag) == "table" then + for key, value in pairs(searchTag) do + numTagKeys = numTagKeys + 1 + if tag[key] and (tag[key] == value) then + numMatchedKeys = numMatchedKeys + 1 + else + break -- this is not the correct tag + end + end + end + if (numSearchKeys > 0) and (numSearchKeys == numMatchedKeys) and (numSearchKeys == numTagKeys) then + return true, i + end + end + return false, 0 end \ No newline at end of file From a7e4d2863ef4100f8b16ef9f07321a5c2aa7dd54 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:24:46 +0200 Subject: [PATCH 3/8] Add option to override condition values via `cfg` It was kinda hard to tell `ModStore:GetCondition` to assume a condition is `false` without changing the actual conditions on the actor, so I added the opton to force the value via `cfg.overrideCond`. I needed to force the condition because I wanted to tabulate mods that do not apply, but are used as basis for the value of other mods. e.g. Leech Elemental Damage *instead* of Physical Damage --- src/Classes/ModStore.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Classes/ModStore.lua b/src/Classes/ModStore.lua index 3523140724..ba89af6b2d 100644 --- a/src/Classes/ModStore.lua +++ b/src/Classes/ModStore.lua @@ -246,7 +246,11 @@ function ModStoreClass:HasMod(modType, cfg, ...) end function ModStoreClass:GetCondition(var, cfg, noMod) - return self.conditions[var] or (self.parent and self.parent:GetCondition(var, cfg, true)) or (not noMod and self:Flag(cfg, conditionName[var])) + if (cfg and cfg.overrideCond and cfg.overrideCond[var] ~= nil) then + return cfg.overrideCond[var] + else + return self.conditions[var] or (self.parent and self.parent:GetCondition(var, cfg, true)) or (not noMod and self:Flag(cfg, conditionName[var])) + end end function ModStoreClass:GetMultiplier(var, cfg, noMod) From 71203d1e447a442fceb74985d628dd8e11257e4d Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:44:00 +0200 Subject: [PATCH 4/8] Improve leech conversion refactor Better handling of actual conversion of mods with the correct tags for display in breakdown later --- src/Data/ModCache.lua | 2 +- src/Modules/CalcOffence.lua | 53 +++++++++++++++++++++++++++---------- src/Modules/ModParser.lua | 7 ++--- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index c68a19fa2c..8f8c81fcfc 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -5090,7 +5090,7 @@ c["Life Leech from your Hits also applies to your Companion"]={nil,"Life Leech f c["Life Leech is Converted to Energy Shield Leech"]={{[1]={flags=0,keywordFlags=0,name="GhostReaver",type="FLAG",value=true}},nil} c["Life Leech is Instant"]={{[1]={flags=0,keywordFlags=0,name="InstantLifeLeech",type="BASE",value=100}},nil} c["Life Leech recovers based on your Chaos damage instead of Physical damage"]={nil,"Life Leech recovers based on your Chaos damage instead of Physical damage "} -c["Life Leech recovers based on your Elemental damage as well as Physical damage"]={{[1]={flags=0,keywordFlags=0,name="PhysicalAsElementalDamageLifeLeech",type="FLAG",value=true}},nil} +c["Life Leech recovers based on your Elemental damage as well as Physical damage"]={{[1]={flags=0,keywordFlags=0,name="LifeLeechBasedOnElementalDamage",type="FLAG",value=true}},nil} c["Life Leeched from Empowered Attacks is Instant"]={nil,"Life Leeched from Empowered Attacks is Instant "} c["Life Recharges"]={nil,"Life Recharges "} c["Life Recharges instead of Energy Shield"]={{[1]={flags=0,keywordFlags=0,name="EnergyShieldRechargeAppliesToLife",type="FLAG",value=true}},nil} diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index f8ae570048..8f2531ca5d 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -3696,22 +3696,47 @@ function calcs.offence(env, actor, activeSkill) -- Determine base leech value according to resource (using function to avoid repetition) ---@param resource string "Life" | "Mana" | "EnergyShield" + ---@param dmgType string "Physical" | "Cold" | "Fire" | "Lightning" | "Chaos" ---@return number - local function getBaseLeech(resource) + local function getBaseLeech(resource, dmgType) local leech = 0 - if (not skillModList:Flag(nil, "No" .. resource .. "LeechFrom" .. damageType .. "Damage" )) then - -- Check if converted phys leech - if skillModList:Flag(nil, resource .. "LeechBasedOn" .. damageType .. "Damage") then - local modSource = "" - for i, result in ipairs(actor.modDB:Tabulate("FLAG", nil, resource .. "LeechBasedOn" .. damageType .. "Damage")) do - if result.mod.value then - modSource = result.mod.source - break + if (not skillModList:Flag(cfg, "Condition:No" .. resource .. "LeechFrom" .. dmgType .. "Damage" )) and not (isElemental[dmgType] and skillModList:Flag(cfg, "No" .. resource .. "LeechFromElementalDamage" )) then + -- Check if converted physical leech (most PoE2 leech is physical only by default) + local convertModName, convertFlag + if isElemental[dmgType] and skillModList:Flag(cfg, resource .. "LeechBasedOnElementalDamage") then + convertFlag = resource .. "LeechBasedOnElementalDamage" + convertModName = "ElementalDamage" .. resource .. "Leech" + elseif skillModList:Flag(cfg, resource .. "LeechBasedOn".. dmgType .. "Damage") then + convertFlag = resource .. "LeechBasedOn" .. dmgType .. "Damage" + convertModName = dmgType .. "Damage" .. resource .. "Leech" + end + if convertModName and convertFlag then + local tempCfg = copyTable(cfg, true) + tempCfg.overrideCond = { ["No" .. resource .. "LeechFromPhysicalDamage"] = false } -- Need to force Condition to `false`, to calculate original phys leech values + local physLeechMods = skillModList:Tabulate("BASE", tempCfg , "PhysicalDamage" .. resource .. "Leech") + for _, entry in ipairs(physLeechMods) do + -- Add new leech mods for that damage type with the same conditions, source, etc. + local newMod = copyTable(entry.mod) + newMod.name = convertModName + -- Tags that specifically disable Physical Damage leech need to be removed + local hasNoPhysLeech, tagIndex = modLib.hasTag(newMod, { type = "Condition", var = "No" .. resource .. "LeechFromPhysicalDamage", neg = true }) + if hasNoPhysLeech then + t_remove(newMod, tagIndex) + end + if not skillModList:ReplaceModInternal(newMod) then -- using `ReplaceModInternal` instead of `ReplaceMod`, so I don't have to unpack the mod first + skillModList:AddMod(newMod) end end - skillModList:ReplaceMod(damageType .. "Damage" .. resource .. "Leech", "BASE", skillModList:Sum("BASE", cfg, "PhysicalDamage" .. resource .. "Leech"), modSource) end - leech = skillModList:Sum("BASE", cfg, "Damage" .. resource .. "Leech", damageType.."Damage" .. resource .. "Leech", isElemental[damageType] and "ElementalDamage" .. resource .. "Leech" or nil) + enemyDB:Sum("BASE", cfg, "SelfDamage" .. resource .. "Leech") / 100 + leech = skillModList:Sum("BASE", cfg, "Damage" .. resource .. "Leech", dmgType.."Damage" .. resource .. "Leech", isElemental[dmgType] and "ElementalDamage" .. resource .. "Leech" or nil) + enemyDB:Sum("BASE", cfg, "SelfDamage" .. resource .. "Leech") / 100 + elseif skillModList:Flag(cfg, "Condition:No" .. resource .. "LeechFrom" .. dmgType .. "Damage" ) then + -- dmgType leech should not apply, but still needs to exist for possible conversion so adding additional condition tag instead + local noLeechFlagTag = { type = "Condition", var = "No" .. resource .. "LeechFrom" .. dmgType .. "Damage", neg = true } + for _, entry in ipairs(skillModList:Tabulate("BASE", cfg, dmgType .. "Damage" .. resource .. "Leech")) do + if not modLib.hasTag(entry.mod, noLeechFlagTag) then + t_insert(entry.mod, noLeechFlagTag ) + end + end end return leech and leech or 0 end @@ -3719,9 +3744,9 @@ function calcs.offence(env, actor, activeSkill) if skillFlags.mine or skillFlags.trap or skillFlags.totem then lifeLeech = skillModList:Sum("BASE", cfg, "DamageLifeLeechToPlayer") else - lifeLeech = getBaseLeech("Life") - energyShieldLeech = getBaseLeech("EnergyShield") - manaLeech = getBaseLeech("Mana") + lifeLeech = getBaseLeech("Life", damageType) + energyShieldLeech = getBaseLeech("EnergyShield", damageType) + manaLeech = getBaseLeech("Mana", damageType) end if ghostReaver and not noLifeLeech then diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 588a9eec24..dfd423d59e 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -2468,10 +2468,7 @@ local specialModList = { mod(firstToUpper(dmgType) .. "Min", "BASE", 1, nil, ModFlag.Attack, { type = "PercentStat", stat = "AccuracyOnWeapon 2", percent = num }, { type = "SkillType", skillType = SkillType.NonWeaponAttack, neg = true } , { type = "Condition", var = "OffHandAttack" }), mod(firstToUpper(dmgType) .. "Max", "BASE", 1, nil, ModFlag.Attack, { type = "PercentStat", stat = "AccuracyOnWeapon 2", percent = num }, { type = "SkillType", skillType = SkillType.NonWeaponAttack, neg = true } , { type = "Condition", var = "OffHandAttack" }), } end, - ["life leech recovers based on your elemental damage as well as physical damage"] = { - flag("LifeLeechBasedOnColdDamage"), - flag("LifeLeechBasedOnFireDamage"), - flag("LifeLeechBasedOnLightningDamage"), }, + ["life leech recovers based on your elemental damage as well as physical damage"] = { flag("LifeLeechBasedOnElementalDamage"), }, ["evasion rating from equipped helmet, gloves and boots is doubled"] = { mod("Evasion", "MORE", 100, { type = "SlotName", slotNameList = { "Helmet", "Boots", "Gloves" } }) }, ["evasion rating from equipped body armour is halved"] = { mod("Evasion", "MORE", -50, { type = "SlotName", slotName = "Body Armour" }) }, -- Ascendant @@ -4464,7 +4461,7 @@ local specialModList = { }, ["with 5 corrupted items equipped: life leech recovers based on your chaos damage instead"] = { flag("LifeLeechBasedOnChaosDamage", { type = "MultiplierThreshold", var = "CorruptedItem", threshold = 5 }), - flag("NoLifeLeechFromPhysicalDamage", { type = "MultiplierThreshold", var = "CorruptedItem", threshold = 5 }), + flag("Condition:NoLifeLeechFromPhysicalDamage", { type = "MultiplierThreshold", var = "CorruptedItem", threshold = 5 }), }, ["you have vaal pact if you've dealt a critical hit recently"] = { mod("Keystone", "LIST", "Vaal Pact", { type = "Condition", var = "CritRecently" }) }, ["you have vaal pact while at maximum endurance charges"] = { mod("Keystone", "LIST", "Vaal Pact", { type = "StatThreshold", stat = "EnduranceCharges", thresholdStat = "EnduranceChargesMax" }) }, From a9f3edc4ea3d85700c0a90875ef4390e81fa4d80 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:45:02 +0200 Subject: [PATCH 5/8] Add new leech modifiers to relevant breakdowns --- src/Modules/CalcSections.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 59b630cabb..a6f3549b73 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -1101,6 +1101,7 @@ return { { label = "Off Hand", notFlag = "totem", flag = "weapon2Attack", modName = { "DamageLeech", "DamageLifeLeech", "PhysicalDamageLifeLeech", "LightningDamageLifeLeech", "ColdDamageLifeLeech", "FireDamageLifeLeech", "ChaosDamageLifeLeech", "ElementalDamageLifeLeech" }, modType = "BASE", cfg = "weapon2" }, { label = "Totem modifiers", flag = "totem", modName = { "DamageLifeLeechToPlayer" }, modType = "BASE", cfg = "skill" }, { label = "Enemy modifiers", modName = { "SelfDamageLifeLeech" }, modType = "BASE", enemy = true }, + { label = "Leech Conversion", modName = { "LifeLeechBasedOnPhysicalDamage", "LifeLeechBasedOnColdDamage", "LifeLeechBasedOnFireDamage", "LifeLeechBasedOnLightningDamage", "LifeLeechBasedOnElementalDamage", "LifeLeechBasedOnChaosDamage", "Condition:NoLifeLeechFromPhysicalDamage", "Condition:NoLifeLeechFromColdDamage", "Condition:NoLifeLeechFromFireDamage", "Condition:NoLifeLeechFromLightningDamage", "Condition:NoLifeLeechFromElementalDamage", "Condition:NoLifeLeechFromChaosDamage" }, cfg = "skill" }, }, }, { label = "Life Leech per Hit", flagList = { "leechLife", "showAverage" }, { format = "{1:output:LifeLeechPerHit}", { breakdown = "LifeLeech" }, @@ -1109,6 +1110,7 @@ return { { label = "Off Hand", notFlag = "totem", flag = "weapon2Attack", modName = { "DamageLeech", "DamageLifeLeech", "PhysicalDamageLifeLeech", "LightningDamageLifeLeech", "ColdDamageLifeLeech", "FireDamageLifeLeech", "ChaosDamageLifeLeech", "ElementalDamageLifeLeech" }, modType = "BASE", cfg = "weapon2" }, { label = "Totem modifiers", flag = "totem", modName = { "DamageLifeLeechToPlayer" }, modType = "BASE", cfg = "skill" }, { label = "Enemy modifiers", modName = { "SelfDamageLifeLeech" }, modType = "BASE", enemy = true }, + { label = "Leech Conversion", modName = { "LifeLeechBasedOnPhysicalDamage", "LifeLeechBasedOnColdDamage", "LifeLeechBasedOnFireDamage", "LifeLeechBasedOnLightningDamage", "LifeLeechBasedOnElementalDamage", "LifeLeechBasedOnChaosDamage", "Condition:NoLifeLeechFromPhysicalDamage", "Condition:NoLifeLeechFromColdDamage", "Condition:NoLifeLeechFromFireDamage", "Condition:NoLifeLeechFromLightningDamage", "Condition:NoLifeLeechFromElementalDamage", "Condition:NoLifeLeechFromChaosDamage" }, cfg = "skill" }, }, }, { label = "Life Gain Rate", notFlag = "showAverage", haveOutput = "LifeOnHitRate", { format = "{1:output:LifeOnHitRate}", { label = "Player modifiers", notFlag = "attack", modName = "LifeOnHit", modType = "BASE", cfg = "skill" }, @@ -1136,6 +1138,7 @@ return { { label = "Off Hand", notFlag = "totem", flag = "weapon2Attack", modName = { "DamageEnergyShieldLeech", "PhysicalDamageEnergyShieldLeech", "LightningDamageEnergyShieldLeech", "ColdDamageEnergyShieldLeech", "FireDamageEnergyShieldLeech", "ChaosDamageEnergyShieldLeech", "ElementalDamageEnergyShieldLeech" }, modType = "BASE", cfg = "weapon2" }, { label = "Totem modifiers", flag = "totem", modName = { "DamageEnergyShieldLeechToPlayer" }, modType = "BASE", cfg = "skill" }, { label = "Enemy modifiers", modName = { "SelfDamageEnergyShieldLeech" }, modType = "BASE", enemy = true }, + { label = "Leech Conversion", modName = { "EnergyShieldLeechBasedOnPhysicalDamage", "EnergyShieldLeechBasedOnColdDamage", "EnergyShieldLeechBasedOnFireDamage", "EnergyShieldLeechBasedOnLightningDamage", "EnergyShieldLeechBasedOnElementalDamage", "EnergyShieldLeechBasedOnChaosDamage", "Condition:NoEnergyShieldLeechFromPhysicalDamage", "Condition:NoEnergyShieldLeechFromColdDamage", "Condition:NoEnergyShieldLeechFromFireDamage", "Condition:NoEnergyShieldLeechFromLightningDamage", "Condition:NoEnergyShieldLeechFromElementalDamage", "Condition:NoEnergyShieldLeechFromChaosDamage" }, cfg = "skill" }, }, }, { label = "ES Leech per Hit", flagList = { "leechES", "showAverage" }, { format = "{1:output:EnergyShieldLeechPerHit}", { breakdown = "EnergyShieldLeech" }, @@ -1144,6 +1147,7 @@ return { { label = "Off Hand", notFlag = "totem", flag = "weapon2Attack", modName = { "DamageEnergyShieldLeech", "PhysicalDamageEnergyShieldLeech", "LightningDamageEnergyShieldLeech", "ColdDamageEnergyShieldLeech", "FireDamageEnergyShieldLeech", "ChaosDamageEnergyShieldLeech", "ElementalDamageEnergyShieldLeech" }, modType = "BASE", cfg = "weapon2" }, { label = "Totem modifiers", flag = "totem", modName = { "DamageEnergyShieldLeechToPlayer" }, modType = "BASE", cfg = "skill" }, { label = "Enemy modifiers", modName = { "SelfDamageEnergyShieldLeech" }, modType = "BASE", enemy = true }, + { label = "Leech Conversion", modName = { "EnergyShieldLeechBasedOnPhysicalDamage", "EnergyShieldLeechBasedOnColdDamage", "EnergyShieldLeechBasedOnFireDamage", "EnergyShieldLeechBasedOnLightningDamage", "EnergyShieldLeechBasedOnElementalDamage", "EnergyShieldLeechBasedOnChaosDamage", "Condition:NoEnergyShieldLeechFromPhysicalDamage", "Condition:NoEnergyShieldLeechFromColdDamage", "Condition:NoEnergyShieldLeechFromFireDamage", "Condition:NoEnergyShieldLeechFromLightningDamage", "Condition:NoEnergyShieldLeechFromElementalDamage", "Condition:NoEnergyShieldLeechFromChaosDamage" }, cfg = "skill" }, }, }, { label = "ES Gain Rate", notFlag = "showAverage", haveOutput = "EnergyShieldOnHitRate", { format = "{1:output:EnergyShieldOnHitRate}", { label = "Player modifiers", notFlag = "attack", modName = "EnergyShieldOnHit", modType = "BASE", cfg = "skill" }, @@ -1170,6 +1174,7 @@ return { { label = "Main Hand", flag = "weapon1Attack", modName = { "DamageLeech", "DamageManaLeech", "PhysicalDamageManaLeech", "LightningDamageManaLeech", "ColdDamageManaLeech", "FireDamageManaLeech", "ChaosDamageManaLeech", "ElementalDamageManaLeech" }, modType = "BASE", cfg = "weapon1" }, { label = "Off Hand", flag = "weapon2Attack", modName = { "DamageLeech", "DamageManaLeech", "PhysicalDamageManaLeech", "LightningDamageManaLeech", "ColdDamageManaLeech", "FireDamageManaLeech", "ChaosDamageManaLeech", "ElementalDamageManaLeech" }, modType = "BASE", cfg = "weapon2" }, { label = "Enemy modifiers", modName = { "SelfDamageManaLeech" }, modType = "BASE", cfg = "skill", enemy = true }, + { label = "Leech Conversion", modName = { "ManaLeechBasedOnPhysicalDamage", "ManaLeechBasedOnColdDamage", "ManaLeechBasedOnFireDamage", "ManaLeechBasedOnLightningDamage", "ManaLeechBasedOnElementalDamage", "ManaLeechBasedOnChaosDamage", "Condition:NoManaLeechFromPhysicalDamage", "Condition:NoManaLeechFromColdDamage", "Condition:NoManaLeechFromFireDamage", "Condition:NoManaLeechFromLightningDamage", "Condition:NoManaLeechFromElementalDamage", "Condition:NoManaLeechFromChaosDamage" }, cfg = "skill" }, }, }, { label = "Mana Leech per Hit", flagList = { "leechMana", "showAverage" }, { format = "{1:output:ManaLeechPerHit}", { breakdown = "ManaLeech" }, @@ -1177,6 +1182,7 @@ return { { label = "Main Hand", flag = "weapon1Attack", modName = { "DamageLeech", "DamageManaLeech", "PhysicalDamageManaLeech", "LightningDamageManaLeech", "ColdDamageManaLeech", "FireDamageManaLeech", "ChaosDamageManaLeech", "ElementalDamageManaLeech" }, modType = "BASE", cfg = "weapon1" }, { label = "Off Hand", flag = "weapon2Attack", modName = { "DamageLeech", "DamageManaLeech", "PhysicalDamageManaLeech", "LightningDamageManaLeech", "ColdDamageManaLeech", "FireDamageManaLeech", "ChaosDamageManaLeech", "ElementalDamageManaLeech" }, modType = "BASE", cfg = "weapon2" }, { label = "Enemy modifiers", modName = { "SelfDamageManaLeech" }, modType = "BASE", enemy = true }, + { label = "Leech Conversion", modName = { "ManaLeechBasedOnPhysicalDamage", "ManaLeechBasedOnColdDamage", "ManaLeechBasedOnFireDamage", "ManaLeechBasedOnLightningDamage", "ManaLeechBasedOnElementalDamage", "ManaLeechBasedOnChaosDamage", "Condition:NoManaLeechFromPhysicalDamage", "Condition:NoManaLeechFromColdDamage", "Condition:NoManaLeechFromFireDamage", "Condition:NoManaLeechFromLightningDamage", "Condition:NoManaLeechFromElementalDamage", "Condition:NoManaLeechFromChaosDamage" }, cfg = "skill" }, }, }, { label = "Mana Gain Rate", notFlag = "showAverage", haveOutput = "ManaOnHitRate", { format = "{1:output:ManaOnHitRate}", { label = "Player modifiers", notFlag = "attack", modName = "ManaOnHit", modType = "BASE", cfg = "skill" }, From cbaf160dae86b021806507325938bed66940c102 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:46:05 +0200 Subject: [PATCH 6/8] Add parsing for "Walker of the Wilds" Keystone --- src/Data/ModCache.lua | 2 +- src/Modules/ModParser.lua | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 8f8c81fcfc..fe453b65b6 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -5142,7 +5142,7 @@ c["Mana Flasks gain 0.1 charges per Second"]={{[1]={flags=0,keywordFlags=0,name= c["Mana Flasks gain 0.22 charges per Second"]={{[1]={flags=0,keywordFlags=0,name="ManaFlaskChargesGenerated",type="BASE",value=0.22}},nil} c["Mana Flasks gain 0.25 charges per Second"]={{[1]={flags=0,keywordFlags=0,name="ManaFlaskChargesGenerated",type="BASE",value=0.25}},nil} c["Mana Flasks used while on Low Mana apply Recovery Instantly"]={{[1]={[1]={type="Condition",var="LowMana"},flags=0,keywordFlags=0,name="ManaFlaskInstantRecovery",type="BASE",value=100}},nil} -c["Mana Leech recovers based on Elemental Damage Types instead of Physical Damage"]={nil,"Mana Leech recovers based on Elemental Damage Types instead of Physical Damage "} +c["Mana Leech recovers based on Elemental Damage Types instead of Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="ManaLeechBasedOnElementalDamage",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="Condition:NoManaLeechFromPhysicalDamage",type="FLAG",value=true}},nil} c["Mana Recovery from Regeneration is not applied"]={{[1]={flags=0,keywordFlags=0,name="UnaffectedByManaRegen",type="FLAG",value=true}},nil} c["Mana Recovery other than Regeneration cannot Recover Mana"]={nil,"Mana Recovery other than Regeneration cannot Recover Mana "} c["Mark Skills have 10% increased Use Speed"]={{}," Use Speed "} diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index dfd423d59e..2e28773e1a 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -2396,6 +2396,10 @@ local specialModList = { flag("IgniteToChaos", { type = "SkillType", skillType = SkillType.Spell },{ type = "SkillType", skillType = SkillType[firstToUpper(spellType)]}), mod("SkillData", "LIST", { key = "IgniteToChaos", value = true }, { type = "SkillType", skillType = SkillType.Spell },{ type = "SkillType", skillType = SkillType[firstToUpper(spellType)]}), } end, + ["mana leech recovers based on elemental damage types instead of physical damage"] = { + flag("ManaLeechBasedOnElementalDamage"), + flag("Condition:NoManaLeechFromPhysicalDamage"), + }, -- Legacy support ["(%d+)%% chance to defend with double armour"] = function(numChance) return { mod("ArmourDefense", "MAX", 100, "Armour Mastery: Max Calc", { type = "Condition", var = "ArmourMax" }), From d9290ed766df6fc692d5057a0d31ddf0cc951851 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:03:03 +0200 Subject: [PATCH 7/8] Add support for further leech conversion mods This adds general support for parsing most expected forms of leech conversion mods. Specific mods now supported: - Sap of Nightmares (Acolyte of Chayula) - Mystic Harvest (Amazon) - Spire of Ire (Unique Helix Spear) --- src/Data/ModCache.lua | 4 ++-- src/Modules/ModParser.lua | 44 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index fe453b65b6..3ccf0f7c35 100755 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -5057,7 +5057,7 @@ c["Leech Life 20% slower"]={{[1]={flags=0,keywordFlags=0,name="LifeLeechRate",ty c["Leech Life 25% faster"]={{[1]={flags=0,keywordFlags=0,name="LifeLeechRate",type="INC",value=25}},nil} c["Leech Life 5% slower"]={{[1]={flags=0,keywordFlags=0,name="LifeLeechRate",type="INC",value=-5}},nil} c["Leech from Critical Hits is instant"]={{[1]={[1]={type="Condition",var="CriticalStrike"},flags=0,keywordFlags=0,name="InstantLifeLeech",type="BASE",value=100},[2]={[1]={type="Condition",var="CriticalStrike"},flags=0,keywordFlags=0,name="InstantManaLeech",type="BASE",value=100},[3]={[1]={type="Condition",var="CriticalStrike"},flags=0,keywordFlags=0,name="InstantEnergyShieldLeech",type="BASE",value=100}},nil} -c["Leech recovers based on Chaos Damage as well as Physical Damage"]={nil,"Leech recovers based on Chaos Damage as well as Physical Damage "} +c["Leech recovers based on Chaos Damage as well as Physical Damage"]={{[1]={flags=0,keywordFlags=0,name="LifeLeechBasedOnChaosDamage",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="ManaLeechBasedOnChaosDamage",type="FLAG",value=true},[3]={flags=0,keywordFlags=0,name="EnergyShieldLeechBasedOnChaosDamage",type="FLAG",value=true}},nil} c["Leeches 0.1% of Physical Damage as Life"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamageLifeLeech",type="BASE",value=0.1}},nil} c["Leeches 1% of maximum Life when you Cast a Spell"]={nil,"Leeches 1% of maximum Life when you Cast a Spell "} c["Leeches 10% of Physical Damage as Life"]={{[1]={flags=0,keywordFlags=0,name="PhysicalDamageLifeLeech",type="BASE",value=10}},nil} @@ -5089,7 +5089,7 @@ c["Life Leech effects are not removed when Unreserved Life is Filled"]={{[1]={fl c["Life Leech from your Hits also applies to your Companion"]={nil,"Life Leech from your Hits also applies to your Companion "} c["Life Leech is Converted to Energy Shield Leech"]={{[1]={flags=0,keywordFlags=0,name="GhostReaver",type="FLAG",value=true}},nil} c["Life Leech is Instant"]={{[1]={flags=0,keywordFlags=0,name="InstantLifeLeech",type="BASE",value=100}},nil} -c["Life Leech recovers based on your Chaos damage instead of Physical damage"]={nil,"Life Leech recovers based on your Chaos damage instead of Physical damage "} +c["Life Leech recovers based on your Chaos damage instead of Physical damage"]={{[1]={flags=0,keywordFlags=0,name="LifeLeechBasedOnChaosDamage",type="FLAG",value=true},[2]={flags=0,keywordFlags=0,name="Condition:NoLifeLeechFromPhysicalDamage",type="FLAG",value=true}},nil} c["Life Leech recovers based on your Elemental damage as well as Physical damage"]={{[1]={flags=0,keywordFlags=0,name="LifeLeechBasedOnElementalDamage",type="FLAG",value=true}},nil} c["Life Leeched from Empowered Attacks is Instant"]={nil,"Life Leeched from Empowered Attacks is Instant "} c["Life Recharges"]={nil,"Life Recharges "} diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 2e28773e1a..cee3385ee3 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -2463,6 +2463,44 @@ local specialModList = { ["leeche?s? ([%d%.]+)%% of (%a+) attack damage as life"] = function(num, _, dmgType) return { mod(firstToUpper(dmgType) .. "DamageLifeLeech", "BASE", num, nil, ModFlag.Attack, 0), } end, + ["(%a+%s?%a*) leech recovers based on y?o?u?r? ?(%a+) damage as well as physical damage"] = function(_, resource, dmgType) + local resourceTable = { + ["life"] = "Life", + ["mana"] = "Mana", + ["energy shield"] = "EnergyShield", + } + if resourceTable[resource] then + return { flag(resourceTable[resource] .. "LeechBasedOn" .. firstToUpper(dmgType) .. "Damage"), } + else + return { nil } + end + end, + ["(%a+%s?%a*) leech recovers based on y?o?u?r? ?(%a+) damage instead of physical damage"] = function(_, resource, dmgType) + local resourceTable = { + ["life"] = "Life", + ["mana"] = "Mana", + ["energy shield"] = "EnergyShield", + } + if resourceTable[resource] then + return { flag(resourceTable[resource] .. "LeechBasedOn" .. firstToUpper(dmgType) .. "Damage"), flag("Condition:No" .. resourceTable[resource] .. "LeechFromPhysicalDamage"), } + else + return { nil } + end + end, + ["leech recovers based on y?o?u?r? ?(%a+) damage as well as physical damage"] = function(_, dmgType) + return { + flag("LifeLeechBasedOn" .. firstToUpper(dmgType) .. "Damage"), + flag("ManaLeechBasedOn" .. firstToUpper(dmgType) .. "Damage"), + flag("EnergyShieldLeechBasedOn" .. firstToUpper(dmgType) .. "Damage"), + } + end, + ["leech recovers based on y?o?u?r? ?(%a+) damage instead of physical damage"] = function(_, dmgType) + return { + flag("LifeLeechBasedOn" .. firstToUpper(dmgType) .. "Damage"), flag("Condition:NoLifeLeechFromPhysicalDamage"), + flag("ManaLeechBasedOn" .. firstToUpper(dmgType) .. "Damage"), flag("Condition:NoManaLeechFromPhysicalDamage"), + flag("EnergyShieldLeechBasedOn" .. firstToUpper(dmgType) .. "Damage"), flag("Condition:NoEnergyShieldLeechFromPhysicalDamage"), + } + end, -- Amazon ["chance to hit with attacks can exceed 100%%"] = {flag("Condition:HitChanceCanExceed100", { type = "Skilltype", skillType = SkillType.Attack})}, ["gain additional critical hit chance equal to (%d+)%% of excess chance to hit with attacks"] = function(num) return { mod("CritChance", "BASE", 0.01 * num, { type = "Multiplier", var = "ExcessHitChance" }, { type = "SkillType", skillType = SkillType.Attack})} end, @@ -2472,7 +2510,6 @@ local specialModList = { mod(firstToUpper(dmgType) .. "Min", "BASE", 1, nil, ModFlag.Attack, { type = "PercentStat", stat = "AccuracyOnWeapon 2", percent = num }, { type = "SkillType", skillType = SkillType.NonWeaponAttack, neg = true } , { type = "Condition", var = "OffHandAttack" }), mod(firstToUpper(dmgType) .. "Max", "BASE", 1, nil, ModFlag.Attack, { type = "PercentStat", stat = "AccuracyOnWeapon 2", percent = num }, { type = "SkillType", skillType = SkillType.NonWeaponAttack, neg = true } , { type = "Condition", var = "OffHandAttack" }), } end, - ["life leech recovers based on your elemental damage as well as physical damage"] = { flag("LifeLeechBasedOnElementalDamage"), }, ["evasion rating from equipped helmet, gloves and boots is doubled"] = { mod("Evasion", "MORE", 100, { type = "SlotName", slotNameList = { "Helmet", "Boots", "Gloves" } }) }, ["evasion rating from equipped body armour is halved"] = { mod("Evasion", "MORE", -50, { type = "SlotName", slotName = "Body Armour" }) }, -- Ascendant @@ -3046,7 +3083,10 @@ local specialModList = { mod("DamageGainAs"..firstToUpper(strType), "BASE", tonumber(num2) * (num / 100), nil, ModFlag.Hit, 0), } end, ["effect and duration of flames of chayula on you is doubled"] = { mod("Multiplier:FlameEffect", "BASE", 1) } , - ["mana leech recovers based on other damage types damage as well as physical damage"] = { flag("PhysicalAsAllDamageManaLeech") }, + ["mana leech recovers based on other damage types damage as well as physical damage"] = { -- legacy wording + flag("ManaLeechBasedOnElementalDamage"), + flag("ManaLeechBasedOnChaosDamage"), + }, -- Monk - Invoker ["critical hits ignore non%-negative enemy monster elemental resistances"] = { flag("IgnoreNonNegativeEleRes", { type = "Condition", var = "CriticalStrike" }) }, ["(%d+)%% chance on shocking enemies to created shocked ground"] = { mod("ShockBase", "BASE", data.nonDamagingAilment["Shock"].default, { type = "ActorCondition", actor = "enemy", var = "OnShockedGround" }) }, From 57ae62b0076937ede78a17a11e829a82e3400e48 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:25:28 +0200 Subject: [PATCH 8/8] Add support for Energy Shield leech mods Mostly just for testing, as these mods don't exist in the game yet, but might be added in the future via unique items. --- src/Modules/ModParser.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index cee3385ee3..4423b1561f 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -2457,12 +2457,18 @@ local specialModList = { ["leeche?s? ([%d%.]+)%% of (%a+) damage as life"] = function(num, _, dmgType) return { mod(firstToUpper(dmgType) .. "DamageLifeLeech", "BASE", num), } end, + ["leeche?s? ([%d%.]+)%% of (%a+) damage as energy shield"] = function(num, _, dmgType) return { + mod(firstToUpper(dmgType) .. "DamageEnergyShieldLeech", "BASE", num), + } end, ["leeche?s? ([%d%.]+)%% of (%a+) attack damage as mana"] = function(num, _, dmgType) return { mod(firstToUpper(dmgType) .. "DamageManaLeech", "BASE", num, nil, ModFlag.Attack, 0), } end, ["leeche?s? ([%d%.]+)%% of (%a+) attack damage as life"] = function(num, _, dmgType) return { mod(firstToUpper(dmgType) .. "DamageLifeLeech", "BASE", num, nil, ModFlag.Attack, 0), } end, + ["leeche?s? ([%d%.]+)%% of (%a+) attack damage as energy shield"] = function(num, _, dmgType) return { + mod(firstToUpper(dmgType) .. "DamageEnergyShieldLeech", "BASE", num, nil, ModFlag.Attack, 0), + } end, ["(%a+%s?%a*) leech recovers based on y?o?u?r? ?(%a+) damage as well as physical damage"] = function(_, resource, dmgType) local resourceTable = { ["life"] = "Life",