diff --git a/spec/System/TestOffence_spec.lua b/spec/System/TestOffence_spec.lua index d93047e864..b16b674774 100644 --- a/spec/System/TestOffence_spec.lua +++ b/spec/System/TestOffence_spec.lua @@ -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 diff --git a/src/Data/SkillStatMap.lua b/src/Data/SkillStatMap.lua index 946a24ab69..880e8c8e42 100644 --- a/src/Data/SkillStatMap.lua +++ b/src/Data/SkillStatMap.lua @@ -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), diff --git a/src/Modules/CalcBreakdown.lua b/src/Modules/CalcBreakdown.lua index d32ceb4182..02fcc7a69e 100644 --- a/src/Modules/CalcBreakdown.lua +++ b/src/Modules/CalcBreakdown.lua @@ -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 diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 3fd3f028e3..920afacf43 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -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. @@ -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 @@ -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 @@ -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") @@ -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 @@ -419,6 +437,9 @@ 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) @@ -426,7 +447,7 @@ function calcs.offence(env, actor, activeSkill) 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) @@ -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 @@ -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 @@ -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