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
37 changes: 37 additions & 0 deletions spec/System/TestOffence_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,43 @@ describe("TestOffence", function()
assert.are.equals(damageWithoutArrowMod, build.calcsTab.mainOutput.AverageDamage)
end)

it("rounds area modifiers before calculating radius", function()
build.skillsTab:PasteSocketGroup("Fireball 20/0 1")
build.configTab.input.customMods = [[
1% increased Area of Effect
50% more Area of Effect
]]
build.configTab:BuildModList()
runCallback("OnFrame")

-- The client truncates 101% * 150% to 151% before taking the square root.
assert.are.equals(1.51, build.calcsTab.mainOutput.AreaOfEffectMod)
end)

it("applies final skill radius modifiers after area scaling", function()
build.skillsTab:PasteSocketGroup("Summon Carrion Golem 20/0 1")
runCallback("OnFrame")

local mainSocketGroup = build.skillsTab.socketGroupList[build.mainSocketGroup]
local activeSkill = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill]
local foundLeapSlam
for index, minionSkill in ipairs(activeSkill.minion.activeSkillList) do
if minionSkill.activeEffect.grantedEffect.id == "BoneGolemLeapSlam" then
activeSkill.activeEffect.srcInstance.skillMinionSkill = index
activeSkill.activeEffect.srcInstance.skillMinionSkillCalcs = index
foundLeapSlam = true
break
end
end
assert.is_true(foundLeapSlam)
build.modFlag = true
build.buildFlag = true
runCallback("OnFrame")

-- Leap Slam has 15 base radius and 30% final radius: floor(15 * 1.30) = 19.
assert.are.equals(19, build.calcsTab.mainEnv.minion.output.AreaOfEffectRadius)
end)

it("parses more/less/increased/reduced minimum and maximum damage of every type", function()
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Expand Down
2 changes: 1 addition & 1 deletion src/Data/SkillStatMap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ return {
mod("AreaOfEffect", "MORE", nil, 0, 0, { type = "Condition", var = "CastOnFrostbolt" }),
},
["active_skill_area_of_effect_radius_+%_final"] = {
mod("AreaOfEffect", "MORE", nil),
mod("AreaOfEffectRadius", "MORE", nil),
},
["active_skill_area_of_effect_+%_final"] = {
mod("AreaOfEffect", "MORE", nil),
Expand Down
5 changes: 4 additions & 1 deletion src/Modules/CalcBreakdown.lua
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,15 @@ return function(modDB, output, actor)
})
end

function breakdown.area(base, areaMod, total, incBreakpoint, moreBreakpoint, redBreakpoint, lessBreakpoint, label)
function breakdown.area(base, areaMod, total, incBreakpoint, moreBreakpoint, redBreakpoint, lessBreakpoint, label, radiusMod)
local out = {}
t_insert(out, label)
if base ~= total then
t_insert(out, s_format("%.1fm ^8(base radius)", base / 10))
t_insert(out, s_format("x %.2f ^8(square root of area of effect modifier)", m_floor(100 * m_sqrt(areaMod)) / 100))
if radiusMod and radiusMod ~= 1 then
t_insert(out, s_format("x %.2f ^8(final radius modifier)", radiusMod))
end
t_insert(out, s_format("= %.1fm", total / 10))
end
if incBreakpoint and moreBreakpoint and redBreakpoint and lessBreakpoint then
Expand Down
69 changes: 45 additions & 24 deletions src/Modules/CalcOffence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,28 @@ local function calcAilmentSourceDamage(activeSkill, output, cfg, breakdown, dama
return min * convMult, max * convMult
end

---Calculates the area percentage after the client integer rounding stages.
---@param incArea number @Additive modifier
---@param moreArea number @Multiplicative modifier
---@return number
local function calcAreaMod(incArea, moreArea)
local baseAreaPercent = roundSymmetric(incArea * 100)
local finalAreaPercent = roundSymmetric(m_max(0, moreArea) * 100)
local areaPercent = baseAreaPercent * finalAreaPercent / 100
areaPercent = areaPercent >= 0 and m_floor(areaPercent) or m_ceil(areaPercent)
return m_max(0, areaPercent) / 100
end

---Calculates skill radius
---@param baseRadius number
---@param areaMod number
---@param radiusMod number|nil
---@return number
local function calcRadius(baseRadius, areaMod)
return m_floor(baseRadius * m_floor(100 * m_sqrt(areaMod)) / 100)
local function calcRadius(baseRadius, areaMod, radiusMod)
local radiusPercent = m_floor(100 * m_sqrt(areaMod))
-- Final radius modifiers apply after Area of Effect has been converted to radius.
radiusPercent = m_floor(radiusPercent * (radiusMod or 1))
return m_floor(baseRadius * radiusPercent / 100)
end

---Calculates the tertiary radius for Molten Strike, correctly handling the deadzone.
Expand All @@ -173,30 +189,31 @@ end
---@param baseRadius number
---@param incArea number @Additive modifier
---@param moreArea number @Multiplicative modifier
---@param radiusMod number|nil @Final radius modifier
---@return number, number, number, number @Next breakpoint: increased, more; Previous breakpoint: reduced, less
local function calcRadiusBreakpoints(baseRadius, incArea, moreArea)
local radius = calcRadius(baseRadius, round(round(incArea * moreArea, 10), 2))
local function calcRadiusBreakpoints(baseRadius, incArea, moreArea, radiusMod)
local radius = calcRadius(baseRadius, calcAreaMod(incArea, moreArea), radiusMod)
local incAreaBreakpoint, redAreaBreakpoint, moreAreaBreakpoint, lessAreaBreakpoint
if radius > 0 then
incAreaBreakpoint = 0
repeat
incAreaBreakpoint = incAreaBreakpoint + 1
local newRadius = calcRadius(baseRadius, round(round((incArea + incAreaBreakpoint / 100) * moreArea, 10), 2))
local newRadius = calcRadius(baseRadius, calcAreaMod(incArea + incAreaBreakpoint / 100, moreArea), radiusMod)
until (newRadius > radius)
redAreaBreakpoint = 0
repeat
redAreaBreakpoint = redAreaBreakpoint + 1
local newRadius = calcRadius(baseRadius, round(round((incArea - redAreaBreakpoint / 100) * moreArea, 10), 2))
local newRadius = calcRadius(baseRadius, calcAreaMod(incArea - redAreaBreakpoint / 100, moreArea), radiusMod)
until (newRadius < radius)
moreAreaBreakpoint = 0
repeat
moreAreaBreakpoint = moreAreaBreakpoint + 1
local newRadius = calcRadius(baseRadius, round(round(incArea * moreArea * (1 + moreAreaBreakpoint / 100), 10), 2))
local newRadius = calcRadius(baseRadius, calcAreaMod(incArea, moreArea * (1 + moreAreaBreakpoint / 100)), radiusMod)
until (newRadius > radius)
lessAreaBreakpoint = 0
repeat
lessAreaBreakpoint = lessAreaBreakpoint + 1
local newRadius = calcRadius(baseRadius, round(round(incArea * moreArea * (1 - lessAreaBreakpoint / 100), 10), 2))
local newRadius = calcRadius(baseRadius, calcAreaMod(incArea, moreArea * (1 - lessAreaBreakpoint / 100)), radiusMod)
until (newRadius < radius)
end
return incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint
Expand Down Expand Up @@ -345,7 +362,8 @@ function calcs.offence(env, actor, activeSkill)

local function calcAreaOfEffect(skillModList, skillCfg, skillData, skillFlags, output, breakdown)
local incArea, moreArea = calcLib.mods(skillModList, skillCfg, "AreaOfEffect", "AreaOfEffectPrimary")
output.AreaOfEffectMod = round(round(incArea * moreArea, 10), 2)
output.AreaOfEffectMod = calcAreaMod(incArea, moreArea)
local radiusMod = calcLib.mod(skillModList, skillCfg, "AreaOfEffectRadius")
if skillData.radiusIsWeaponRange then
local range = 0
if skillFlags.weapon1Attack then
Expand All @@ -359,29 +377,29 @@ function calcs.offence(env, actor, activeSkill)
if skillData.radius then
skillFlags.area = true
local baseRadius = skillData.radius + (skillData.radiusExtra or 0) + skillModList:Sum("BASE", skillCfg, "AreaOfEffect")
output.AreaOfEffectRadius = calcRadius(baseRadius, output.AreaOfEffectMod)
output.AreaOfEffectRadius = calcRadius(baseRadius, output.AreaOfEffectMod, radiusMod)
output.AreaOfEffectRadiusMetres = output.AreaOfEffectRadius / 10
if breakdown then
local incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint = calcRadiusBreakpoints(baseRadius, incArea, moreArea)
breakdown.AreaOfEffectRadius = breakdown.area(baseRadius, output.AreaOfEffectMod, output.AreaOfEffectRadius, incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint, skillData.radiusLabel)
local incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint = calcRadiusBreakpoints(baseRadius, incArea, moreArea, radiusMod)
breakdown.AreaOfEffectRadius = breakdown.area(baseRadius, output.AreaOfEffectMod, output.AreaOfEffectRadius, incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint, skillData.radiusLabel, radiusMod)
end
if skillData.radiusSecondary then
local incAreaSecondary, moreAreaSecondary = calcLib.mods(skillModList, skillCfg, "AreaOfEffect", "AreaOfEffectSecondary")
output.AreaOfEffectModSecondary = round(round(incAreaSecondary * moreAreaSecondary, 10), 2)
output.AreaOfEffectModSecondary = calcAreaMod(incAreaSecondary, moreAreaSecondary)
baseRadius = skillData.radiusSecondary + (skillData.radiusExtra or 0)
output.AreaOfEffectRadiusSecondary = calcRadius(baseRadius, output.AreaOfEffectModSecondary)
output.AreaOfEffectRadiusSecondary = calcRadius(baseRadius, output.AreaOfEffectModSecondary, radiusMod)
output.AreaOfEffectRadiusSecondaryMetres = output.AreaOfEffectRadiusSecondary / 10
if breakdown then
local incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary
if not skillData.projectileSpeedAppliesToMSAreaOfEffect then
incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary = calcRadiusBreakpoints(baseRadius, incAreaSecondary, moreAreaSecondary)
incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary = calcRadiusBreakpoints(baseRadius, incAreaSecondary, moreAreaSecondary, radiusMod)
end
breakdown.AreaOfEffectRadiusSecondary = breakdown.area(baseRadius, output.AreaOfEffectModSecondary, output.AreaOfEffectRadiusSecondary, incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary, skillData.radiusSecondaryLabel)
breakdown.AreaOfEffectRadiusSecondary = breakdown.area(baseRadius, output.AreaOfEffectModSecondary, output.AreaOfEffectRadiusSecondary, incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary, skillData.radiusSecondaryLabel, radiusMod)
end
end
if skillData.radiusTertiary then
local incAreaTertiary, moreAreaTertiary = calcLib.mods(skillModList, skillCfg, "AreaOfEffect", "AreaOfEffectTertiary")
output.AreaOfEffectModTertiary = round(round(incAreaTertiary * moreAreaTertiary, 10), 2)
output.AreaOfEffectModTertiary = calcAreaMod(incAreaTertiary, moreAreaTertiary)
baseRadius = skillData.radiusTertiary + (skillData.radiusExtra or 0)
if skillData.projectileSpeedAppliesToMSAreaOfEffect then
local incSpeedTertiary, moreSpeedTertiary = calcLib.mods(skillModList, skillCfg, "ProjectileSpeed")
Expand All @@ -407,7 +425,7 @@ function calcs.offence(env, actor, activeSkill)
local radiusForBaseRadius = {}
local radiiOccurrences = {}
for adjustedBaseRadius, occurrenceCount in pairs(baseRadiiOccurrences) do
local radiusForDeviation = calcRadius(adjustedBaseRadius, output.AreaOfEffectModTertiary)
local radiusForDeviation = calcRadius(adjustedBaseRadius, output.AreaOfEffectModTertiary, radiusMod)
radiusForBaseRadius[adjustedBaseRadius] = radiusForDeviation
sumOfRandomRadii = sumOfRandomRadii + radiusForDeviation * occurrenceCount
radiiOccurrences[radiusForDeviation] = (radiiOccurrences[radiusForDeviation] or 0) + occurrenceCount
Expand All @@ -419,14 +437,17 @@ function calcs.offence(env, actor, activeSkill)
local incAreaBreakpointTertiary, moreAreaBreakpointTertiary, redAreaBreakpointTertiary, lessAreaBreakpointTertiary = m_huge, m_huge, m_huge, m_huge
t_insert(out, skillData.radiusTertiaryLabel)
t_insert(out, s_format("R ^8(base radius)^7 x %.2f ^8(square root of area of effect modifier)", m_floor(100 * m_sqrt(output.AreaOfEffectModTertiary)) / 100))
if radiusMod ~= 1 then
t_insert(out, s_format("x %.2f ^8(final radius modifier)", radiusMod))
end
local baseRadii = {}
for adjustedBaseRadius in pairs(baseRadiiOccurrences) do
t_insert(baseRadii, adjustedBaseRadius)
end
table.sort(baseRadii, function(a,b) return a < b end)
for _, adjustedBaseRadius in ipairs(baseRadii) do
t_insert(out, s_format("%.1f%% ^8chance of^7 %.1fm ^8base radius resulting in^7 %.1fm ^8final radius", baseRadiiOccurrences[adjustedBaseRadius] / marginWidth * 100, adjustedBaseRadius / 10, radiusForBaseRadius[adjustedBaseRadius] / 10))
local incAreaBreakpointTertiaryIntermediate, moreAreaBreakpointTertiaryIntermediate, redAreaBreakpointTertiaryIntermediate, lessAreaBreakpointTertiaryIntermediate = calcRadiusBreakpoints(adjustedBaseRadius, incAreaTertiary, moreAreaTertiary)
local incAreaBreakpointTertiaryIntermediate, moreAreaBreakpointTertiaryIntermediate, redAreaBreakpointTertiaryIntermediate, lessAreaBreakpointTertiaryIntermediate = calcRadiusBreakpoints(adjustedBaseRadius, incAreaTertiary, moreAreaTertiary, radiusMod)
incAreaBreakpointTertiary = m_min(incAreaBreakpointTertiary, incAreaBreakpointTertiaryIntermediate)
moreAreaBreakpointTertiary = m_min(moreAreaBreakpointTertiary, moreAreaBreakpointTertiaryIntermediate)
redAreaBreakpointTertiary = m_min(redAreaBreakpointTertiary, redAreaBreakpointTertiaryIntermediate)
Expand All @@ -438,10 +459,10 @@ function calcs.offence(env, actor, activeSkill)
breakdown.AreaOfEffectRadiusTertiary = out
end
else
output.AreaOfEffectRadiusTertiary = calcRadius(baseRadius, output.AreaOfEffectModTertiary)
output.AreaOfEffectRadiusTertiary = calcRadius(baseRadius, output.AreaOfEffectModTertiary, radiusMod)
if breakdown then
local incAreaBreakpointTertiary, moreAreaBreakpointTertiary, redAreaBreakpointTertiary, lessAreaBreakpointTertiary = calcRadiusBreakpoints(baseRadius, incAreaTertiary, moreAreaTertiary)
breakdown.AreaOfEffectRadiusTertiary = breakdown.area(baseRadius, output.AreaOfEffectModTertiary, output.AreaOfEffectRadiusTertiary, incAreaBreakpointTertiary, moreAreaBreakpointTertiary, redAreaBreakpointTertiary, lessAreaBreakpointTertiary, skillData.radiusTertiaryLabel)
local incAreaBreakpointTertiary, moreAreaBreakpointTertiary, redAreaBreakpointTertiary, lessAreaBreakpointTertiary = calcRadiusBreakpoints(baseRadius, incAreaTertiary, moreAreaTertiary, radiusMod)
breakdown.AreaOfEffectRadiusTertiary = breakdown.area(baseRadius, output.AreaOfEffectModTertiary, output.AreaOfEffectRadiusTertiary, incAreaBreakpointTertiary, moreAreaBreakpointTertiary, redAreaBreakpointTertiary, lessAreaBreakpointTertiary, skillData.radiusTertiaryLabel, radiusMod)
end
end
output.AreaOfEffectRadiusTertiaryMetres = output.AreaOfEffectRadiusTertiary / 10
Expand Down Expand Up @@ -1276,7 +1297,7 @@ function calcs.offence(env, actor, activeSkill)
end
end
local incArea, moreArea = calcLib.mods(skillModList, skillCfg, "TrapTriggerAreaOfEffect")
local areaMod = round(round(incArea * moreArea, 10), 2)
local areaMod = calcAreaMod(incArea, moreArea)
output.TrapTriggerRadius = calcRadius(data.misc.TrapTriggerRadiusBase, areaMod)
output.TrapTriggerRadiusMetre = output.TrapTriggerRadius / 10
if breakdown then
Expand Down Expand Up @@ -1360,7 +1381,7 @@ function calcs.offence(env, actor, activeSkill)
end

local incArea, moreArea = calcLib.mods(skillModList, skillCfg, "MineDetonationAreaOfEffect")
local areaMod = round(round(incArea * moreArea, 10), 2)
local areaMod = calcAreaMod(incArea, moreArea)
output.MineDetonationRadius = calcRadius(data.misc.MineDetonationRadiusBase, areaMod)
output.MineDetonationRadiusMetre = output.MineDetonationRadius / 10
if breakdown then
Expand Down
Loading