From f858c6c53726e371c0b447234f8dc815262e00af Mon Sep 17 00:00:00 2001 From: MoldyDwarf Date: Sat, 26 Jun 2021 15:10:39 -0400 Subject: [PATCH] Fixed secondary and tertiary radii calcs for Molten Strike This commit updates the secondary (inner deadzone) and tertiary (max ball landing distance) radii for the Molten Strike skill. It also overhauls most of the breakdown display for the tertiary radius. The new calculations are based on the derivation from the standalone Molten Strike DPS Calculator (same author as this commit). Look for the maxBallLandingDist variable here for details: https://gitlab.com/MoldyDwarf/MoltenStrikeDPS/-/blob/master/moltenstrike.html Notable math changes: - Projectile speed modifies the distance traveled, not the landing area (making it basically a multiplier on radius, not on area). It's easy to verify this with swapping faster vs. slower projectiles in Haku's hideout and seeing how many tiles away the balls land under the two conditions. - The inner deadzone radius ("secondary radius" in PoB) cannot be modified in any way. This makes all of the tertiary radius calculations slightly trickier. See the aforementioned external calculator link for more details. Notable functions added or changed: - calcMoltenStrikeTertiaryRadius (new): actual calculation, including rounding (but using a simple assumption about how rounding is done for the skill) - setMoltenStrikeTertiaryRadiusBreakdown (new): does all the calculating and rendering of the breakdown display for the tertiary radius. We do both in the same place because we compute and display so many intermediate values. - calcs.offence (modified): now calls the two functions above when skillData.projectileSpeedAppliesToMSAreaOfEffect exists, instead of inlining some math that wasn't quite correct. Also, disables the breakpoint calculations for the secondary radius for Molten Strike since no mods affect that radius. Also: - Moved the label to the top of breakdown.area display for better readability and to be more consistent with other breakdowns. - Allow devs to put vscode workspace config files inside the repo directory. Future work: - The external calculator will be simplified after these changes are released to the community. Many of its form elements will be replaced by having users just type in the skill-specific stats from PoB. - If we're ever able to reliably datamine boss radii, then the DPS calculations from the external calculator could be integrated into PoB. If that ever happens, the external calculator will be retired. --- .gitignore | 1 + src/Modules/CalcBreakdown.lua | 2 +- src/Modules/CalcOffence.lua | 120 +++++++++++++++++++++++++++++----- src/Modules/CalcSections.lua | 6 +- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 6f0dcce6..a4fd3586 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .idea/ .vs/ .vscode/ +*.code-workspace # Development files *.lnk diff --git a/src/Modules/CalcBreakdown.lua b/src/Modules/CalcBreakdown.lua index 9a69df24..afc2fe8c 100644 --- a/src/Modules/CalcBreakdown.lua +++ b/src/Modules/CalcBreakdown.lua @@ -95,6 +95,7 @@ end function breakdown.area(base, areaMod, total, incBreakpoint, moreBreakpoint, redBreakpoint, lessBreakpoint, label) local out = {} + t_insert(out, label) if base ~= total then t_insert(out, s_format("%d ^8(base radius)", base)) t_insert(out, s_format("x %.2f ^8(square root of area of effect modifier)", m_floor(100 * m_sqrt(areaMod)) / 100)) @@ -104,7 +105,6 @@ function breakdown.area(base, areaMod, total, incBreakpoint, moreBreakpoint, red t_insert(out, s_format("^8Next breakpoint: %d%% increased AoE / a %d%% more AoE multiplier", incBreakpoint, moreBreakpoint)) t_insert(out, s_format("^8Previous breakpoint: %d%% reduced AoE / a %d%% less AoE multiplier", redBreakpoint, lessBreakpoint)) end - t_insert(out, label) out.radius = total return out end diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 4b9ab90f..b77da98f 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -134,6 +134,18 @@ local function calcRadius(baseRadius, areaMod) return m_floor(baseRadius * m_floor(100 * m_sqrt(areaMod)) / 100) end +---Calculates the tertiary radius for Molten Strike, correctly handling the deadzone. +---@param baseRadius number +---@param deadzoneRadius number +---@param areaMod number +---@param speedMod number +local function calcMoltenStrikeTertiaryRadius(baseRadius, deadzoneRadius, areaMod, speedMod) + -- For now, we assume that PoE only rounds at the end. + local maxDistIgnoringSpeed = m_sqrt(baseRadius * baseRadius * areaMod - deadzoneRadius * deadzoneRadius * (areaMod - 1)) + local maxDist = m_floor((maxDistIgnoringSpeed - deadzoneRadius) * speedMod + deadzoneRadius) + return maxDist +end + ---Calculates modifiers needed to reach the next and previous radius breakpoints ---@param baseRadius number ---@param incArea number @Additive modifier @@ -167,6 +179,76 @@ local function calcRadiusBreakpoints(baseRadius, incArea, moreArea) return incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint end +---Computes and sets the breakdown for Molten Strike's tertiary radius. +---@param breakdown table +---@param deadzoneRadius number min ball landing distance (cannot be changed by any mods) +---@param baseRadius number default max landing distance with no aoe or proj. speed modifiers +---@param label string top level label to use for the breakdown +---@param incArea number current net increased area modifier +---@param moreArea number current product of all "more" and "less" area modifiers +---@param incSpd number current net increased projectile speed modifier +---@param moreSpd number current product of all "more" and "less" projectile speed modifiers +local function setMoltenStrikeTertiaryRadiusBreakdown(breakdown, deadzoneRadius, baseRadius, label, incArea, moreArea, incSpd, moreSpd) + -- nil -> 1 (no multiplier) + incArea = incArea or 1 + moreArea = moreArea or 1 + incSpd = incSpd or 1 + moreSpd = moreSpd or 1 + ---Helper that calculates the tertiary radius with incremental modifiers to the 4 relevant pools. + ---This helps declutter the code below. + local function calc(extraIncAoePct, extraMoreAoePct, extraIncSpdPct, extraMoreSpdPct) + local areaMod = round(round((incArea + extraIncAoePct / 100) * moreArea * (1 + extraMoreAoePct / 100), 10), 2) + local speedMod = round(round((incSpd + extraIncSpdPct / 100) * moreSpd * (1 + extraMoreSpdPct / 100), 10), 2) + local dist = calcMoltenStrikeTertiaryRadius(baseRadius, deadzoneRadius, areaMod, speedMod) + return dist, areaMod, speedMod + end + -- Current settings. + local currentDist, currentAreaMod, currentSpeedMod = calc(0, 0, 0, 0) + -- Create the detailed breakdown. This includes: + -- * the complete formula as an algebraic expression (ignoring rounding), + -- * the final value, + -- * breakpoints on the 4 modifier pools (increased vs. more crossed with aoe and projectile speed), and + -- * the input variables for the algebraic expression. + local breakdownRadius = breakdown.AreaOfEffectRadiusTertiary or { } + breakdown.AreaOfEffectRadiusTertiary = breakdownRadius + t_insert(breakdownRadius, label) + t_insert(breakdownRadius, " = (sqrt(R*R*a - r*r*(a-1)) - r) * s + r") + t_insert(breakdownRadius, s_format(" = %d", currentDist)) + if currentDist > 0 then + ---Helper for finding one tertiary radius breakpoint value. This is a little slower than what + ---we do in the generic calcRadiusBreakpoints, but this approach requires a lot less code and + ---should be more maintainable given that we need to search for 8 different breakpoints. + ---@param sign number +1 (for increased and more breakpoints) or -1 (for reduced and less breakpoints) + ---@param argIdx number which argument to the calc function we're modifying + local function findBreakpoint(sign, argIdx) + local args = {0, 0, 0, 0} -- starter args for the calc function + repeat + args[argIdx] = args[argIdx] + sign -- increment or decrement the desired arg + local newDist, _, _ = calc(unpack(args)) + until (newDist ~= currentDist) or (newDist == 0) -- stop once we've hit a new radius breakpoint + return args[argIdx] * sign -- remove the sign since we want all positive numbers + end + t_insert(breakdownRadius, s_format("^8Next AoE breakpoint: %d%% increased or %d%% more", findBreakpoint(1, 1), findBreakpoint(1, 2))) + t_insert(breakdownRadius, s_format("^8Next Proj. Speed breakpoint: %d%% increased or %d%% more", findBreakpoint(1, 3), findBreakpoint(1, 4))) + t_insert(breakdownRadius, s_format("^8Previous AoE breakpoint: %d%% increased or %d%% more", findBreakpoint(-1, 1), findBreakpoint(-1, 2))) + t_insert(breakdownRadius, s_format("^8Previous Proj. Speed breakpoint: %d%% increased or %d%% more", findBreakpoint(-1, 3), findBreakpoint(-1, 4))) + end + -- This is the input variable table. + breakdownRadius.label = "Inputs" + breakdownRadius.rowList = { } + breakdownRadius.colList = { + { label = "Variable", key = "name" }, + { label = "Value", key = "value"}, + { label = "Description", key = "description" } + } + t_insert(breakdownRadius.rowList, { name = "r", value = s_format("%d", deadzoneRadius), description = "fixed deadzone radius" }) + t_insert(breakdownRadius.rowList, { name = "R", value = s_format("%d", baseRadius), description = "base outer radius" }) + t_insert(breakdownRadius.rowList, { name = "a", value = s_format("%.2f", currentAreaMod), description = "net AoE multiplier (scales area)" }) + t_insert(breakdownRadius.rowList, { name = "s", value = s_format("%.2f", currentSpeedMod), description = "net projectile speed multiplier (scales range)" }) + -- Trigger the inclusion of the radius display. + breakdownRadius.radius = currentDist +end + function calcSkillCooldown(skillModList, skillCfg, skillData) local cooldownOverride = skillModList:Override(skillCfg, "CooldownRecovery") local cooldown = cooldownOverride or (skillData.cooldown + skillModList:Sum("BASE", skillCfg, "CooldownRecovery")) / calcLib.mod(skillModList, skillCfg, "CooldownRecovery") @@ -249,7 +331,10 @@ function calcs.offence(env, actor, activeSkill) baseRadius = skillData.radiusSecondary + (skillData.radiusExtra or 0) output.AreaOfEffectRadiusSecondary = calcRadius(baseRadius, output.AreaOfEffectModSecondary) if breakdown then - local incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary = calcRadiusBreakpoints(baseRadius, incAreaSecondary, moreAreaSecondary) + local incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary + if not skillData.projectileSpeedAppliesToMSAreaOfEffect then + local incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary = calcRadiusBreakpoints(baseRadius, incAreaSecondary, moreAreaSecondary) + end breakdown.AreaOfEffectRadiusSecondary = breakdown.area(baseRadius, output.AreaOfEffectModSecondary, output.AreaOfEffectRadiusSecondary, incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary, skillData.radiusSecondaryLabel) end end @@ -257,10 +342,22 @@ function calcs.offence(env, actor, activeSkill) local incAreaTertiary, moreAreaTertiary = calcLib.mods(skillModList, skillCfg, "AreaOfEffect", "AreaOfEffectTertiary") output.AreaOfEffectModTertiary = round(round(incAreaTertiary * moreAreaTertiary, 10), 2) baseRadius = skillData.radiusTertiary + (skillData.radiusExtra or 0) - output.AreaOfEffectRadiusTertiary = calcRadius(baseRadius, output.AreaOfEffectModTertiary) - 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) + if skillData.projectileSpeedAppliesToMSAreaOfEffect then + local incSpeedTertiary, moreSpeedTertiary = calcLib.mods(skillModList, skillCfg, "ProjectileSpeed") + output.SpeedModTertiary = round(round(incSpeedTertiary * moreSpeedTertiary, 10), 2) + output.AreaOfEffectRadiusTertiary = calcMoltenStrikeTertiaryRadius(baseRadius, skillData.radiusSecondary, output.AreaOfEffectModTertiary, output.SpeedModTertiary) + if breakdown then + setMoltenStrikeTertiaryRadiusBreakdown( + breakdown, skillData.radiusSecondary, baseRadius, skillData.radiusTertiaryLabel, + incAreaTertiary, moreAreaTertiary, incSpeedTertiary, moreSpeedTertiary + ) + end + else + output.AreaOfEffectRadiusTertiary = calcRadius(baseRadius, output.AreaOfEffectModTertiary) + 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) + end end end end @@ -509,19 +606,6 @@ function calcs.offence(env, actor, activeSkill) skillModList:NewMod("AreaOfEffect", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod)) end end - if skillData.projectileSpeedAppliesToMSAreaOfEffect then - -- Projectile Speed conversion for Molten Stikes Projectile Range - for i, value in ipairs(skillModList:Tabulate("INC", { }, "ProjectileSpeed")) do - local mod = value.mod - skillModList:NewMod("AreaOfEffectSecondary", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod)) - skillModList:NewMod("AreaOfEffectTertiary", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod)) - end - for i, value in ipairs(skillModList:Tabulate("MORE", { }, "ProjectileSpeed")) do - local mod = value.mod - skillModList:NewMod("AreaOfEffectSecondary", "MORE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod)) - skillModList:NewMod("AreaOfEffectTertiary", "MORE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod)) - end - end if skillModList:Flag(nil, "SequentialProjectiles") and not skillModList:Flag(nil, "OneShotProj") and not skillModList:Flag(nil,"NoAdditionalProjectiles") and not skillModList:Flag(nil, "TriggeredBySnipe") then -- Applies DPS multiplier based on projectile count skillData.dpsMultiplier = skillModList:Sum("BASE", skillCfg, "ProjectileCount") diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 4b9994ed..579254b9 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -1002,7 +1002,7 @@ return { { label = "Player modifiers", modName = "EnemyShockChance", cfg = "skill" }, { label = "Enemy modifiers", modName = "SelfShockChance", enemy = true }, }, }, - { label = "Shock Dur. Mod", flag = "shock", { format = "x {2:output:ShockDurationMod}", + { label = "Shock Dur. Mod", flag = "shock", { format = "x {2:output:ShockDurationMod}", { breakdown = "ShockDurationMod" }, { breakdown = "MainHand.ShockDPS" }, { breakdown = "OffHand.ShockDPS" }, @@ -1010,10 +1010,10 @@ return { { label = "Player modifiers", modName = { "EnemyShockDuration", "ShockAsThoughDealing" }, cfg = "skill" }, { label = "Enemy modifiers", modName = "SelfShockDuration", enemy = true }, }, }, - { label = "Maximum Shock", flag = "shock", { format = "{0:output:MaximumShock}%", + { label = "Maximum Shock", flag = "shock", { format = "{0:output:MaximumShock}%", { modName = "ShockMax" }, }, }, - { label = "Current Shock", haveOutput = "CurrentShock", { format = "{0:output:CurrentShock}%", + { label = "Current Shock", haveOutput = "CurrentShock", { format = "{0:output:CurrentShock}%", { label = "Configured Shock", modName = "ShockVal", enemy = true, modType = "BASE" }, { label = "Guaranteed Shocks", modName = "ShockOverride", modType = "BASE" }, }, },