Skip to content

Commit 86b3acf

Browse files
author
LocalIdentity
committed
Add test + fix interaction with bifurcated crits
1 parent becd859 commit 86b3acf

2 files changed

Lines changed: 86 additions & 14 deletions

File tree

spec/System/TestAttacks_spec.lua

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe("TestAttacks", function()
114114
runCallback("OnFrame")
115115
build.calcsTab:BuildOutput()
116116
runCallback("OnFrame")
117-
117+
118118
-- 1: Get base damage with no crits
119119
local critChance = 0
120120
local critMult = 2
@@ -180,7 +180,7 @@ describe("TestAttacks", function()
180180
runCallback("OnFrame")
181181
build.calcsTab:BuildOutput()
182182
runCallback("OnFrame")
183-
183+
184184
-- 1: Get base damage with no crits
185185
local critChance = 0.0
186186
local critMult = 2
@@ -202,10 +202,10 @@ describe("TestAttacks", function()
202202
critChance = 0.1
203203
local nonCritChance = 1 - critChance
204204

205-
local critBonusMultiplier =
206-
1 * critChance
207-
+ .7 * nonCritChance * critChance
208-
+ .4 * nonCritChance * nonCritChance * critChance
205+
local critBonusMultiplier =
206+
1 * critChance
207+
+ .7 * nonCritChance * critChance
208+
+ .4 * nonCritChance * nonCritChance * critChance
209209
+ .1 * nonCritChance * nonCritChance * nonCritChance * critChance
210210

211211
-- When adding them as MORE mods, they get auto rounded after *100, so we need to do the same
@@ -218,4 +218,68 @@ describe("TestAttacks", function()
218218
local forcedExpectedAvgHit = averageHit * critMult
219219
assert.are.equals(forcedExpectedAvgHit, build.calcsTab.mainOutput.MainHand.AverageHit)
220220
end)
221-
end)
221+
222+
it("does not force critical hits when critical hit chance is zero", function()
223+
build.itemsTab:CreateDisplayItemFromRaw([[
224+
New Item
225+
Heavy Bow
226+
Quality: 0
227+
-100% increased critical hit chance
228+
-100% increased physical damage
229+
adds 1 to 1 physical damage to attacks
230+
nearby enemies have 100% less armour
231+
nearby enemies have 100% less evasion
232+
]])
233+
build.itemsTab:AddDisplayItem()
234+
runCallback("OnFrame")
235+
236+
build.configTab.input.customMods = "inevitable critical hits"
237+
build.configTab:BuildModList()
238+
runCallback("OnFrame")
239+
build.calcsTab:BuildOutput()
240+
runCallback("OnFrame")
241+
242+
assert.are.equals(0, build.calcsTab.mainOutput.MainHand.CritChance)
243+
assert.are.equals(1, build.calcsTab.mainOutput.MainHand.AverageHit)
244+
end)
245+
246+
it("correctly calculates forced outcome with bifurcated critical hits", function()
247+
build.itemsTab:CreateDisplayItemFromRaw([[
248+
New Item
249+
Heavy Bow
250+
Quality: 0
251+
-100% increased critical hit chance
252+
-100% increased physical damage
253+
adds 1 to 1 physical damage to attacks
254+
nearby enemies have 100% less armour
255+
nearby enemies have 100% less evasion
256+
]])
257+
build.itemsTab:AddDisplayItem()
258+
runCallback("OnFrame")
259+
260+
build.configTab.input.customMods = [[
261+
+10% to critical hit chance
262+
inevitable critical hits
263+
bifurcates critical hits
264+
]]
265+
build.configTab:BuildModList()
266+
runCallback("OnFrame")
267+
build.calcsTab:BuildOutput()
268+
runCallback("OnFrame")
269+
270+
local critChance = 0.1
271+
local failedStageChance = (1 - critChance) ^ 2
272+
local critBonusMultiplier = 2 * critChance * (
273+
1
274+
+ .7 * failedStageChance
275+
+ .4 * failedStageChance ^ 2
276+
+ .1 * failedStageChance ^ 3
277+
)
278+
critBonusMultiplier = math.floor(critBonusMultiplier * 100 + 0.5) / 100
279+
280+
assert.are.equals(100, build.calcsTab.mainOutput.MainHand.CritChance)
281+
local bifurcateChance = (critChance * 100) ^ 2 / ((1 - failedStageChance) * 100)
282+
assert.is_true(math.abs(bifurcateChance - build.calcsTab.mainOutput.MainHand.CritBifurcates) < 0.000001)
283+
assert.are.equals(1 + critBonusMultiplier, build.calcsTab.mainOutput.MainHand.AverageHit)
284+
end)
285+
end)

src/Modules/CalcOffence.lua

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3340,6 +3340,7 @@ function calcs.offence(env, actor, activeSkill)
33403340
end
33413341

33423342
-- Calculate crit chance, crit multiplier, and their combined effect
3343+
local forcedOutcomeApplies = false
33433344
if skillModList:Flag(cfg, "NeverCrit") then
33443345
output.PreEffectiveCritChance = 0
33453346
output.CritChance = 0
@@ -3429,10 +3430,11 @@ function calcs.offence(env, actor, activeSkill)
34293430
if env.mode_effective and skillModList:Flag(cfg, "BifurcateCrit") then
34303431
output.CritChance = (1 - (1 - output.CritChance / 100) ^ 2) * 100
34313432
end
3432-
if env.mode_effective and skillModList:Flag(cfg, "ForcedOutcome") then
3433-
-- Lucky crits: not considered a 'reroll' for the sake of forced outcome penalties, just a higher crit chance
3434-
-- Bifurcated crits: only considered a 'reroll' if both of them fail to crit
3435-
-- So just use the crit chance as calculated, and it already works with both of them
3433+
local preForcedOutcomeCritChance = output.CritChance
3434+
if env.mode_effective and skillModList:Flag(cfg, "ForcedOutcome") and output.CritChance > 0 then
3435+
forcedOutcomeApplies = true
3436+
-- Lucky crits use their effective crit chance without an extra roll-down penalty.
3437+
-- Bifurcated crits roll twice per roll-down; only both rolls failing advances the penalty.
34363438
local critChance = output.CritChance / 100
34373439
local nonCritChance = 1 - critChance
34383440

@@ -3441,6 +3443,12 @@ function calcs.offence(env, actor, activeSkill)
34413443
0.7 * nonCritChance * critChance + -- 70% if we roll non-crit then a crit
34423444
0.4 * m_pow(nonCritChance, 2) * critChance + -- 40% if we roll two non-crit then a crit
34433445
0.1 * m_pow(nonCritChance, 3) * critChance -- 10% if we roll three non-crits then a crit
3446+
if skillModList:Flag(cfg, "BifurcateCrit") then
3447+
-- The terminating stage can crit twice, so scale its one-or-more-crit bonus
3448+
-- by the expected number of crits on that stage.
3449+
critBonusMultiplier = critBonusMultiplier * 2 * output.PreBifurcateCritChance / output.CritChance
3450+
output.CritBifurcates = output.PreBifurcateCritChance ^ 2 / output.CritChance
3451+
end
34443452

34453453
-- This gets rounded when used in damage logic, so round it ahead of time to make the breakdown accurate (and less ugly)
34463454
local lessCritBonus = round((1 - critBonusMultiplier) * -100.0, 0)
@@ -3484,9 +3492,9 @@ function calcs.offence(env, actor, activeSkill)
34843492
if env.mode_effective and skillModList:Flag(cfg, "BifurcateCrit") then
34853493
t_insert(breakdown.CritChance, "Critical Strike Bifurcates:")
34863494
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f) x (1 - %.4f)", preBifurcateCritChance / 100, preBifurcateCritChance / 100))
3487-
t_insert(breakdown.CritChance, s_format("= %.2f%%", output.CritChance))
3495+
t_insert(breakdown.CritChance, s_format("= %.2f%%", preForcedOutcomeCritChance))
34883496
end
3489-
if env.mode_effective and skillModList:Flag(cfg, "ForcedOutcome") then
3497+
if forcedOutcomeApplies then
34903498
t_insert(breakdown.CritChance, "Inevitable Critical Hits (Forced Outcome):")
34913499
t_insert(breakdown.CritChance, "= 100% ^8(override)")
34923500
end
@@ -3508,7 +3516,7 @@ function calcs.offence(env, actor, activeSkill)
35083516

35093517
output.PreEffectiveCritMultiplier = 1 + extraDamage
35103518
-- if crit bifurcates are enabled, roll for crit twice and add multiplier for each
3511-
if env.mode_effective and skillModList:Flag(cfg, "BifurcateCrit") then
3519+
if env.mode_effective and skillModList:Flag(cfg, "BifurcateCrit") and not forcedOutcomeApplies then
35123520
-- get crit chance and calculate odds of critting twice
35133521
local critChancePercentage = output.PreBifurcateCritChance
35143522
local bifurcateMultiChance = (critChancePercentage ^ 2) / 100

0 commit comments

Comments
 (0)