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.
This commit is contained in:
MoldyDwarf
2021-06-26 15:10:39 -04:00
parent 790664d861
commit f858c6c537
4 changed files with 107 additions and 22 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
.idea/
.vs/
.vscode/
*.code-workspace
# Development files
*.lnk

View File

@@ -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

View File

@@ -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")

View File

@@ -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" },
}, },