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
11 changes: 11 additions & 0 deletions src/Classes/CalcBreakdownControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ local m_cos = math.cos
local m_pi = math.pi
local band = AND64 -- bit.band

local graph = require("Modules.BreakdownGraph")
---@class CalcBreakdownControl: Control, ControlHost
local CalcBreakdownClass = newClass("CalcBreakdownControl", "Control", "ControlHost")

Expand Down Expand Up @@ -176,6 +177,14 @@ function CalcBreakdownClass:AddBreakdownSection(sectionData)
})
end

if breakdown.graph then
t_insert(self.sectionList, {
type = "GRAPH",
graph = breakdown.graph,
width = 400,
height = 200,
})
end
if breakdown.rowList and #breakdown.rowList > 0 then
-- sort by the first column (the value)
local rowList = copyTable(breakdown.rowList, true)
Expand Down Expand Up @@ -768,6 +777,8 @@ function CalcBreakdownClass:Draw(viewPort)
SetDrawColor(1, 1, 1)
DrawImage(nil, x + 2, sectionY, section.width - 4, section.height)
self:DrawRadiusVisual(x + 4, sectionY + 2, section.width - 8, section.height - 4, section.radius)
elseif section.type == "GRAPH" then
graph.DrawGraph(x + 1, sectionY + 1, section.width - 2, section.height - 2, section.graph)
end
end
SetDrawLayer(nil, 0)
Expand Down
1 change: 0 additions & 1 deletion src/Classes/Tooltip.lua
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,6 @@ end
---@param w number|nil optional width of the UI element being hovered over. Tooltip will position itself outside this box (if possible)
---@param h number|nil optional height of the UI element being hovered over. Needs to be provided alongside `w`
---@param viewPort table A table `{x, y, width, height}` contains active screen boundaries

function TooltipClass:Draw(x, y, w, h, viewPort)
if #self.lines == 0 then
return
Expand Down
214 changes: 214 additions & 0 deletions src/Modules/BreakdownGraph.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
local M = {}

local m_max = math.max
local m_min = math.min
local m_log10 = math.log10
local m_floor = math.floor
local s_format = string.format
local t_insert = table.insert

local function armourFunc(graphData, hit)
local armourDR = m_min(graphData.baseDR / 100 + graphData.armour / (graphData.armour + 10 * hit), graphData.maxDR / 100)
return m_max(armourDR - graphData.overwhelm / 100, 0)
end
local maxEvasion = data.misc.EvadeChanceCap / 100
local function evasionFunc(graphData, accuracy)
local uncappedEvasion = (1 - (0.95 * graphData.evasion) / (graphData.evasion + 4 * accuracy))
return 1 - m_min(maxEvasion, m_max(0.05, uncappedEvasion))
end
local maxDeflection = data.misc.DeflectionChanceCap / 100
local function deflectionFunc(graphData, accuracy)
local penChance = accuracy / (accuracy + graphData.deflection * 0.12)
local uncappedDeflection = (1 -
penChance) * 1.5
return m_min(maxDeflection, uncappedDeflection)
end

local function xToFrac(x, xMin, xMax, log)
if log then
local lo, hi = m_log10(xMin), m_log10(xMax)
return (m_log10(m_max(x, 1e-6)) - lo) / (hi - lo)
end
return (x - xMin) / (xMax - xMin)
end

local function fracToX(frac, xMin, xMax, log)
if log then
local lo, hi = m_log10(xMin), m_log10(xMax)
return 10 ^ (lo + frac * (hi - lo))
end
return xMin + frac * (xMax - xMin)
end

local function formatLabel(n)
if n >= 1000000 then
return s_format("%.1fM", n / 1000000)
elseif n >= 1000 then
return s_format("%.1fk", n / 1000)
else
return s_format("%d", n)
end
end

local function drawDashedLine(x, yStart, yEnd)
local dashPxLength = 3
local spacingPxLength = 4
local y = yStart
while y < yEnd do
DrawImage(nil, x, y, 1, dashPxLength)
y += dashPxLength + spacingPxLength
end
end

local tooltip = new("Tooltip"):Tooltip()

local graphConfigs = {
armour = {
yCapForStat = |gd| -> gd.maxDR / 100,
func = armourFunc,
xLabel = |v| -> string.format("Hit: %d", v),
yLabel = |v| -> string.format("DR: %d%%", v * 100),
xLog = true,
xMax = |gd| -> gd.maxHit * 4,
xMin = |gd| -> gd.configHit / 10,
},
deflection = {
yCapForStat = |_| -> maxDeflection,
func = deflectionFunc,
xLabel = |v| -> string.format("Enemy Accuracy: %d", v),
yLabel = |v| -> string.format("Def. Chance: %d%%", v * 100),
xMax = |gd| -> gd.configAccuracy * 3,
xMin = |gd| -> gd.configAccuracy / 10,
},
evasion = {
yCapForStat = |_| -> maxEvasion,
func = evasionFunc,
xLabel = |v| -> string.format("Enemy Accuracy: %d", v),
yLabel = |v| -> string.format("Dodge Chance: %d%%", v * 100),
xMax = |gd| -> gd.configAccuracy * 3,
xMin = |gd| -> gd.configAccuracy / 10,
}
}
---@param sectionX number
---@param sectionY number
---@param sectionWidth number
---@param sectionHeight number
---@param graphData ArmourGraphData|EvasionGraphData|DeflectionGraphData
function M.DrawGraph(sectionX, sectionY, sectionWidth, sectionHeight, graphData)
local config
if graphData.armour then
config = graphConfigs.armour
elseif graphData.deflection then
config = graphConfigs.deflection
elseif graphData.evasion then
config = graphConfigs.evasion
end
-- value used for max y label
local yCapForStat = config.yCapForStat(graphData)
local yTickText = string.format("%d%%", yCapForStat * 100)

local yLabelWidth = DrawStringWidth(16, "VAR", yTickText)
local graphLeftPad, graphRightPad = 8 + yLabelWidth, 10
local graphTopPad, graphBottomPad = 6, 40
local graphX, graphY = sectionX + graphLeftPad, sectionY + graphTopPad
local graphWidth, graphHeight = sectionWidth - (graphLeftPad + graphRightPad), sectionHeight - (graphTopPad + graphBottomPad)
-- draw background
SetDrawColor(1, 1, 1)
DrawImage(nil, graphX - 1, graphY - 1, graphWidth + 2, graphHeight + 2)
SetDrawColor(0.1, 0.1, 0.1)
DrawImage(nil, graphX, graphY, graphWidth, graphHeight)
local xMin = config.xMin(graphData)
local xMax = config.xMax(graphData)
local log = config.xLog
local samples = {}
for i = 0, graphWidth do
samples[i] = config.func(graphData, fracToX(i / graphWidth, xMin, xMax, log))
end

local bottomY = sectionY + graphTopPad + graphHeight
local subSamples = 4
for i = 1, #samples do
local pointX = graphX + i - 1
local yLeft, yRight = bottomY - samples[i - 1] * graphHeight, bottomY - samples[i] * graphHeight
-- since the shape at this point is actually a slope, this is the top and bottom y of the top edge of the slope
local top = m_max(m_floor(m_min(yLeft, yRight)), graphY)
local bottom = m_min(m_floor(m_max(yLeft, yRight)), bottomY - 1)
-- draw a fully solid 1 x h bar
SetDrawColor(0.5, 0.5, 0.5)
DrawImage(nil, pointX, bottom + 1, 1, bottomY - bottom - 1)
-- draw 1 x 1 pixels to anti-alias the top edge
for row = top, bottom do
local alpha = 0
for s = 1, subSamples do
-- interpolate between the left and right y value
local y = yLeft + (yRight - yLeft) * (s - 0.5) / subSamples
-- test how much is actually covered vertically here
alpha += m_min(m_max(row + 1 - y, 0), 1)
end
SetDrawColor(0.5, 0.5, 0.5, alpha / subSamples)
DrawImage(nil, pointX, row, 1, 1)
end
end

SetDrawColor(1, 1, 1)
local textSize = 16
local textY = bottomY + 2

-- min x
DrawString(graphX + 1, textY, nil, textSize, "VAR", formatLabel(xMin))
if graphData.armour then
-- hit x
DrawString(xToFrac(graphData.configHit, xMin, xMax, true) * graphWidth + graphX, textY, "CENTER_X", textSize, "VAR", formatLabel(graphData.configHit) .. "\nEnemy Hit")
-- max hit x
DrawString(xToFrac(graphData.maxHit, xMin, xMax, true) * graphWidth + graphX, textY, "CENTER_X", textSize, "VAR", formatLabel(graphData.maxHit) .. "\nMax Hit")
-- dashed line hit x
drawDashedLine(xToFrac(graphData.configHit, xMin, xMax, true) * graphWidth + graphX, graphY, graphY + graphHeight)
-- dashed line max hit x
drawDashedLine(xToFrac(graphData.maxHit, xMin, xMax, true) * graphWidth + graphX, graphY, graphY + graphHeight)
elseif graphData.evasion or graphData.deflection then
-- configured enemy accuracy label and dashed line
DrawString(xToFrac(graphData.configAccuracy, xMin, xMax) * graphWidth + graphX, textY, "CENTER_X", textSize, "VAR", formatLabel(graphData.configAccuracy) .. "\nEnemy Accuracy")
drawDashedLine(xToFrac(graphData.configAccuracy, xMin, xMax) * graphWidth + graphX, graphY, graphY + graphHeight)
end
-- max x
DrawString(sectionX + sectionWidth - 8, textY, "RIGHT_X", textSize, "VAR", formatLabel(xMax))

-- max y tick and label
local yTickPos = (1 - yCapForStat) * graphHeight + graphY
DrawImage(nil, graphX - 3, yTickPos, 5, 1)
DrawString(graphX - 3, yTickPos - 8, "RIGHT_X", 16, "VAR", yTickText)

-- zero y label
DrawString(graphX - 3, graphY + graphHeight - 8, "RIGHT_X", 16, "VAR", "0%")

local cursorX, cursorY = GetCursorPos()

-- draw hover tooltip which shows exact value
if graphX <= cursorX and cursorX <= (graphX + graphWidth) and graphY <= cursorY and cursorY <= (graphY + graphHeight) then
local xPx = (cursorX - graphX) - 1
local x = fracToX(xPx / graphWidth, xMin, xMax, log)
local y = samples[xPx]
if y then
local ttTextSize = 16
tooltip:Clear()
tooltip:AddLine(ttTextSize, config.xLabel(x))
tooltip:AddLine(ttTextSize, config.yLabel(y))

-- draw tooltip to the top right of the point on the line
local w, h = GetScreenSize()
local lineX = graphX + xPx
local _, ttHeight = tooltip:GetSize()
local lineY = bottomY - y * graphHeight
-- this gets around the breakdown cell borders drawing on top of the graph
SetDrawLayer(nil, 11)
tooltip:Draw(lineX + 6, lineY - ttHeight + 3, nil, nil, { width = w, height = h, x = 0, y = 0 })
SetDrawLayer(nil, 10)

-- a dot (actually a small square) on the line
SetDrawColor(1, 1, 1)
DrawImage(nil, lineX, lineY - 1, 3, 3)
end
end
end

return M
43 changes: 42 additions & 1 deletion src/Modules/CalcDefence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,9 @@ function calcs.doActorLifeManaSpiritReservation(actor)
end

-- Based on code from FR and BS found in act_*.txt
---@param activeSkill/output/breakdown references table passed in from calc offence
---@param activeSkill any
---@param output Output
---@param breakdown Breakdown
---@param sourceType string type of incoming damage - it will be converted (taken as) from this type if applicable
---@param baseDmg string for which to calculate the damage
---@return table of taken damage parts, and number, sum of damages
Expand Down Expand Up @@ -1521,29 +1523,50 @@ function calcs.defence(env, actor)
s_format("Average enemy accuracy: %d", enemyAccuracy),
s_format("Approximate evade chance: %d%%", output.EvadeChance),
}
---@class EvasionGraphData
breakdown.EvadeChance.graph = {
configAccuracy = enemyAccuracy,
evasion = output.MeleeEvasion,
}
breakdown.MeleeEvadeChance = {
s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"),
s_format("Average enemy accuracy: %d", enemyAccuracy),
s_format("Effective Evasion: %d", output.MeleeEvasion),
s_format("Approximate melee evade chance: %d%%", output.MeleeEvadeChance),
graph = {
configAccuracy = enemyAccuracy,
evasion = output.MeleeEvasion,
}
}
breakdown.ProjectileEvadeChance = {
s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"),
s_format("Average enemy accuracy: %d", enemyAccuracy),
s_format("Effective Evasion: %d", output.ProjectileEvasion),
s_format("Approximate projectile evade chance: %d%%", output.ProjectileEvadeChance),
graph = {
configAccuracy = enemyAccuracy,
evasion = output.ProjectileEvasion,
}
}
breakdown.SpellEvadeChance = {
s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"),
s_format("Average enemy accuracy: %d", enemyAccuracy),
s_format("Effective Evasion: %d", output.SpellEvasion),
s_format("Approximate spell evade chance: %d%%", output.SpellEvadeChance),
graph = {
configAccuracy = enemyAccuracy,
evasion = output.SpellEvasion,
}
}
breakdown.SpellProjectileEvadeChance = {
s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"),
s_format("Average enemy accuracy: %d", enemyAccuracy),
s_format("Effective Evasion: %d", output.SpellProjectileEvasion),
s_format("Approximate spell projectile evade chance: %d%%", output.SpellProjectileEvadeChance),
graph = {
configAccuracy = enemyAccuracy,
evasion = output.SpellProjectileEvasion,
}
}
end
end
Expand All @@ -1560,6 +1583,11 @@ function calcs.defence(env, actor)
s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"),
s_format("Average enemy accuracy: %d", enemyAccuracy),
s_format("Approximate deflect chance: %d%%", output.DeflectChance),
---@class DeflectionGraphData
graph = {
configAccuracy = enemyAccuracy,
deflection = output.DeflectionRating,
}
}
end
end
Expand Down Expand Up @@ -2532,6 +2560,7 @@ function calcs.buildDefenceEstimations(env, actor)
local reduction = modDB:Flag(nil, "SelfIgnore".."Base"..damageType.."DamageReduction") and 0 or output["Base"..damageType.."DamageReductionWhenHit"] or output["Base"..damageType.."DamageReduction"]
local enemyPen = modDB:Flag(nil, "SelfIgnore"..damageType.."Resistance", "EnemyCannotPen"..damageType.."Resistance") and 0 or output[damageType.."EnemyPen"]
local enemyOverwhelm = modDB:Flag(nil, "SelfIgnore"..damageType.."DamageReduction") and 0 or output[damageType.."EnemyOverwhelm"]
---@type number
local damage = output[damageType.."TakenDamage"]
local impaleDamage = enemyImpaleChance > 0 and (damageType == "Physical" and (damage * data.misc.ImpaleStoredDamageBase) or 0) or 0

Expand Down Expand Up @@ -2613,6 +2642,15 @@ function calcs.buildDefenceEstimations(env, actor)
else
t_insert(breakdown[damageType.."DamageReduction"], s_format("Enemy Hit Damage: %d ^8(total incoming damage)", damage))
end
---@class ArmourGraphData
---@field maxHit number Added later by max hit calcs
breakdown[damageType .. "DamageReduction"].graph = {
baseDR = reduction,
maxDR = output[damageType .. "DamageReductionMax"],
overwhelm = enemyOverwhelm,
armour = effectiveAppliedArmour,
configHit = damage,
}
end
if reduction ~= 0 then
t_insert(breakdown[damageType.."DamageReduction"], s_format("Base %s Damage Reduction: %d%%", damageType, reduction))
Expand Down Expand Up @@ -3828,6 +3866,9 @@ function calcs.buildDefenceEstimations(env, actor)

t_insert(breakdown[maxHitCurType], "^8Such a hit would drain the following resources:")
breakdown[maxHitCurType] = incomingDamageBreakdown(breakdown[maxHitCurType], calcs.reducePoolsByDamage(nil, takenDamages, actor), output)
if breakdown[damageType .. "DamageReduction"]?.graph then
breakdown[damageType .. "DamageReduction"].graph.maxHit = finalMaxHit
end
end
end

Expand Down
Loading