Skip to content

Commit 03ae462

Browse files
vaisestLocalIdentity
andauthored
Port support for spell bifurcated crits (#9940)
* Port support for spell bifurcated crits * Fix breakdown not showing and tidy up code * Fix breakdown ordering * Fix breakdown + move tests --------- Co-authored-by: LocalIdentity <localidentity2@gmail.com>
1 parent e42520b commit 03ae462

4 files changed

Lines changed: 148 additions & 3 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
describe("Bifurcated critical strikes", function()
2+
local function setupBifurcate(socketGroup, bifurcate, lucky, extremeLuck, useDefaultCritMultiplier)
3+
newBuild()
4+
build.itemsTab:CreateDisplayItemFromRaw([[
5+
New Item
6+
Imbued Wand
7+
Quality: 0
8+
100% reduced lightning damage
9+
adds 1 to 1 physical damage to spells
10+
nearby enemies have 100% less armour
11+
]])
12+
build.itemsTab:AddDisplayItem()
13+
runCallback("OnFrame")
14+
build.skillsTab:PasteSocketGroup(socketGroup)
15+
runCallback("OnFrame")
16+
17+
build.configTab.input.customMods = "+44% to critical hit chance\n"
18+
.. (bifurcate and "spell critical strike chance bifurcates\n" or "")
19+
.. (lucky and "your critical strike chance is lucky\n" or "")
20+
.. (extremeLuck and "your lucky or unlucky effects use the best or worst from three rolls instead of two\n" or "")
21+
.. (useDefaultCritMultiplier and "" or "your critical strike multiplier is 1000000%\n")
22+
build.configTab:BuildModList()
23+
runCallback("OnFrame")
24+
25+
return build.calcsTab.mainOutput
26+
end
27+
28+
it("calculates bifurcated critical hit damage", function()
29+
local normalOutput = setupBifurcate("Spark 1/0 1")
30+
assert.are.equals(50, normalOutput.CritChance)
31+
assert.are.equals(10000, normalOutput.CritMultiplier)
32+
assert.are.equals(10001, normalOutput.AverageHit)
33+
34+
local bifurcateOutput = setupBifurcate("Spark 1/0 1", true)
35+
assert.are.equals(50, bifurcateOutput.PreBifurcateCritChance)
36+
assert.are.equals(75, bifurcateOutput.CritChance)
37+
assert.are.near(1 + 1 / 3, bifurcateOutput.CritBifurcates, 10 ^ -9)
38+
assert.are.equals(20000, bifurcateOutput.AverageHit)
39+
40+
local mysticalInfusion = build.spec.nodes[1945]
41+
mysticalInfusion.alloc = true
42+
build.spec.allocNodes[mysticalInfusion.id] = mysticalInfusion
43+
build.configTab.input.customMods = "+44% to critical hit chance\nyour critical strike multiplier is 1000000%\n"
44+
build.configTab:BuildModList()
45+
build.buildFlag = true
46+
runCallback("OnFrame")
47+
assert.are.equals(35, build.calcsTab.mainOutput.PreBifurcateCritChance)
48+
assert.are.near(57.75, build.calcsTab.mainOutput.CritChance, 10 ^ -9)
49+
end)
50+
51+
it("accounts for guaranteed critical strikes", function()
52+
local normalOutput = setupBifurcate("Spark 1/0 1", nil, nil, nil, true)
53+
assert.are.equals(1.5, normalOutput.CritMultiplier)
54+
55+
local markedOutput = setupBifurcate("Spark 1/0 1\nAssassin's Mark 1/0 1", nil, nil, nil, true)
56+
assert.are.equals(1.8, markedOutput.CritMultiplier)
57+
58+
local bifurcateOutput = setupBifurcate("Spark 1/0 1\nAssassin's Mark 1/0 1", true, nil, nil, true)
59+
assert.are.equals(2.07, floor(bifurcateOutput.CritMultiplier, 2))
60+
61+
local tendrilsOutput = setupBifurcate("Lightning Tendrils 1/0 1", true)
62+
assert.are.equals(50, tendrilsOutput.PreBifurcateCritChance)
63+
assert.are.near(100 / 3 + (200 / 3) * 0.75, tendrilsOutput.CritChance, 10 ^ -6)
64+
assert.are.equals(1.2, tendrilsOutput.CritBifurcates)
65+
66+
local eccentricityOutput = setupBifurcate("Lightning Tendrils of Eccentricity 1/0 1", true)
67+
assert.are.equals(50, eccentricityOutput.PreBifurcateCritChance)
68+
assert.are.equals(80, eccentricityOutput.CritChance)
69+
assert.are.equals(1.25, eccentricityOutput.CritBifurcates)
70+
end)
71+
72+
it("applies lucky rolls independently", function()
73+
local luckyOutput = setupBifurcate("Spark 1/0 1", false, true)
74+
assert.are.equals(75, luckyOutput.CritChance)
75+
assert.are.equals(10000, luckyOutput.CritMultiplier)
76+
assert.are.equals(15000.5, luckyOutput.AverageHit)
77+
78+
local bifurcateOutput = setupBifurcate("Spark 1/0 1", true, true)
79+
assert.are.equals(75, bifurcateOutput.PreBifurcateCritChance)
80+
assert.are.equals(93.75, bifurcateOutput.CritChance)
81+
assert.are.equals(1.6, bifurcateOutput.CritBifurcates)
82+
assert.are.equals(29999, bifurcateOutput.AverageHit)
83+
84+
local extremeLuckOutput = setupBifurcate("Spark 1/0 1", true, true, true)
85+
assert.are.equals(87.5, extremeLuckOutput.PreBifurcateCritChance)
86+
assert.are.equals(98.4375, extremeLuckOutput.CritChance)
87+
assert.are.equals(1 + 0.875 ^ 2 / 0.984375, extremeLuckOutput.CritBifurcates)
88+
assert.are.near(34998.5, extremeLuckOutput.AverageHit, 0.01)
89+
end)
90+
end)

src/Modules/CalcOffence.lua

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,6 +2901,7 @@ function calcs.offence(env, actor, activeSkill)
29012901

29022902
if critOverride == 100 then
29032903
output.PreEffectiveCritChance = 100
2904+
output.PreBifurcateCritChance = 100
29042905
output.CritChance = 100
29052906
else
29062907
local base = 0
@@ -2935,8 +2936,13 @@ function calcs.offence(env, actor, activeSkill)
29352936
output.CritChance = (1 - (1 - output.CritChance / 100) ^ (critRolls + 1)) * 100
29362937
end
29372938
end
2939+
output.PreBifurcateCritChance = output.CritChance
2940+
local preBifurcateCritChance = output.CritChance
2941+
if env.mode_effective and skillModList:Flag(cfg, "BifurcateCrit") then
2942+
output.CritChance = (1 - (1 - output.CritChance / 100) ^ 2) * 100
2943+
end
29382944
local preHitCheckCritChance = output.CritChance
2939-
local preSkillUseCritChance= output.CritChance
2945+
local preSkillUseCritChance = output.CritChance
29402946
if env.mode_effective then
29412947
if skillModList:Flag(skillCfg, "Every3UseCrit") then
29422948
output.CritChance = (2 * output.CritChance + 100) / 3
@@ -2966,7 +2972,7 @@ function calcs.offence(env, actor, activeSkill)
29662972
local overCap = preCapCritChance - 100
29672973
t_insert(breakdown.CritChance, s_format("Crit is overcapped by %.2f%% (%d%% increased Critical Strike Chance)", overCap, overCap / more / (baseCrit + base) * 100))
29682974
end
2969-
if env.mode_effective and (critRolls ~= 0 or skillModList:Flag(skillCfg, "Every3UseCrit") or skillModList:Flag(skillCfg, "Every5UseCrit")) then
2975+
if env.mode_effective then
29702976
if critRolls ~= 0 then
29712977
if skillModList:Flag(skillCfg, "Unexciting") then
29722978
t_insert(breakdown.CritChance, "Crit Chance is Unexciting:")
@@ -2975,14 +2981,22 @@ function calcs.offence(env, actor, activeSkill)
29752981
t_insert(breakdown.CritChance, "Crit Chance is Lucky:")
29762982
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f)^ %d", preLuckyCritChance / 100, critRolls + 1))
29772983
end
2984+
t_insert(breakdown.CritChance, s_format("= %.2f%%", preBifurcateCritChance))
2985+
end
2986+
if skillModList:Flag(cfg, "BifurcateCrit") then
2987+
t_insert(breakdown.CritChance, "Critical Strike Bifurcates:")
2988+
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f) x (1 - %.4f)", preBifurcateCritChance / 100, preBifurcateCritChance / 100))
2989+
t_insert(breakdown.CritChance, s_format("= %.2f%%", preSkillUseCritChance))
29782990
end
29792991
if skillModList:Flag(skillCfg, "Every3UseCrit") then
29802992
t_insert(breakdown.CritChance, s_format("+ %.2f%% ^8(crit every 3rd use)", (2 * preSkillUseCritChance + 100) / 3 - preSkillUseCritChance))
29812993
end
29822994
if skillModList:Flag(skillCfg, "Every5UseCrit") then
29832995
t_insert(breakdown.CritChance, s_format("+ %.2f%% ^8(crit every 5th use)", (4 * preSkillUseCritChance + 100) / 5 - preSkillUseCritChance))
29842996
end
2985-
t_insert(breakdown.CritChance, s_format("= %.2f%%", preHitCheckCritChance))
2997+
if skillModList:Flag(skillCfg, "Every3UseCrit") or skillModList:Flag(skillCfg, "Every5UseCrit") then
2998+
t_insert(breakdown.CritChance, s_format("= %.2f%%", preHitCheckCritChance))
2999+
end
29863000
end
29873001
if env.mode_effective and output.AccuracyHitChance < 100 then
29883002
t_insert(breakdown.CritChance, "Crit confirmation roll:")
@@ -3014,6 +3028,42 @@ function calcs.offence(env, actor, activeSkill)
30143028
}
30153029
end
30163030
end
3031+
-- if crit bifurcates are enabled, roll for crit twice and add multiplier for each
3032+
local critOverride = skillModList:Override(cfg, "CritChance")
3033+
if env.mode_effective and skillModList:Flag(cfg, "BifurcateCrit") and output.PreBifurcateCritChance and not (critOverride == 100) then
3034+
-- get crit chance and calculate odds of critting twice
3035+
local critChancePercentage = output.PreBifurcateCritChance
3036+
local bifurcateMultiChance = (critChancePercentage ^ 2) / 100
3037+
local effectiveCritChance = output.CritChance
3038+
local bifurcateUseChance = 1
3039+
-- Guaranteed crit uses do not roll crit chance and therefore cannot bifurcate
3040+
if skillModList:Flag(skillCfg, "Every3UseCrit") then
3041+
bifurcateUseChance = bifurcateUseChance * 2 / 3
3042+
end
3043+
if skillModList:Flag(skillCfg, "Every5UseCrit") then
3044+
bifurcateUseChance = bifurcateUseChance * 4 / 5
3045+
end
3046+
bifurcateMultiChance = bifurcateMultiChance * bifurcateUseChance
3047+
local conditionalBifurcateChance = effectiveCritChance > 0 and bifurcateMultiChance / effectiveCritChance or 0
3048+
output.CritBifurcates = 1 + conditionalBifurcateChance
3049+
local damageBonus = extraDamage
3050+
local bifurcatedBonus = conditionalBifurcateChance * extraDamage
3051+
if breakdown then
3052+
breakdown.CritBifurcates = {
3053+
s_format("%.2f%% ^8(pre-bifurcate crit chance)", critChancePercentage),
3054+
s_format("x %.2f%%", critChancePercentage),
3055+
}
3056+
if bifurcateUseChance < 1 then
3057+
t_insert(breakdown.CritBifurcates, s_format("x %.2f%% ^8(uses that can bifurcate)", bifurcateUseChance * 100))
3058+
end
3059+
t_insert(breakdown.CritBifurcates, s_format("= %.2f%% ^8(chance both crit rolls succeed)", bifurcateMultiChance))
3060+
t_insert(breakdown.CritBifurcates, s_format("/ %.2f%% ^8(effective crit chance)", effectiveCritChance))
3061+
t_insert(breakdown.CritBifurcates, s_format("= %.2f ^8(crit Bifurcates effect)", 1 + conditionalBifurcateChance))
3062+
end
3063+
extraDamage = damageBonus + bifurcatedBonus
3064+
-- mod doesn't affect output and is purely descriptive
3065+
skillModList:NewMod("CritMultiplier", "MORE", floor(conditionalBifurcateChance * 100, 2), "Bifurcated Crit Damage Bonus", ModFlag.Hit)
3066+
end
30173067
output.CritMultiplier = 1 + m_max(0, extraDamage)
30183068
end
30193069
local critChancePercentage = output.CritChance / 100

src/Modules/CalcSections.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,10 @@ return {
527527
{ label = "Player modifiers", modName = "CritMultiplier", cfg = "skill" },
528528
{ label = "Enemy modifiers", modName = "SelfCritMultiplier", enemy = true },
529529
}, },
530+
{ label = "Crit Bifurcates", notFlag = "attack", haveOutput = "CritBifurcates", { format = "x {2:output:CritBifurcates}",
531+
{ breakdown = "CritBifurcates" },
532+
{ label = "Player modifiers", modName = "BifurcateCrit", cfg = "skill" },
533+
}, },
530534
{ label = "Crit Effect Mod", notFlag = "attack", { format = "x {3:output:CritEffect}", { breakdown = "CritEffect" }, }, },
531535
-- Main Hand
532536
{ label = "MH Inc. Crit Chance", bgCol = colorCodes.MAINHANDBG, flag = "weapon1Attack", { format = "{0:mod:1,2}%",

src/Modules/ModParser.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3408,6 +3408,7 @@ local specialModList = {
34083408
["minion critical strikes do not deal extra damage"] = { mod("MinionModifier", "LIST", { mod = flag("NoCritMultiplier") }) },
34093409
["lightning damage with non%-critical strikes is lucky"] = { flag("LightningNoCritLucky") },
34103410
["your damage with critical strikes is lucky"] = { flag("CritLucky") },
3411+
["spell critical strike chance bifurcates"] = { flag("BifurcateCrit", nil, ModFlag.Spell) },
34113412
["critical strikes deal no damage"] = { mod("Damage", "MORE", -100, { type = "Condition", var = "CriticalStrike" }) },
34123413
["critical strike chance is increased by uncapped lightning resistance"] = { flag("CritChanceIncreasedByUncappedLightningRes") },
34133414
["critical strike chance is increased by lightning resistance"] = { flag("CritChanceIncreasedByLightningRes") },

0 commit comments

Comments
 (0)