From 6e20c7b2d18ec1cea772c5851b05313861d4fab5 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Fri, 29 May 2026 11:06:35 +0200 Subject: [PATCH 1/4] Change combined hit flag to `combinesHitsWhenDualWielding` In PoE2 `doubleHitsWhenDualWielding` does not actually cause a combined hit from both weapons, but instead causes two separate near simultaneous hits. In order to still be able to reuse the old logic, I changed all checks for `doublenHitsWhenDualWielding` to `combinesHitsWhenDualWielding` instead. Support for the old flag will be in separate commit. --- src/Modules/CalcOffence.lua | 12 ++++++------ src/Modules/CalcTriggers.lua | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index fd2a91bc2..5922ee2da 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -2448,7 +2448,7 @@ function calcs.offence(env, actor, activeSkill) elseif mode == "AVERAGE" then output[stat] = ((output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)) / 2 elseif mode == "CRIT" then - if skillFlags.bothWeaponAttack and skillData.doubleHitsWhenDualWielding then + if skillFlags.bothWeaponAttack and skillData.combinesHitsWhenDualWielding then output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0) - ((output.MainHand[stat] or 0) * (output.OffHand[stat] or 0) / 100) else output[stat] = ((output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)) / 2 @@ -2522,7 +2522,7 @@ function calcs.offence(env, actor, activeSkill) end elseif mode == "DPS" then output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0) - if not skillData.doubleHitsWhenDualWielding then + if not skillData.combinesHitsWhenDualWielding then output[stat] = output[stat] / 2 end end @@ -4463,7 +4463,7 @@ function calcs.offence(env, actor, activeSkill) if breakdown then breakdown.AverageDamage = { } t_insert(breakdown.AverageDamage, "Both weapons:") - if skillData.doubleHitsWhenDualWielding then + if skillData.combinesHitsWhenDualWielding then t_insert(breakdown.AverageDamage, s_format("%.1f + %.1f ^8(skill hits with both weapons at once)", output.MainHand.AverageDamage, output.OffHand.AverageDamage)) else t_insert(breakdown.AverageDamage, s_format("(%.1f + %.1f) / 2 ^8(skill alternates weapons)", output.MainHand.AverageDamage, output.OffHand.AverageDamage)) @@ -4472,7 +4472,7 @@ function calcs.offence(env, actor, activeSkill) if skillFlags.isPvP then breakdown.PvpAverageDamage = { } t_insert(breakdown.PvpAverageDamage, "Both weapons:") - if skillData.doubleHitsWhenDualWielding then + if skillData.combinesHitsWhenDualWielding then t_insert(breakdown.PvpAverageDamage, s_format("%.1f + %.1f ^8(skill hits with both weapons at once)", output.MainHand.PvpAverageDamage, output.OffHand.PvpAverageDamage)) else t_insert(breakdown.PvpAverageDamage, s_format("(%.1f + %.1f) / 2 ^8(skill alternates weapons)", output.MainHand.PvpAverageDamage, output.OffHand.PvpAverageDamage)) @@ -5938,7 +5938,7 @@ function calcs.offence(env, actor, activeSkill) end if skillFlags.impale then local mainHandImpaleDPS, offHandImpaleDPS - if skillFlags.attack and skillData.doubleHitsWhenDualWielding and skillFlags.bothWeaponAttack then + if skillFlags.attack and skillData.combinesHitsWhenDualWielding and skillFlags.bothWeaponAttack then -- separately combine mainHandImpaleDPS = output.MainHand.impaleStoredHitAvg * ((output.MainHand.ImpaleModifier or 1) - 1) * output.MainHand.HitChance / 100 * skillData.dpsMultiplier offHandImpaleDPS = output.OffHand.impaleStoredHitAvg * ((output.OffHand.ImpaleModifier or 1) - 1) * output.OffHand.HitChance / 100 * skillData.dpsMultiplier @@ -5960,7 +5960,7 @@ function calcs.offence(env, actor, activeSkill) output.CombinedDPS = output.CombinedDPS + output.ImpaleDPS if breakdown then breakdown.ImpaleDPS = {} - if skillFlags.attack and skillData.doubleHitsWhenDualWielding and skillFlags.bothWeaponAttack then + if skillFlags.attack and skillData.combinesHitsWhenDualWielding and skillFlags.bothWeaponAttack then t_insert(breakdown.ImpaleDPS, s_format("Main Hand:")) t_insert(breakdown.ImpaleDPS, s_format("%.2f ^8(MH average physical hit before mitigation)", output.MainHand.impaleStoredHitAvg)) t_insert(breakdown.ImpaleDPS, s_format("x %.2f ^8(MH chance to hit)", output.MainHand.HitChance / 100)) diff --git a/src/Modules/CalcTriggers.lua b/src/Modules/CalcTriggers.lua index d861968b9..2ee4867f7 100644 --- a/src/Modules/CalcTriggers.lua +++ b/src/Modules/CalcTriggers.lua @@ -429,7 +429,7 @@ local function defaultTriggerHandler(env, config) end -- Dual wield triggers - if trigRate and source and env.player.weaponData1.type and env.player.weaponData2.type and not source.skillData.doubleHitsWhenDualWielding and (source.skillTypes[SkillType.Melee] or source.skillTypes[SkillType.Attack]) and actor.mainSkill.triggeredBy and actor.mainSkill.triggeredBy.grantedEffect.support and actor.mainSkill.triggeredBy.grantedEffect.fromItem then + if trigRate and source and env.player.weaponData1.type and env.player.weaponData2.type and not source.skillData.combinesHitsWhenDualWielding and (source.skillTypes[SkillType.Melee] or source.skillTypes[SkillType.Attack]) and actor.mainSkill.triggeredBy and actor.mainSkill.triggeredBy.grantedEffect.support and actor.mainSkill.triggeredBy.grantedEffect.fromItem then trigRate = trigRate / 2 if breakdown then t_insert(breakdown.EffectiveSourceRate, 2, s_format("/ 2 ^8(due to dual wielding)")) @@ -722,7 +722,7 @@ local function defaultTriggerHandler(env, config) local sourceHitChance = GlobalCache.cachedData[env.mode][uuid].HitChance or 0 if sourceHitChance ~= 100 then -- Some skills hit with both weapons at the same time. Each weapon rolls accuracy and crit independently - if source and env.player.weaponData1.type and env.player.weaponData2.type and source.skillData.doubleHitsWhenDualWielding then + if source and env.player.weaponData1.type and env.player.weaponData2.type and source.skillData.combinesHitsWhenDualWielding then local mainHandHit = GlobalCache.cachedData[env.mode][uuid].Env.player.output.MainHand.HitChance local offHandHit = GlobalCache.cachedData[env.mode][uuid].Env.player.output.OffHand.HitChance local bothHit = mainHandHit * offHandHit / 100 @@ -747,7 +747,7 @@ local function defaultTriggerHandler(env, config) local sourceCritChance = GlobalCache.cachedData[env.mode][uuid].CritChance or 0 if sourceCritChance ~= 100 then -- Some skills hit with both weapons at the same time. Each weapon rolls accuracy and crit independently - if source and env.player.weaponData1.type and env.player.weaponData2.type and source.skillData.doubleHitsWhenDualWielding then + if source and env.player.weaponData1.type and env.player.weaponData2.type and source.skillData.combinesHitsWhenDualWielding then local mainHandCrit = GlobalCache.cachedData[env.mode][uuid].Env.player.output.MainHand.CritChance local offHandCrit = GlobalCache.cachedData[env.mode][uuid].Env.player.output.OffHand.CritChance local bothHit = mainHandCrit * offHandCrit / 100 From 8c7b74ca5e0813134b74fba9aad78119a5f93f0c Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Fri, 29 May 2026 11:20:18 +0200 Subject: [PATCH 2/4] Make `combineStat("HARMONICMEAN")` use double hits --- src/Modules/CalcOffence.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 5922ee2da..ab0a4abe0 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -2458,6 +2458,9 @@ function calcs.offence(env, actor, activeSkill) output[stat] = 0 else output[stat] = 2 / ((1 / output.MainHand[stat]) + (1 / output.OffHand[stat])) + if skillData.doubleHitsWhenDualWielding then + output[stat] = output[stat] * 2 + end end elseif mode == "CHANCE" then if output.MainHand[stat] and output.OffHand[stat] then From d299f283ac0758c9297c2b2770f942aa59d50b1e Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Fri, 29 May 2026 14:45:38 +0200 Subject: [PATCH 3/4] Update the `Speed` breakdown Now correctly reflects behavior of the three possible dual wield hitRate scenarios: 1. One combined hit from both weapons (Harmonic Mean) 2. Two separate simultaneous hits from each weapon (2x Harmonic Mean) 3. Alternating between Mainhand and Offhand (Harmonic Mean) --- src/Modules/CalcOffence.lua | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index ab0a4abe0..972dbb384 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -3040,11 +3040,26 @@ function calcs.offence(env, actor, activeSkill) end elseif skillFlags.bothWeaponAttack then if breakdown then - breakdown.Speed = { - "Both weapons:", - s_format("2 / (1 / %.2f + 1 / %.2f)", output.MainHand.Speed, output.OffHand.Speed), - s_format("= %.2f", output.Speed), - } + if skillData.combinesHitsWhenDualWielding then + breakdown.Speed = { + "Combined hit from both weapons:", + s_format("1 / (1 / %.2f + 1 / %.2f)", output.MainHand.Speed, output.OffHand.Speed), + s_format("= %.2f", output.Speed), + } + elseif skillData.doubleHitsWhenDualWielding then + breakdown.Speed = { + "Simultaneous hits from each weapon:", + s_format("2 / (1 / %.2f + 1 / %.2f)", output.MainHand.Speed, output.OffHand.Speed), + s_format("%.2f * 2 ^8(hits twice per attack)", output.Speed / 2), + s_format("= %.2f", output.Speed), + } + else + breakdown.Speed = { + "Alternating both weapons:", + s_format("2 / (1 / %.2f + 1 / %.2f)", output.MainHand.Speed, output.OffHand.Speed), + s_format("= %.2f", output.Speed), + } + end end end if skillData.channelTimeMultiplier then From 9e477901ada9426472369e7f73cad6d3d653f2fe Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Fri, 29 May 2026 17:40:49 +0200 Subject: [PATCH 4/4] Use "DPS" mod instead of directly changing `Speed` This is still not ideal imo, but it's mostly due to the fact that we don't have a good breakdown for "DPS" mods --- src/Modules/CalcOffence.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 972dbb384..33ceec1c8 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -2458,9 +2458,6 @@ function calcs.offence(env, actor, activeSkill) output[stat] = 0 else output[stat] = 2 / ((1 / output.MainHand[stat]) + (1 / output.OffHand[stat])) - if skillData.doubleHitsWhenDualWielding then - output[stat] = output[stat] * 2 - end end elseif mode == "CHANCE" then if output.MainHand[stat] and output.OffHand[stat] then @@ -3050,7 +3047,7 @@ function calcs.offence(env, actor, activeSkill) breakdown.Speed = { "Simultaneous hits from each weapon:", s_format("2 / (1 / %.2f + 1 / %.2f)", output.MainHand.Speed, output.OffHand.Speed), - s_format("%.2f * 2 ^8(hits twice per attack)", output.Speed / 2), + s_format("%.2f * 2 ^8(hits twice per attack)", output.Speed), s_format("= %.2f", output.Speed), } else @@ -3745,6 +3742,12 @@ function calcs.offence(env, actor, activeSkill) output.DoubleDamageEffect = output.DoubleDamageChance / 100 output.ScaledDamageEffect = output.ScaledDamageEffect * (1 + output.DoubleDamageEffect + output.TripleDamageEffect) + -- Dual wield DPS multiplier + -- NOTE: This solution is a bit "hacky", but ensures that the hit rate multiplier for dual wielding isn't applied multiple times + if skillFlags.bothWeaponAttack and skillData.doubleHitsWhenDualWielding and pass.label == "Off Hand" then + skillModList:NewMod("DPS", "MORE", 100, "Hits with both weapons") + end + skillData.dpsMultiplier = ( skillData.dpsMultiplier or 1 ) * calcLib.mod(skillModList, skillCfg, "DPS") local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * skillData.dpsMultiplier @@ -4483,6 +4486,8 @@ function calcs.offence(env, actor, activeSkill) t_insert(breakdown.AverageDamage, "Both weapons:") if skillData.combinesHitsWhenDualWielding then t_insert(breakdown.AverageDamage, s_format("%.1f + %.1f ^8(skill hits with both weapons at once)", output.MainHand.AverageDamage, output.OffHand.AverageDamage)) + elseif skillData.doubleHitsWhenDualWielding then + t_insert(breakdown.AverageDamage, s_format("%.1f + %.1f ^8(skill hits once with each weapon)", output.MainHand.AverageDamage, output.OffHand.AverageDamage)) else t_insert(breakdown.AverageDamage, s_format("(%.1f + %.1f) / 2 ^8(skill alternates weapons)", output.MainHand.AverageDamage, output.OffHand.AverageDamage)) end