Skip to content
Open
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
86 changes: 86 additions & 0 deletions spec/System/TestSkills_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,90 @@ describe("TestAttacks", function()

assert.True(preAdrenalineMaxStages < build.calcsTab.mainEnv.player.activeSkillList[1].skillModList:Sum("BASE", nil, "Multiplier:BlightMaxStages"))
end)
local function setupBifurcate(socketGroup, bifurcate, lucky, turboLucky, noCritMulti)
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")

local customCrit = 50 - 6 -- spark 6% base crit
build.configTab.input.customMods = string.format("+%d%% to critical hit chance\n", customCrit)
.. (bifurcate and "spell critical strike chance bifurcates\n" or "")
.. (lucky and "your critical strike chance is lucky\n" or "")
.. (turboLucky and "your lucky or unlucky effects use the best or worst from three rolls instead of two\n" or "")
.. (noCritMulti and "" or "your critical strike multiplier is 1000000%\n")
build.configTab:BuildModList()
runCallback("OnFrame")
build.calcsTab:BuildOutput()
runCallback("OnFrame")

return build.calcsTab.mainOutput
end
local function closeEnough(a, b, epsilon)
if not epsilon then epsilon = 10 ^ -9 end
return math.abs(a - b) < epsilon
end
it("correctly 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 garukhanOutput = setupBifurcate("Spark 1/0 1", true)
assert.are.equals(50, garukhanOutput.PreBifurcateCritChance)
assert.are.equals(75, garukhanOutput.CritChance)
assert.is_true(closeEnough(1 + 1 / 3, garukhanOutput.CritBifurcates))
assert.are.equals(20000, garukhanOutput.AverageHit)
end)
it("correctly calculates bifurcated for every nth crits spells", function()
local normalOutput = setupBifurcate("Spark 1/0 1", nil, nil, nil, true)
assert.are.equals(1.5, normalOutput.CritMultiplier)

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

local garukhanOutput = setupBifurcate("Spark 1/0 1\nAssassin's Mark 1/0 1", true, nil, nil, true)
assert.are.equals(2.07, floor(garukhanOutput.CritMultiplier, 2))
end)
it("multiplies assassin's mark ", function()
local normalOutput = setupBifurcate("Lightning Tendrils 1/0 1")
assert.are.equals(200 / 3, normalOutput.CritChance)
assert.are.equals(10000, normalOutput.CritMultiplier)

local garukhanOutput = setupBifurcate("Lightning Tendrils 1/0 1", true)
assert.are.equals(50, garukhanOutput.PreBifurcateCritChance)
assert.is_true(closeEnough(100 / 3 + (200 / 3) * 0.75, garukhanOutput.CritChance, 10 ^ -6))
assert.are.equals(1.2, garukhanOutput.CritBifurcates)
local garukhanOutput = setupBifurcate("Lightning Tendrils of Eccentricity 1/0 1", true)
assert.are.equals(50, garukhanOutput.PreBifurcateCritChance)
assert.are.equals(80, garukhanOutput.CritChance)
assert.are.equals(1.25, garukhanOutput.CritBifurcates)
end)
it("correctly calculates lucky bifurcated critical hit damage", function()
local normalOutput = setupBifurcate("Spark 1/0 1", false, true)
assert.are.equals(75, normalOutput.CritChance)
assert.are.equals(10000, normalOutput.CritMultiplier)
assert.are.equals(15000.5, normalOutput.AverageHit)

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

local garukhanOutput = setupBifurcate("Spark 1/0 1", true, true, true)
assert.are.equals(87.5, garukhanOutput.PreBifurcateCritChance)
assert.are.equals(98.4375, garukhanOutput.CritChance)
assert.are.equals(1 + 0.875 * 0.875 / (0.984375), garukhanOutput.CritBifurcates)
assert.is_true(math.abs(34998.5 - garukhanOutput.AverageHit) < 0.01)
end)
end)
45 changes: 44 additions & 1 deletion 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,6 +2964,11 @@ 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 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 env.mode_effective and (critRolls ~= 0 or skillModList:Flag(skillCfg, "Every3UseCrit") or skillModList:Flag(skillCfg, "Every5UseCrit")) then
if critRolls ~= 0 then
if skillModList:Flag(skillCfg, "Unexciting") then
Expand Down Expand Up @@ -3006,6 +3017,38 @@ 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 conditionalBifurcateChance = effectiveCritChance > 0 and bifurcateMultiChance / effectiveCritChance or 0
-- scale damage bonus chance to account for guaranteed crit
-- not being able to benefit from it
if skillModList:Flag(skillCfg, "Every3UseCrit") then
conditionalBifurcateChance = conditionalBifurcateChance * 2 / 3
end
if skillModList:Flag(skillCfg, "Every5UseCrit") then
conditionalBifurcateChance = conditionalBifurcateChance * 4 / 5
end
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),
s_format("= %.2f%% ^8(chance both crit rolls succeed)", bifurcateMultiChance),
s_format("/ %.2f%% ^8(chance at least one crit roll succeeds)", effectiveCritChance),
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