diff --git a/spec/System/TestAttacks_spec.lua b/spec/System/TestAttacks_spec.lua index c79e00738..63f88687a 100644 --- a/spec/System/TestAttacks_spec.lua +++ b/spec/System/TestAttacks_spec.lua @@ -282,4 +282,166 @@ describe("TestAttacks", function() assert.is_true(math.abs(bifurcateChance - build.calcsTab.mainOutput.MainHand.CritBifurcates) < 0.000001) assert.are.equals(1 + critBonusMultiplier, build.calcsTab.mainOutput.MainHand.AverageHit) end) + + -- Dual Wield tests + local setupDualWieldTestConditions = function() + local slowHighDmgMace = [[ + Slow High Crit High Damage Mace + Marauding Mace + Quality: 0 + 200% increased physical damage + 100% increased critical hit chance + -25% increased attack speed + ]] + + local fastLowDmgMace = [[ + Fast Low Crit Low Damage Mace + Marauding Mace + Quality: 0 + -50% increased physical damage + -100% increased critical hit chance + 50% increased attack speed + ]] + + build.itemsTab:CreateDisplayItemFromRaw(slowHighDmgMace) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + build.itemsTab.slots["Weapon 1"]:SetSelItemId(build.itemsTab.items[1].id) + + build.itemsTab:CreateDisplayItemFromRaw(fastLowDmgMace) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + build.itemsTab.slots["Weapon 2"]:SetSelItemId(build.itemsTab.items[2].id) + + build.configTab.input.customMods = [[ + nearby enemies have 100% less armour + your hits can't be evaded + ]] + build.configTab:BuildModList() + runCallback("OnFrame") + end + + local function harmonicMean(a, b) + return 2 / (1/a + 1/b) + end + + it("correctly calculates dual wield DPS for double hits", function() + setupDualWieldTestConditions() + build.skillsTab:PasteSocketGroup("skillId:MeleeMaceMacePlayer Mace Strike 20/0 1") + runCallback("OnFrame") + build.calcsTab:BuildOutput() + runCallback("OnFrame") + + -- Attack Speed + local mainHandSpeed = build.calcsTab.mainOutput.MainHand.Speed + local offHandSpeed = build.calcsTab.mainOutput.OffHand.Speed + local combinedSpeed = harmonicMean(mainHandSpeed, offHandSpeed) + assert.are.equals(round(combinedSpeed, 4), round(build.calcsTab.mainOutput.Speed, 4)) + + -- Average Hit + local mainHandAvgDmg = build.calcsTab.mainOutput.MainHand.AverageDamage + local offHandAvgDmg = build.calcsTab.mainOutput.OffHand.AverageDamage + local combinedAvgDmg = build.calcsTab.mainOutput.AverageDamage + assert.are.equals(round((mainHandAvgDmg + offHandAvgDmg) / 2, 4), round(combinedAvgDmg, 4)) + + -- DPS (hits twice per attack) + local combinedDPS = build.calcsTab.mainOutput.TotalDPS + assert.are.equals(round(combinedAvgDmg * combinedSpeed * 2,4), round(combinedDPS,4)) + end) + + it("correctly calculates dual wield crit chance for double hits", function() + setupDualWieldTestConditions() + build.skillsTab:PasteSocketGroup("skillId:MeleeMaceMacePlayer Mace Strike 20/0 1") + runCallback("OnFrame") + build.calcsTab:BuildOutput() + runCallback("OnFrame") + + -- Double hits roll crit individually per weapon, so should be average + local mainHandCritChance = build.calcsTab.mainOutput.MainHand.CritChance + local offHandCritChance = build.calcsTab.mainOutput.OffHand.CritChance + local combinedCritChance = (mainHandCritChance + offHandCritChance) / 2 + assert.are.equals(combinedCritChance, build.calcsTab.mainOutput.CritChance) + end) + + it("correctly calculates dual wield DPS for alternating hits", function() + setupDualWieldTestConditions() + build.skillsTab:PasteSocketGroup("Armour Breaker 20/0 1") + runCallback("OnFrame") + build.calcsTab:BuildOutput() + runCallback("OnFrame") + + -- Attack Speed + local mainHandSpeed = build.calcsTab.mainOutput.MainHand.Speed + local offHandSpeed = build.calcsTab.mainOutput.OffHand.Speed + local combinedSpeed = harmonicMean(mainHandSpeed, offHandSpeed) + assert.are.equals(round(combinedSpeed, 4), round(build.calcsTab.mainOutput.Speed, 4)) + + -- Average Hit + local mainHandAvgDmg = build.calcsTab.mainOutput.MainHand.AverageDamage + local offHandAvgDmg = build.calcsTab.mainOutput.OffHand.AverageDamage + local combinedAvgDmg = build.calcsTab.mainOutput.AverageDamage + assert.are.equals(round((mainHandAvgDmg + offHandAvgDmg) / 2, 4), round(combinedAvgDmg, 4)) + + -- DPS (hits once per attack) + local combinedDPS = build.calcsTab.mainOutput.TotalDPS + assert.are.equals(round(combinedAvgDmg * combinedSpeed, 4), round(combinedDPS, 4)) + end) + + it("correctly calculates dual wield crit chance for alternating hits", function() + setupDualWieldTestConditions() + build.skillsTab:PasteSocketGroup("Armour Breaker 20/0 1") + runCallback("OnFrame") + build.calcsTab:BuildOutput() + runCallback("OnFrame") + + -- Alternating hits roll crit individually per weapon, so should be average + local mainHandCritChance = build.calcsTab.mainOutput.MainHand.CritChance + local offHandCritChance = build.calcsTab.mainOutput.OffHand.CritChance + local combinedCritChance = (mainHandCritChance + offHandCritChance) / 2 + assert.are.equals(combinedCritChance, build.calcsTab.mainOutput.CritChance) + end) + + --[[ + NOTE: the following section contains tests for "combined hits", which PoE2 doesn't have as of 2026-06-02, + which means the tests were written for a temporary test skill that will not be committed. + The test can be updated by simply replacing `"skillId:MeleeMaceMacePlayerCombinedTEST Mace Strike TEST 20/0 1"` + with actual skill data once available + ]] + --[[ it("correctly calculates dual wield DPS for combined hits", function() + setupDualWieldTestConditions() + build.skillsTab:PasteSocketGroup("skillId:MeleeMaceMacePlayerCombinedTEST Mace Strike TEST 20/0 1") + runCallback("OnFrame") + build.calcsTab:BuildOutput() + runCallback("OnFrame") + + -- Attack Speed + local mainHandSpeed = build.calcsTab.mainOutput.MainHand.Speed + local offHandSpeed = build.calcsTab.mainOutput.OffHand.Speed + local combinedSpeed = harmonicMean(mainHandSpeed, offHandSpeed) + assert.are.equals(round(combinedSpeed, 4), round(build.calcsTab.mainOutput.Speed, 4)) + + -- Average Hit + local mainHandAvgDmg = build.calcsTab.mainOutput.MainHand.AverageDamage + local offHandAvgDmg = build.calcsTab.mainOutput.OffHand.AverageDamage + local combinedAvgDmg = build.calcsTab.mainOutput.AverageDamage + assert.are.equals(round((mainHandAvgDmg + offHandAvgDmg), 4), round(combinedAvgDmg, 4)) + + -- DPS (hits twice per attack) + local combinedDPS = build.calcsTab.mainOutput.TotalDPS + assert.are.equals(round(combinedAvgDmg * combinedSpeed, 4), round(combinedDPS,4)) + end) + + it("correctly calculates dual wield crit chance for combined hits", function() + setupDualWieldTestConditions() + build.skillsTab:PasteSocketGroup("skillId:MeleeMaceMacePlayerCombinedTEST Mace Strike TEST 20/0 1") + runCallback("OnFrame") + build.calcsTab:BuildOutput() + runCallback("OnFrame") + + -- combined hits count whole attack as crit, as long as one hand rolls crit) + local mainHandCritChance = build.calcsTab.mainOutput.MainHand.CritChance + local offHandCritChance = build.calcsTab.mainOutput.OffHand.CritChance + local combinedCritChance = mainHandCritChance + offHandCritChance - (mainHandCritChance * offHandCritChance / 100) + assert.are.equals(combinedCritChance, build.calcsTab.mainOutput.CritChance) + end) ]] end) diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index 833b60116..3a813ace1 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -629,6 +629,21 @@ function SkillsTabClass:CopySocketGroup(socketGroup) Copy(skillText) end +--- Parses pasted socketGroup or custom test string to generate new socketGroup +--- @param testInput string? optional string input mainly used for tests +--- +--- **Expected `testInput` Format:** +--- ```text +--- [Label: ] (Optional) +--- [Slot: ] (Optional) +--- [skillId:] / [STATE] [C] [+/-CorruptLevel] +--- ``` +--- `skillId` is only needed if skill name is not unique +--- `STATE` only used to disable gem via `DISABLED` +--- +--- **Example lines:** +--- * `"skillId:LightningSpearPlayer Lightning Spear 20/7 2 C +1"` (Level 20, 7 Quality Lightning Spear, with +1 Level Corruption and Count set to 2 ) +--- * `"RhoaMountPlayer Rhoa Mount 10/0 DISABLED 1"` (Level 10, 0 quality Rhoa Mount with count 1 that has been disabled) function SkillsTabClass:PasteSocketGroup(testInput) local skillText = sanitiseText(Paste() or testInput) if skillText then @@ -642,8 +657,21 @@ function SkillsTabClass:PasteSocketGroup(testInput) newGroup.slot = slot end for line in skillText:gmatch("([^\r\n]+)") do + local currentLine = line -- reassignment to local var to avoid modifying iter var + + -- Check if specific skillId was provided via `testInput` + local skillId = currentLine:match("^skillId:(%w+) ") + if skillId then + currentLine = currentLine:gsub("^skillId:%w+ ", "") + end + local nameSpec, level, quality, state, count, cFlag, cLevel = - line:match("^([ %a':]+) (%d+)/(%d+)%s*(%u*)%s+([%d%.]+)%s*(C?)([+%-]?%d*)%s*$") + currentLine:match("^([ %a':]+) (%d+)/(%d+)%s*(%u*)%s+([%d%.]+)%s*(C?)([+%-]?%d*)%s*$") + + -- Ignore invalid or mismatched skillId + if skillId and not (self.build.data.skills[skillId] and self.build.data.skills[skillId].baseTypeName == nameSpec) then + skillId = nil + end if nameSpec then local skillMinion = nil local skillMinionCalcs = nil @@ -685,7 +713,8 @@ function SkillsTabClass:PasteSocketGroup(testInput) enableGlobal1 = true, enableGlobal2 = true, skillMinion = skillMinion, - skillMinionCalcs = skillMinionCalcs + skillMinionCalcs = skillMinionCalcs, + skillId = skillId }) end end diff --git a/src/Data/SkillStatMap.lua b/src/Data/SkillStatMap.lua index f7260a6a8..caa0fe5b5 100644 --- a/src/Data/SkillStatMap.lua +++ b/src/Data/SkillStatMap.lua @@ -278,6 +278,9 @@ return { ["skill_double_hits_when_dual_wielding"] = { skill("doubleHitsWhenDualWielding", true), }, +["skill_combines_hits_when_dual_wielding"] = { -- NOTE: This is before PoE2 has "combined hit" dual wield skills, so stat will have to be updated in the future + skill("combinesHitsWhenDualWielding", true), +}, ["support_spell_echo_number_of_echo_cascades"] = { mod("RepeatCount", "BASE", nil, 0, 0, {type = "SkillType", skillType = SkillType.Cascadable }), }, diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 30336ce92..20250af90 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -1123,6 +1123,11 @@ function calcs.offence(env, actor, activeSkill) modDB:NewMod("DPS", "MORE", detonateTwice, "Grenade Activate Twice") end + -- Dual wield DPS multiplier + if skillFlags.bothWeaponAttack and skillData.doubleHitsWhenDualWielding then + skillModList:NewMod("DPS", "MORE", 100, "Hits with both weapons") + end + if skillModList:Flag(nil, "HasSeals") and not skillModList:Flag(nil, "NoRepeatBonuses") then -- Applies seal bonuses based on seal count local totalCastSpeed = 1 / activeSkill.activeEffect.grantedEffect.castTime * calcLib.mod(skillModList, skillCfg, "Speed") @@ -2454,7 +2459,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 @@ -2528,7 +2533,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 @@ -3043,11 +3048,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 ^8(hits twice per attack)", output.Speed), + 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 @@ -3859,9 +3879,11 @@ function calcs.offence(env, actor, activeSkill) output.DoubleDamageEffect = output.DoubleDamageChance / 100 output.ScaledDamageEffect = output.ScaledDamageEffect * (1 + output.DoubleDamageEffect + output.TripleDamageEffect) - skillData.dpsMultiplier = ( skillData.dpsMultiplier or 1 ) * calcLib.mod(skillModList, skillCfg, "DPS") - local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * skillData.dpsMultiplier + + output.DpsMultiplier = ( skillData.dpsMultiplier or 1 ) * calcLib.mod(skillModList, skillCfg, "DPS") + + local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * output.DpsMultiplier local enemyRarity = (enemyDB:Flag(nil, "Condition:Unique") and "Unique" or (enemyDB:Flag(nil, "Condition:RareOrUnique") and "Rare" or "Normal")) -- Calculate culling DPS @@ -4403,7 +4425,7 @@ function calcs.offence(env, actor, activeSkill) local repeatPenalty = skillModList:Flag(nil, "HasSeals") and skillModList:Flag(nil, "DamageSeal") and not skillModList:Flag(nil, "NoRepeatBonuses") and calcLib.mod(skillModList, skillCfg, "SealRepeatPenalty") or 1 globalOutput.AverageBurstDamage = output.AverageDamage + output.AverageDamage * (globalOutput.AverageBurstHits - 1) * repeatPenalty or 0 globalOutput.ShowBurst = globalOutput.AverageBurstHits > 1 - output.TotalDPS = output.AverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * skillData.dpsMultiplier * quantityMultiplier + output.TotalDPS = output.AverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * output.DpsMultiplier * quantityMultiplier if breakdown then if output.CritEffect ~= 1 then breakdown.AverageHit = { } @@ -4495,7 +4517,7 @@ function calcs.offence(env, actor, activeSkill) local portionElemental = (output.AverageHit / PvpTvalue / PvpElemental2 ) ^ PvpElemental1 * PvpTvalue * PvpElemental2 * percentageElemental output.PvpAverageHit = (portionNonElemental + portionElemental) * PvpMultiplier output.PvpAverageDamage = output.PvpAverageHit * output.HitChance / 100 - output.PvpTotalDPS = output.PvpAverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * skillData.dpsMultiplier + output.PvpTotalDPS = output.PvpAverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * output.DpsMultiplier -- fix for these being nan if output.PvpAverageHit ~= output.PvpAverageHit then @@ -4557,6 +4579,7 @@ function calcs.offence(env, actor, activeSkill) combineStat("CritBifurcates", "AVERAGE") combineStat("AverageDamage", "DPS") combineStat("PvpAverageDamage", "DPS") + combineStat("DpsMultiplier", "DPS") combineStat("TotalDPS", "DPS") combineStat("PvpTotalDPS", "DPS") combineStat("LifeLeechDuration", "DPS") @@ -4626,8 +4649,10 @@ 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)) + elseif skillData.doubleHitsWhenDualWielding then + t_insert(breakdown.AverageDamage, s_format("(%.1f + %.1f) / 2 ^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 @@ -4635,7 +4660,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)) @@ -4669,8 +4694,8 @@ function calcs.offence(env, actor, activeSkill) output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(cast rate)", output.Speed), } end - if skillData.dpsMultiplier ~= 1 then - t_insert(breakdown.TotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier)) + if output.DpsMultiplier ~= 1 then + t_insert(breakdown.TotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", output.DpsMultiplier)) end if quantityMultiplier > 1 then t_insert(breakdown.TotalDPS, s_format("x %g ^8(quantity multiplier for this skill)", quantityMultiplier)) @@ -4687,8 +4712,8 @@ function calcs.offence(env, actor, activeSkill) s_format("%.1f ^8(average pvp hit)", output.PvpAverageDamage), output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(%s rate)", output.Speed, rateType), } - if skillData.dpsMultiplier ~= 1 then - t_insert(breakdown.PvpTotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier)) + if output.DpsMultiplier ~= 1 then + t_insert(breakdown.PvpTotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", output.DpsMultiplier)) end if quantityMultiplier > 1 then t_insert(breakdown.PvpTotalDPS, s_format("x %g ^8(quantity multiplier for this skill)", quantityMultiplier)) @@ -4698,7 +4723,7 @@ function calcs.offence(env, actor, activeSkill) end if skillFlags.minion then - skillData.summonSpeed = output.SummonedMinionsPerCast * (output.HitSpeed or output.Speed) * skillData.dpsMultiplier + skillData.summonSpeed = output.SummonedMinionsPerCast * (output.HitSpeed or output.Speed) * output.DpsMultiplier end -- Calculate leech rates @@ -4744,7 +4769,7 @@ function calcs.offence(env, actor, activeSkill) output.ManaLeechGainRate = output.ManaLeechRate + output.ManaOnHitRate end if breakdown then - local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * skillData.dpsMultiplier + local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * output.DpsMultiplier if skillFlags.leechLife then breakdown.LifeLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.Life, "LifeLeechRate", output.MaxLifeLeechRate, output.LifeLeechDuration, output.LifeLeechInstantProportion, hitRate) end @@ -5042,7 +5067,7 @@ function calcs.offence(env, actor, activeSkill) local ailmentChance = output[ailment .. "ChanceOnHit"] / 100 * (1 - output.CritChance / 100) + output[ailment .. "ChanceOnCrit"] / 100 * output.CritChance / 100 -- The average number of ailment that will be active on the enemy at once - local ailmentStacks = output.HitChance / 100 * ailmentChance * skillData.dpsMultiplier + local ailmentStacks = output.HitChance / 100 * ailmentChance * output.DpsMultiplier local configStacks = enemyDB:Sum("BASE", nil, "Multiplier:" .. ailment .. "Stacks") if not skillData.triggeredOnDeath then if output.Cooldown then @@ -5084,8 +5109,8 @@ function calcs.offence(env, actor, activeSkill) elseif (globalOutput.HitSpeed or globalOutput.Speed) > 0 then t_insert(globalBreakdown[ailment .. "StackPotential"], s_format("* (%.2f / %.2f) ^8(Duration / Attack Time)", globalOutput[ailment .. "Duration"], (globalOutput.HitTime or output.Time))) end - if skillData.dpsMultiplier ~= 1 then - t_insert(globalBreakdown[ailment .. "StackPotential"], s_format("* %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier)) + if output.DpsMultiplier ~= 1 then + t_insert(globalBreakdown[ailment .. "StackPotential"], s_format("* %g ^8(DPS multiplier for this skill)", output.DpsMultiplier)) end end t_insert(globalBreakdown[ailment .. "StackPotential"], s_format("/ %d ^8(max number of stacks)", maxStacks)) @@ -5927,7 +5952,7 @@ function calcs.offence(env, actor, activeSkill) elseif band(dotCfg.keywordFlags, KeywordFlag.Trap) ~= 0 then speed = output.TrapThrowingSpeed end - output.TotalDot = m_min(output.TotalDotInstance * speed * output.Duration * skillData.dpsMultiplier * quantityMultiplier, data.misc.DotDpsCap) + output.TotalDot = m_min(output.TotalDotInstance * speed * output.Duration * output.DpsMultiplier * quantityMultiplier, data.misc.DotDpsCap) output.TotalDotCalcSection = output.TotalDot if breakdown then breakdown.TotalDot = { @@ -5935,8 +5960,8 @@ function calcs.offence(env, actor, activeSkill) s_format("x %.2f ^8(hits per second)", speed), s_format("x %.2f ^8(skill duration)", output.Duration), } - if skillData.dpsMultiplier ~= 1 then - t_insert(breakdown.TotalDot, s_format("x %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier)) + if output.DpsMultiplier ~= 1 then + t_insert(breakdown.TotalDot, s_format("x %g ^8(DPS multiplier for this skill)", output.DpsMultiplier)) end if quantityMultiplier > 1 then t_insert(breakdown.TotalDot, s_format("x %g ^8(quantity multiplier for this skill)", quantityMultiplier)) @@ -6120,13 +6145,13 @@ 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 + mainHandImpaleDPS = output.MainHand.impaleStoredHitAvg * ((output.MainHand.ImpaleModifier or 1) - 1) * output.MainHand.HitChance / 100 * output.DpsMultiplier + offHandImpaleDPS = output.OffHand.impaleStoredHitAvg * ((output.OffHand.ImpaleModifier or 1) - 1) * output.OffHand.HitChance / 100 * output.DpsMultiplier output.ImpaleDPS = mainHandImpaleDPS + offHandImpaleDPS else - output.ImpaleDPS = output.PhysicalStoredCombinedAvg * ((output.ImpaleModifier or 1) - 1) * output.HitChance / 100 * skillData.dpsMultiplier + output.ImpaleDPS = output.PhysicalStoredCombinedAvg * ((output.ImpaleModifier or 1) - 1) * output.HitChance / 100 * output.DpsMultiplier end if skillData.showAverage then output.WithImpaleDPS = output.AverageDamage + output.ImpaleDPS @@ -6142,7 +6167,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)) @@ -6163,8 +6188,8 @@ function calcs.offence(env, actor, activeSkill) if skillFlags.notAverage then t_insert(breakdown.ImpaleDPS, output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(%s rate)", output.Speed, skillFlags.attack and "attack" or "cast")) end - if skillData.dpsMultiplier ~= 1 then - t_insert(breakdown.ImpaleDPS, s_format("x %g ^8(dps multiplier for this skill)", skillData.dpsMultiplier)) + if output.DpsMultiplier ~= 1 then + t_insert(breakdown.ImpaleDPS, s_format("x %g ^8(dps multiplier for this skill)", output.DpsMultiplier)) end if quantityMultiplier > 1 then t_insert(breakdown.ImpaleDPS, s_format("x %g ^8(quantity multiplier for this skill)", quantityMultiplier)) diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 1568e93b7..f7dc2fb3d 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -1894,7 +1894,7 @@ function calcs.initEnv(build, mode, override, specEnv) if grantedEffect.name:match("^Companion:") or grantedEffect.name:match("^Spectre:") then group.displayLabel = (group.displayLabel and group.displayLabel..", " or "") .. gemInstance.nameSpec else - group.displayLabel = (group.displayLabel and group.displayLabel..", " or "") .. gemName or grantedEffect.name + group.displayLabel = gemName and ((group.displayLabel and group.displayLabel..", " or "") .. gemName) or grantedEffect.name end end end diff --git a/src/Modules/CalcTriggers.lua b/src/Modules/CalcTriggers.lua index 975ad5d77..5dafee82a 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