Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions spec/System/TestBifurcatedCrit_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
describe("Bifurcated critical strikes", function()
local function setupBifurcate(socketGroup, bifurcate, lucky, extremeLuck, useDefaultCritMultiplier)
newBuild()
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Imbued Wand
Quality: 0
100% reduced lightning damage
adds 1 to 1 physical damage to spells
nearby enemies have 100% less armour
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")
build.skillsTab:PasteSocketGroup(socketGroup)
runCallback("OnFrame")

build.configTab.input.customMods = "+44% to critical hit chance\n"
.. (bifurcate and "spell critical strike chance bifurcates\n" or "")
.. (lucky and "your critical strike chance is lucky\n" or "")
.. (extremeLuck and "your lucky or unlucky effects use the best or worst from three rolls instead of two\n" or "")
.. (useDefaultCritMultiplier and "" or "your critical strike multiplier is 1000000%\n")
build.configTab:BuildModList()
runCallback("OnFrame")

return build.calcsTab.mainOutput
end

it("calculates bifurcated critical hit damage", function()
local normalOutput = setupBifurcate("Spark 1/0 1")
assert.are.equals(50, normalOutput.CritChance)
assert.are.equals(10000, normalOutput.CritMultiplier)
assert.are.equals(10001, normalOutput.AverageHit)

local bifurcateOutput = setupBifurcate("Spark 1/0 1", true)
assert.are.equals(50, bifurcateOutput.PreBifurcateCritChance)
assert.are.equals(75, bifurcateOutput.CritChance)
assert.are.near(1 + 1 / 3, bifurcateOutput.CritBifurcates, 10 ^ -9)
assert.are.equals(20000, bifurcateOutput.AverageHit)

local mysticalInfusion = build.spec.nodes[1945]
mysticalInfusion.alloc = true
build.spec.allocNodes[mysticalInfusion.id] = mysticalInfusion
build.configTab.input.customMods = "+44% to critical hit chance\nyour critical strike multiplier is 1000000%\n"
build.configTab:BuildModList()
build.buildFlag = true
runCallback("OnFrame")
assert.are.equals(35, build.calcsTab.mainOutput.PreBifurcateCritChance)
assert.are.near(57.75, build.calcsTab.mainOutput.CritChance, 10 ^ -9)
end)

it("accounts for guaranteed critical strikes", function()
local normalOutput = setupBifurcate("Spark 1/0 1", nil, nil, nil, true)
assert.are.equals(1.5, normalOutput.CritMultiplier)

local markedOutput = setupBifurcate("Spark 1/0 1\nAssassin's Mark 1/0 1", nil, nil, nil, true)
assert.are.equals(1.8, markedOutput.CritMultiplier)

local bifurcateOutput = setupBifurcate("Spark 1/0 1\nAssassin's Mark 1/0 1", true, nil, nil, true)
assert.are.equals(2.07, floor(bifurcateOutput.CritMultiplier, 2))

local tendrilsOutput = setupBifurcate("Lightning Tendrils 1/0 1", true)
assert.are.equals(50, tendrilsOutput.PreBifurcateCritChance)
assert.are.near(100 / 3 + (200 / 3) * 0.75, tendrilsOutput.CritChance, 10 ^ -6)
assert.are.equals(1.2, tendrilsOutput.CritBifurcates)

local eccentricityOutput = setupBifurcate("Lightning Tendrils of Eccentricity 1/0 1", true)
assert.are.equals(50, eccentricityOutput.PreBifurcateCritChance)
assert.are.equals(80, eccentricityOutput.CritChance)
assert.are.equals(1.25, eccentricityOutput.CritBifurcates)
end)

it("applies lucky rolls independently", function()
local luckyOutput = setupBifurcate("Spark 1/0 1", false, true)
assert.are.equals(75, luckyOutput.CritChance)
assert.are.equals(10000, luckyOutput.CritMultiplier)
assert.are.equals(15000.5, luckyOutput.AverageHit)

local bifurcateOutput = setupBifurcate("Spark 1/0 1", true, true)
assert.are.equals(75, bifurcateOutput.PreBifurcateCritChance)
assert.are.equals(93.75, bifurcateOutput.CritChance)
assert.are.equals(1.6, bifurcateOutput.CritBifurcates)
assert.are.equals(29999, bifurcateOutput.AverageHit)

local extremeLuckOutput = setupBifurcate("Spark 1/0 1", true, true, true)
assert.are.equals(87.5, extremeLuckOutput.PreBifurcateCritChance)
assert.are.equals(98.4375, extremeLuckOutput.CritChance)
assert.are.equals(1 + 0.875 ^ 2 / 0.984375, extremeLuckOutput.CritBifurcates)
assert.are.near(34998.5, extremeLuckOutput.AverageHit, 0.01)
end)
end)
56 changes: 53 additions & 3 deletions src/Modules/CalcOffence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2893,6 +2893,7 @@ function calcs.offence(env, actor, activeSkill)

if critOverride == 100 then
output.PreEffectiveCritChance = 100
output.PreBifurcateCritChance = 100
output.CritChance = 100
else
local base = 0
Expand Down Expand Up @@ -2927,8 +2928,13 @@ function calcs.offence(env, actor, activeSkill)
output.CritChance = (1 - (1 - output.CritChance / 100) ^ (critRolls + 1)) * 100
end
end
output.PreBifurcateCritChance = output.CritChance
local preBifurcateCritChance = output.CritChance
if env.mode_effective and skillModList:Flag(cfg, "BifurcateCrit") then
output.CritChance = (1 - (1 - output.CritChance / 100) ^ 2) * 100
end
local preHitCheckCritChance = output.CritChance
local preSkillUseCritChance= output.CritChance
local preSkillUseCritChance = output.CritChance
if env.mode_effective then
if skillModList:Flag(skillCfg, "Every3UseCrit") then
output.CritChance = (2 * output.CritChance + 100) / 3
Expand Down Expand Up @@ -2958,7 +2964,7 @@ function calcs.offence(env, actor, activeSkill)
local overCap = preCapCritChance - 100
t_insert(breakdown.CritChance, s_format("Crit is overcapped by %.2f%% (%d%% increased Critical Strike Chance)", overCap, overCap / more / (baseCrit + base) * 100))
end
if env.mode_effective and (critRolls ~= 0 or skillModList:Flag(skillCfg, "Every3UseCrit") or skillModList:Flag(skillCfg, "Every5UseCrit")) then
if env.mode_effective then
if critRolls ~= 0 then
if skillModList:Flag(skillCfg, "Unexciting") then
t_insert(breakdown.CritChance, "Crit Chance is Unexciting:")
Expand All @@ -2967,14 +2973,22 @@ function calcs.offence(env, actor, activeSkill)
t_insert(breakdown.CritChance, "Crit Chance is Lucky:")
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f)^ %d", preLuckyCritChance / 100, critRolls + 1))
end
t_insert(breakdown.CritChance, s_format("= %.2f%%", preBifurcateCritChance))
end
if skillModList:Flag(cfg, "BifurcateCrit") then
t_insert(breakdown.CritChance, "Critical Strike Bifurcates:")
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f) x (1 - %.4f)", preBifurcateCritChance / 100, preBifurcateCritChance / 100))
t_insert(breakdown.CritChance, s_format("= %.2f%%", preSkillUseCritChance))
end
if skillModList:Flag(skillCfg, "Every3UseCrit") then
t_insert(breakdown.CritChance, s_format("+ %.2f%% ^8(crit every 3rd use)", (2 * preSkillUseCritChance + 100) / 3 - preSkillUseCritChance))
end
if skillModList:Flag(skillCfg, "Every5UseCrit") then
t_insert(breakdown.CritChance, s_format("+ %.2f%% ^8(crit every 5th use)", (4 * preSkillUseCritChance + 100) / 5 - preSkillUseCritChance))
end
t_insert(breakdown.CritChance, s_format("= %.2f%%", preHitCheckCritChance))
if skillModList:Flag(skillCfg, "Every3UseCrit") or skillModList:Flag(skillCfg, "Every5UseCrit") then
t_insert(breakdown.CritChance, s_format("= %.2f%%", preHitCheckCritChance))
end
end
if env.mode_effective and output.AccuracyHitChance < 100 then
t_insert(breakdown.CritChance, "Crit confirmation roll:")
Expand Down Expand Up @@ -3006,6 +3020,42 @@ function calcs.offence(env, actor, activeSkill)
}
end
end
-- if crit bifurcates are enabled, roll for crit twice and add multiplier for each
local critOverride = skillModList:Override(cfg, "CritChance")
if env.mode_effective and skillModList:Flag(cfg, "BifurcateCrit") and output.PreBifurcateCritChance and not (critOverride == 100) then
-- get crit chance and calculate odds of critting twice
local critChancePercentage = output.PreBifurcateCritChance
local bifurcateMultiChance = (critChancePercentage ^ 2) / 100
local effectiveCritChance = output.CritChance
local bifurcateUseChance = 1
-- Guaranteed crit uses do not roll crit chance and therefore cannot bifurcate
if skillModList:Flag(skillCfg, "Every3UseCrit") then
bifurcateUseChance = bifurcateUseChance * 2 / 3
end
if skillModList:Flag(skillCfg, "Every5UseCrit") then
bifurcateUseChance = bifurcateUseChance * 4 / 5
end
bifurcateMultiChance = bifurcateMultiChance * bifurcateUseChance
local conditionalBifurcateChance = effectiveCritChance > 0 and bifurcateMultiChance / effectiveCritChance or 0
output.CritBifurcates = 1 + conditionalBifurcateChance
local damageBonus = extraDamage
local bifurcatedBonus = conditionalBifurcateChance * extraDamage
if breakdown then
breakdown.CritBifurcates = {
s_format("%.2f%% ^8(pre-bifurcate crit chance)", critChancePercentage),
s_format("x %.2f%%", critChancePercentage),
}
if bifurcateUseChance < 1 then
t_insert(breakdown.CritBifurcates, s_format("x %.2f%% ^8(uses that can bifurcate)", bifurcateUseChance * 100))
end
t_insert(breakdown.CritBifurcates, s_format("= %.2f%% ^8(chance both crit rolls succeed)", bifurcateMultiChance))
t_insert(breakdown.CritBifurcates, s_format("/ %.2f%% ^8(effective crit chance)", effectiveCritChance))
t_insert(breakdown.CritBifurcates, s_format("= %.2f ^8(crit Bifurcates effect)", 1 + conditionalBifurcateChance))
end
extraDamage = damageBonus + bifurcatedBonus
-- mod doesn't affect output and is purely descriptive
skillModList:NewMod("CritMultiplier", "MORE", floor(conditionalBifurcateChance * 100, 2), "Bifurcated Crit Damage Bonus", ModFlag.Hit)
end
output.CritMultiplier = 1 + m_max(0, extraDamage)
end
local critChancePercentage = output.CritChance / 100
Expand Down
4 changes: 4 additions & 0 deletions src/Modules/CalcSections.lua
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,10 @@ return {
{ label = "Player modifiers", modName = "CritMultiplier", cfg = "skill" },
{ label = "Enemy modifiers", modName = "SelfCritMultiplier", enemy = true },
}, },
{ label = "Crit Bifurcates", notFlag = "attack", haveOutput = "CritBifurcates", { format = "x {2:output:CritBifurcates}",
{ breakdown = "CritBifurcates" },
{ label = "Player modifiers", modName = "BifurcateCrit", cfg = "skill" },
}, },
{ label = "Crit Effect Mod", notFlag = "attack", { format = "x {3:output:CritEffect}", { breakdown = "CritEffect" }, }, },
-- Main Hand
{ label = "MH Inc. Crit Chance", bgCol = colorCodes.MAINHANDBG, flag = "weapon1Attack", { format = "{0:mod:1,2}%",
Expand Down
1 change: 1 addition & 0 deletions src/Modules/ModParser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3386,6 +3386,7 @@ local specialModList = {
["minion critical strikes do not deal extra damage"] = { mod("MinionModifier", "LIST", { mod = flag("NoCritMultiplier") }) },
["lightning damage with non%-critical strikes is lucky"] = { flag("LightningNoCritLucky") },
["your damage with critical strikes is lucky"] = { flag("CritLucky") },
["spell critical strike chance bifurcates"] = { flag("BifurcateCrit", nil, ModFlag.Spell) },
["critical strikes deal no damage"] = { mod("Damage", "MORE", -100, { type = "Condition", var = "CriticalStrike" }) },
["critical strike chance is increased by uncapped lightning resistance"] = { flag("CritChanceIncreasedByUncappedLightningRes") },
["critical strike chance is increased by lightning resistance"] = { flag("CritChanceIncreasedByLightningRes") },
Expand Down
Loading