@@ -30,6 +30,35 @@ describe("TestAttacks", function()
3030 assert .are .equals (2 + 0.25 , build .calcsTab .mainOutput .CritMultiplier )
3131 end )
3232
33+ it (" local Critical Damage Bonus on a weapon does not apply to spells (issue #2199)" , function ()
34+ -- baseline: a spell's crit multi with a plain quarterstaff (no local crit damage)
35+ build .itemsTab :CreateDisplayItemFromRaw ([[
36+ New Item
37+ Razor Quarterstaff
38+ ]] )
39+ build .itemsTab :AddDisplayItem ()
40+ build .skillsTab :PasteSocketGroup (" Fireball 20/0 1" )
41+ build .mainSocketGroup = 1
42+ runCallback (" OnFrame" )
43+ local baseSpellCritMulti = build .calcsTab .mainOutput .CritMultiplier
44+
45+ newBuild ()
46+
47+ -- same spell + quarterstaff, now with a local "+X% to Critical Damage Bonus" affix
48+ build .itemsTab :CreateDisplayItemFromRaw ([[
49+ New Item
50+ Razor Quarterstaff
51+ +50% to Critical Damage Bonus
52+ ]] )
53+ build .itemsTab :AddDisplayItem ()
54+ build .skillsTab :PasteSocketGroup (" Fireball 20/0 1" )
55+ build .mainSocketGroup = 1
56+ runCallback (" OnFrame" )
57+
58+ -- the weapon-local crit damage must NOT reach the spell (it isn't using the weapon)
59+ assert .are .equals (baseSpellCritMulti , build .calcsTab .mainOutput .CritMultiplier )
60+ end )
61+
3362 it (" correctly converts spell damage per stat to attack damage" , function ()
3463 assert .are .equals (0 , build .calcsTab .mainEnv .player .modDB :Sum (" INC" , { flags = ModFlag .Attack }, " Damage" ))
3564 build .itemsTab :CreateDisplayItemFromRaw ([[
@@ -389,4 +418,166 @@ describe("TestAttacks", function()
389418 assert .is_true (math.abs (bifurcateChance - build .calcsTab .mainOutput .MainHand .CritBifurcates ) < 0.000001 )
390419 assert .are .equals (1 + critBonusMultiplier , build .calcsTab .mainOutput .MainHand .AverageHit )
391420 end )
421+
422+ -- Dual Wield tests
423+ local setupDualWieldTestConditions = function ()
424+ local slowHighDmgMace = [[
425+ Slow High Crit High Damage Mace
426+ Marauding Mace
427+ Quality: 0
428+ 200% increased physical damage
429+ 100% increased critical hit chance
430+ -25% increased attack speed
431+ ]]
432+
433+ local fastLowDmgMace = [[
434+ Fast Low Crit Low Damage Mace
435+ Marauding Mace
436+ Quality: 0
437+ -50% increased physical damage
438+ -100% increased critical hit chance
439+ 50% increased attack speed
440+ ]]
441+
442+ build .itemsTab :CreateDisplayItemFromRaw (slowHighDmgMace )
443+ build .itemsTab :AddDisplayItem ()
444+ runCallback (" OnFrame" )
445+ build .itemsTab .slots [" Weapon 1" ]:SetSelItemId (build .itemsTab .items [1 ].id )
446+
447+ build .itemsTab :CreateDisplayItemFromRaw (fastLowDmgMace )
448+ build .itemsTab :AddDisplayItem ()
449+ runCallback (" OnFrame" )
450+ build .itemsTab .slots [" Weapon 2" ]:SetSelItemId (build .itemsTab .items [2 ].id )
451+
452+ build .configTab .input .customMods = [[
453+ nearby enemies have 100% less armour
454+ your hits can't be evaded
455+ ]]
456+ build .configTab :BuildModList ()
457+ runCallback (" OnFrame" )
458+ end
459+
460+ local function harmonicMean (a , b )
461+ return 2 / (1 / a + 1 / b )
462+ end
463+
464+ it (" correctly calculates dual wield DPS for double hits" , function ()
465+ setupDualWieldTestConditions ()
466+ build .skillsTab :PasteSocketGroup (" skillId:MeleeMaceMacePlayer Mace Strike 20/0 1" )
467+ runCallback (" OnFrame" )
468+ build .calcsTab :BuildOutput ()
469+ runCallback (" OnFrame" )
470+
471+ -- Attack Speed
472+ local mainHandSpeed = build .calcsTab .mainOutput .MainHand .Speed
473+ local offHandSpeed = build .calcsTab .mainOutput .OffHand .Speed
474+ local combinedSpeed = harmonicMean (mainHandSpeed , offHandSpeed )
475+ assert .are .equals (round (combinedSpeed , 4 ), round (build .calcsTab .mainOutput .Speed , 4 ))
476+
477+ -- Average Hit
478+ local mainHandAvgDmg = build .calcsTab .mainOutput .MainHand .AverageDamage
479+ local offHandAvgDmg = build .calcsTab .mainOutput .OffHand .AverageDamage
480+ local combinedAvgDmg = build .calcsTab .mainOutput .AverageDamage
481+ assert .are .equals (round ((mainHandAvgDmg + offHandAvgDmg ) / 2 , 4 ), round (combinedAvgDmg , 4 ))
482+
483+ -- DPS (hits twice per attack)
484+ local combinedDPS = build .calcsTab .mainOutput .TotalDPS
485+ assert .are .equals (round (combinedAvgDmg * combinedSpeed * 2 ,4 ), round (combinedDPS ,4 ))
486+ end )
487+
488+ it (" correctly calculates dual wield crit chance for double hits" , function ()
489+ setupDualWieldTestConditions ()
490+ build .skillsTab :PasteSocketGroup (" skillId:MeleeMaceMacePlayer Mace Strike 20/0 1" )
491+ runCallback (" OnFrame" )
492+ build .calcsTab :BuildOutput ()
493+ runCallback (" OnFrame" )
494+
495+ -- Double hits roll crit individually per weapon, so should be average
496+ local mainHandCritChance = build .calcsTab .mainOutput .MainHand .CritChance
497+ local offHandCritChance = build .calcsTab .mainOutput .OffHand .CritChance
498+ local combinedCritChance = (mainHandCritChance + offHandCritChance ) / 2
499+ assert .are .equals (combinedCritChance , build .calcsTab .mainOutput .CritChance )
500+ end )
501+
502+ it (" correctly calculates dual wield DPS for alternating hits" , function ()
503+ setupDualWieldTestConditions ()
504+ build .skillsTab :PasteSocketGroup (" Armour Breaker 20/0 1" )
505+ runCallback (" OnFrame" )
506+ build .calcsTab :BuildOutput ()
507+ runCallback (" OnFrame" )
508+
509+ -- Attack Speed
510+ local mainHandSpeed = build .calcsTab .mainOutput .MainHand .Speed
511+ local offHandSpeed = build .calcsTab .mainOutput .OffHand .Speed
512+ local combinedSpeed = harmonicMean (mainHandSpeed , offHandSpeed )
513+ assert .are .equals (round (combinedSpeed , 4 ), round (build .calcsTab .mainOutput .Speed , 4 ))
514+
515+ -- Average Hit
516+ local mainHandAvgDmg = build .calcsTab .mainOutput .MainHand .AverageDamage
517+ local offHandAvgDmg = build .calcsTab .mainOutput .OffHand .AverageDamage
518+ local combinedAvgDmg = build .calcsTab .mainOutput .AverageDamage
519+ assert .are .equals (round ((mainHandAvgDmg + offHandAvgDmg ) / 2 , 4 ), round (combinedAvgDmg , 4 ))
520+
521+ -- DPS (hits once per attack)
522+ local combinedDPS = build .calcsTab .mainOutput .TotalDPS
523+ assert .are .equals (round (combinedAvgDmg * combinedSpeed , 4 ), round (combinedDPS , 4 ))
524+ end )
525+
526+ it (" correctly calculates dual wield crit chance for alternating hits" , function ()
527+ setupDualWieldTestConditions ()
528+ build .skillsTab :PasteSocketGroup (" Armour Breaker 20/0 1" )
529+ runCallback (" OnFrame" )
530+ build .calcsTab :BuildOutput ()
531+ runCallback (" OnFrame" )
532+
533+ -- Alternating hits roll crit individually per weapon, so should be average
534+ local mainHandCritChance = build .calcsTab .mainOutput .MainHand .CritChance
535+ local offHandCritChance = build .calcsTab .mainOutput .OffHand .CritChance
536+ local combinedCritChance = (mainHandCritChance + offHandCritChance ) / 2
537+ assert .are .equals (combinedCritChance , build .calcsTab .mainOutput .CritChance )
538+ end )
539+
540+ --[[
541+ NOTE: the following section contains tests for "combined hits", which PoE2 doesn't have as of 2026-06-02,
542+ which means the tests were written for a temporary test skill that will not be committed.
543+ The test can be updated by simply replacing `"skillId:MeleeMaceMacePlayerCombinedTEST Mace Strike TEST 20/0 1"`
544+ with actual skill data once available
545+ ]]
546+ --[[ it("correctly calculates dual wield DPS for combined hits", function()
547+ setupDualWieldTestConditions()
548+ build.skillsTab:PasteSocketGroup("skillId:MeleeMaceMacePlayerCombinedTEST Mace Strike TEST 20/0 1")
549+ runCallback("OnFrame")
550+ build.calcsTab:BuildOutput()
551+ runCallback("OnFrame")
552+
553+ -- Attack Speed
554+ local mainHandSpeed = build.calcsTab.mainOutput.MainHand.Speed
555+ local offHandSpeed = build.calcsTab.mainOutput.OffHand.Speed
556+ local combinedSpeed = harmonicMean(mainHandSpeed, offHandSpeed)
557+ assert.are.equals(round(combinedSpeed, 4), round(build.calcsTab.mainOutput.Speed, 4))
558+
559+ -- Average Hit
560+ local mainHandAvgDmg = build.calcsTab.mainOutput.MainHand.AverageDamage
561+ local offHandAvgDmg = build.calcsTab.mainOutput.OffHand.AverageDamage
562+ local combinedAvgDmg = build.calcsTab.mainOutput.AverageDamage
563+ assert.are.equals(round((mainHandAvgDmg + offHandAvgDmg), 4), round(combinedAvgDmg, 4))
564+
565+ -- DPS (hits twice per attack)
566+ local combinedDPS = build.calcsTab.mainOutput.TotalDPS
567+ assert.are.equals(round(combinedAvgDmg * combinedSpeed, 4), round(combinedDPS,4))
568+ end)
569+
570+ it("correctly calculates dual wield crit chance for combined hits", function()
571+ setupDualWieldTestConditions()
572+ build.skillsTab:PasteSocketGroup("skillId:MeleeMaceMacePlayerCombinedTEST Mace Strike TEST 20/0 1")
573+ runCallback("OnFrame")
574+ build.calcsTab:BuildOutput()
575+ runCallback("OnFrame")
576+
577+ -- combined hits count whole attack as crit, as long as one hand rolls crit)
578+ local mainHandCritChance = build.calcsTab.mainOutput.MainHand.CritChance
579+ local offHandCritChance = build.calcsTab.mainOutput.OffHand.CritChance
580+ local combinedCritChance = mainHandCritChance + offHandCritChance - (mainHandCritChance * offHandCritChance / 100)
581+ assert.are.equals(combinedCritChance, build.calcsTab.mainOutput.CritChance)
582+ end) ]]
392583end )
0 commit comments