|
| 1 | +-- Path of Building |
| 2 | +-- |
| 3 | +-- Module: Compare Calcs Helpers |
| 4 | +-- Stateless calcs tooltip helper functions for the Compare Tab. |
| 5 | +-- Handles modifier formatting, source resolution, tabulation, and tooltip rendering. |
| 6 | +-- |
| 7 | +local t_insert = table.insert |
| 8 | +local s_format = string.format |
| 9 | + |
| 10 | +local M = {} |
| 11 | + |
| 12 | +-- Format a modifier value with its type for display |
| 13 | +function M.FormatCalcModValue(value, modType) |
| 14 | + if modType == "BASE" then |
| 15 | + return s_format("%+g base", value) |
| 16 | + elseif modType == "INC" then |
| 17 | + if value >= 0 then |
| 18 | + return value .. "% increased" |
| 19 | + else |
| 20 | + return (-value) .. "% reduced" |
| 21 | + end |
| 22 | + elseif modType == "MORE" then |
| 23 | + if value >= 0 then |
| 24 | + return value .. "% more" |
| 25 | + else |
| 26 | + return (-value) .. "% less" |
| 27 | + end |
| 28 | + elseif modType == "OVERRIDE" then |
| 29 | + return "Override: " .. tostring(value) |
| 30 | + elseif modType == "FLAG" then |
| 31 | + return value and "True" or "False" |
| 32 | + else |
| 33 | + return tostring(value) |
| 34 | + end |
| 35 | +end |
| 36 | + |
| 37 | +-- Format CamelCase mod name to spaced words |
| 38 | +function M.FormatCalcModName(modName) |
| 39 | + return modName:gsub("([%l%d]:?)(%u)", "%1 %2"):gsub("(%l)(%d)", "%1 %2") |
| 40 | +end |
| 41 | + |
| 42 | +-- Resolve a modifier's source to a human-readable name |
| 43 | +function M.ResolveSourceName(mod, build) |
| 44 | + if not mod.source then return "" end |
| 45 | + local sourceType = mod.source:match("[^:]+") or "" |
| 46 | + if sourceType == "Item" then |
| 47 | + local itemId = mod.source:match("Item:(%d+):.+") |
| 48 | + local item = build.itemsTab and build.itemsTab.items[tonumber(itemId)] |
| 49 | + if item then |
| 50 | + return colorCodes[item.rarity] .. item.name |
| 51 | + end |
| 52 | + elseif sourceType == "Tree" then |
| 53 | + local nodeId = mod.source:match("Tree:(%d+)") |
| 54 | + if nodeId then |
| 55 | + local nodeIdNum = tonumber(nodeId) |
| 56 | + local node = (build.spec and build.spec.nodes[nodeIdNum]) |
| 57 | + or (build.spec and build.spec.tree and build.spec.tree.nodes[nodeIdNum]) |
| 58 | + or (build.latestTree and build.latestTree.nodes[nodeIdNum]) |
| 59 | + if node then |
| 60 | + return node.dn or node.name or "" |
| 61 | + end |
| 62 | + end |
| 63 | + elseif sourceType == "Skill" then |
| 64 | + local skillId = mod.source:match("Skill:(.+)") |
| 65 | + if skillId and build.data and build.data.skills[skillId] then |
| 66 | + return build.data.skills[skillId].name |
| 67 | + end |
| 68 | + elseif sourceType == "Pantheon" then |
| 69 | + return mod.source:match("Pantheon:(.+)") or "" |
| 70 | + elseif sourceType == "Spectre" then |
| 71 | + return mod.source:match("Spectre:(.+)") or "" |
| 72 | + end |
| 73 | + return "" |
| 74 | +end |
| 75 | + |
| 76 | +-- Get the modDB and config for a sectionData entry and actor |
| 77 | +function M.GetModStoreAndCfg(sectionData, actor) |
| 78 | + local cfg = {} |
| 79 | + if sectionData.cfg and actor.mainSkill and actor.mainSkill[sectionData.cfg .. "Cfg"] then |
| 80 | + cfg = copyTable(actor.mainSkill[sectionData.cfg .. "Cfg"], true) |
| 81 | + end |
| 82 | + cfg.source = sectionData.modSource |
| 83 | + cfg.actor = sectionData.actor |
| 84 | + |
| 85 | + local modStore |
| 86 | + if sectionData.enemy and actor.enemy then |
| 87 | + modStore = actor.enemy.modDB |
| 88 | + elseif sectionData.cfg and actor.mainSkill then |
| 89 | + modStore = actor.mainSkill.skillModList |
| 90 | + else |
| 91 | + modStore = actor.modDB |
| 92 | + end |
| 93 | + return modStore, cfg |
| 94 | +end |
| 95 | + |
| 96 | +-- Tabulate modifiers for a sectionData entry and actor |
| 97 | +function M.TabulateMods(sectionData, actor) |
| 98 | + local modStore, cfg = M.GetModStoreAndCfg(sectionData, actor) |
| 99 | + if not modStore then return {} end |
| 100 | + |
| 101 | + local rowList |
| 102 | + if type(sectionData.modName) == "table" then |
| 103 | + rowList = modStore:Tabulate(sectionData.modType, cfg, unpack(sectionData.modName)) |
| 104 | + else |
| 105 | + rowList = modStore:Tabulate(sectionData.modType, cfg, sectionData.modName) |
| 106 | + end |
| 107 | + return rowList or {} |
| 108 | +end |
| 109 | + |
| 110 | +-- Build a unique key for a modifier row to match between builds |
| 111 | +function M.ModRowKey(row) |
| 112 | + local src = row.mod.source or "" |
| 113 | + local name = row.mod.name or "" |
| 114 | + local mtype = row.mod.type or "" |
| 115 | + -- Normalize Item sources by stripping the build-specific numeric ID |
| 116 | + -- "Item:5:Body Armour" -> "Item:Body Armour" so same items match across builds |
| 117 | + local normalizedSrc = src:gsub("^(Item):%d+:", "%1:") |
| 118 | + return normalizedSrc .. "|" .. name .. "|" .. mtype |
| 119 | +end |
| 120 | + |
| 121 | +-- Format a single modifier row as a tooltip line |
| 122 | +function M.FormatModRow(row, sectionData, build) |
| 123 | + local displayValue |
| 124 | + if not sectionData.modType then |
| 125 | + displayValue = M.FormatCalcModValue(row.value, row.mod.type) |
| 126 | + else |
| 127 | + displayValue = formatRound(row.value, 2) |
| 128 | + end |
| 129 | + |
| 130 | + local sourceType = row.mod.source and row.mod.source:match("[^:]+") or "?" |
| 131 | + local sourceName = M.ResolveSourceName(row.mod, build) |
| 132 | + local modName = "" |
| 133 | + if type(sectionData.modName) == "table" then |
| 134 | + modName = " " .. M.FormatCalcModName(row.mod.name) |
| 135 | + end |
| 136 | + |
| 137 | + return displayValue, sourceType, sourceName, modName |
| 138 | +end |
| 139 | + |
| 140 | +-- Get breakdown text lines for a build's actor |
| 141 | +function M.GetBreakdownLines(sectionData, build) |
| 142 | + if not sectionData.breakdown then return nil end |
| 143 | + local calcsActor = build.calcsTab and build.calcsTab.calcsEnv and build.calcsTab.calcsEnv.player |
| 144 | + if not calcsActor or not calcsActor.breakdown then return nil end |
| 145 | + |
| 146 | + local breakdown |
| 147 | + local ns, name = sectionData.breakdown:match("^(%a+)%.(%a+)$") |
| 148 | + if ns then |
| 149 | + breakdown = calcsActor.breakdown[ns] and calcsActor.breakdown[ns][name] |
| 150 | + else |
| 151 | + breakdown = calcsActor.breakdown[sectionData.breakdown] |
| 152 | + end |
| 153 | + |
| 154 | + if not breakdown or #breakdown == 0 then return nil end |
| 155 | + |
| 156 | + local lines = {} |
| 157 | + for _, line in ipairs(breakdown) do |
| 158 | + if type(line) == "string" then |
| 159 | + t_insert(lines, line) |
| 160 | + end |
| 161 | + end |
| 162 | + return #lines > 0 and lines or nil |
| 163 | +end |
| 164 | + |
| 165 | +-- Draw the calcs hover tooltip showing breakdown for both builds with common/unique grouping |
| 166 | +-- tooltip, primaryBuild, primaryLabel passed as args instead of self |
| 167 | +function M.DrawCalcsTooltip(tooltip, primaryBuild, primaryLabel, colData, rowLabel, rowX, rowY, rowW, rowH, vp, compareEntry) |
| 168 | + if tooltip:CheckForUpdate(colData, rowLabel) then |
| 169 | + -- Get calcsEnv actors (these have breakdown data populated) |
| 170 | + local primaryCalcsActor = primaryBuild.calcsTab and primaryBuild.calcsTab.calcsEnv |
| 171 | + and primaryBuild.calcsTab.calcsEnv.player |
| 172 | + local compareCalcsActor = compareEntry.calcsTab and compareEntry.calcsTab.calcsEnv |
| 173 | + and compareEntry.calcsTab.calcsEnv.player |
| 174 | + |
| 175 | + local primaryActor = primaryCalcsActor or (primaryBuild.calcsTab.mainEnv and primaryBuild.calcsTab.mainEnv.player) |
| 176 | + local compareActor = compareCalcsActor or (compareEntry.calcsTab.mainEnv and compareEntry.calcsTab.mainEnv.player) |
| 177 | + |
| 178 | + if not primaryActor and not compareActor then |
| 179 | + return |
| 180 | + end |
| 181 | + |
| 182 | + local compareLabel = compareEntry.label or "Compare Build" |
| 183 | + |
| 184 | + -- Tooltip header |
| 185 | + tooltip:AddLine(16, "^7" .. (rowLabel or "")) |
| 186 | + tooltip:AddSeparator(10) |
| 187 | + |
| 188 | + -- Process each sectionData entry in colData |
| 189 | + for _, sectionData in ipairs(colData) do |
| 190 | + -- Show breakdown formulas per build (these are always build-specific) |
| 191 | + if sectionData.breakdown then |
| 192 | + local primaryLines = M.GetBreakdownLines(sectionData, primaryBuild) |
| 193 | + local compareLines = M.GetBreakdownLines(sectionData, compareEntry) |
| 194 | + |
| 195 | + if primaryLines then |
| 196 | + tooltip:AddLine(14, colorCodes.POSITIVE .. primaryLabel .. ":") |
| 197 | + for _, line in ipairs(primaryLines) do |
| 198 | + tooltip:AddLine(14, "^7 " .. line) |
| 199 | + end |
| 200 | + end |
| 201 | + if compareLines then |
| 202 | + tooltip:AddLine(14, colorCodes.WARNING .. compareLabel .. ":") |
| 203 | + for _, line in ipairs(compareLines) do |
| 204 | + tooltip:AddLine(14, "^7 " .. line) |
| 205 | + end |
| 206 | + end |
| 207 | + if primaryLines or compareLines then |
| 208 | + tooltip:AddSeparator(10) |
| 209 | + end |
| 210 | + end |
| 211 | + |
| 212 | + -- Show modifier sources split into common / primary-only / compare-only |
| 213 | + if sectionData.modName then |
| 214 | + local pRows = primaryActor and M.TabulateMods(sectionData, primaryActor) or {} |
| 215 | + local cRows = compareActor and M.TabulateMods(sectionData, compareActor) or {} |
| 216 | + |
| 217 | + if #pRows > 0 or #cRows > 0 then |
| 218 | + -- Build lookup of compare rows by key |
| 219 | + local cByKey = {} |
| 220 | + for _, row in ipairs(cRows) do |
| 221 | + local key = M.ModRowKey(row) |
| 222 | + cByKey[key] = row |
| 223 | + end |
| 224 | + |
| 225 | + -- Classify into common, primary-only, compare-only |
| 226 | + local common = {} -- { { pRow, cRow }, ... } |
| 227 | + local pOnly = {} |
| 228 | + local cMatched = {} -- keys that were matched |
| 229 | + |
| 230 | + for _, pRow in ipairs(pRows) do |
| 231 | + local key = M.ModRowKey(pRow) |
| 232 | + if cByKey[key] then |
| 233 | + t_insert(common, { pRow, cByKey[key] }) |
| 234 | + cMatched[key] = true |
| 235 | + else |
| 236 | + t_insert(pOnly, pRow) |
| 237 | + end |
| 238 | + end |
| 239 | + |
| 240 | + local cOnly = {} |
| 241 | + for _, cRow in ipairs(cRows) do |
| 242 | + local key = M.ModRowKey(cRow) |
| 243 | + if not cMatched[key] then |
| 244 | + t_insert(cOnly, cRow) |
| 245 | + end |
| 246 | + end |
| 247 | + |
| 248 | + -- Sub-section header (e.g., "Sources", "Increased Life Regeneration Rate") |
| 249 | + local sectionLabel = sectionData.label or "Player modifiers" |
| 250 | + tooltip:AddLine(14, "^7" .. sectionLabel .. ":") |
| 251 | + |
| 252 | + -- Common modifiers |
| 253 | + if #common > 0 then |
| 254 | + -- Sort by primary value descending |
| 255 | + table.sort(common, function(a, b) |
| 256 | + if type(a[1].value) == "number" and type(b[1].value) == "number" then |
| 257 | + return a[1].value > b[1].value |
| 258 | + end |
| 259 | + return false |
| 260 | + end) |
| 261 | + tooltip:AddLine(12, "^x808080 Common:") |
| 262 | + for _, pair in ipairs(common) do |
| 263 | + local pVal, sourceType, sourceName, modName = M.FormatModRow(pair[1], sectionData, primaryBuild) |
| 264 | + local cVal = M.FormatModRow(pair[2], sectionData, compareEntry) |
| 265 | + local valStr |
| 266 | + if pVal == cVal then |
| 267 | + valStr = s_format("^7%-10s", pVal) |
| 268 | + else |
| 269 | + valStr = colorCodes.POSITIVE .. s_format("%-5s", pVal) .. "^7/" .. colorCodes.WARNING .. s_format("%-5s", cVal) |
| 270 | + end |
| 271 | + local line = s_format(" %s ^7%-6s ^7%s%s", valStr, sourceType, sourceName, modName) |
| 272 | + tooltip:AddLine(12, line) |
| 273 | + end |
| 274 | + end |
| 275 | + |
| 276 | + -- Primary-only modifiers |
| 277 | + if #pOnly > 0 then |
| 278 | + table.sort(pOnly, function(a, b) |
| 279 | + if type(a.value) == "number" and type(b.value) == "number" then |
| 280 | + return a.value > b.value |
| 281 | + end |
| 282 | + return false |
| 283 | + end) |
| 284 | + tooltip:AddLine(12, colorCodes.POSITIVE .. " " .. primaryLabel .. " only:") |
| 285 | + for _, row in ipairs(pOnly) do |
| 286 | + local displayValue, sourceType, sourceName, modName = M.FormatModRow(row, sectionData, primaryBuild) |
| 287 | + local line = s_format(" ^7%-10s ^7%-6s ^7%s%s", displayValue, sourceType, sourceName, modName) |
| 288 | + tooltip:AddLine(12, line) |
| 289 | + end |
| 290 | + end |
| 291 | + |
| 292 | + -- Compare-only modifiers |
| 293 | + if #cOnly > 0 then |
| 294 | + table.sort(cOnly, function(a, b) |
| 295 | + if type(a.value) == "number" and type(b.value) == "number" then |
| 296 | + return a.value > b.value |
| 297 | + end |
| 298 | + return false |
| 299 | + end) |
| 300 | + tooltip:AddLine(12, colorCodes.WARNING .. " " .. compareLabel .. " only:") |
| 301 | + for _, row in ipairs(cOnly) do |
| 302 | + local displayValue, sourceType, sourceName, modName = M.FormatModRow(row, sectionData, compareEntry) |
| 303 | + local line = s_format(" ^7%-10s ^7%-6s ^7%s%s", displayValue, sourceType, sourceName, modName) |
| 304 | + tooltip:AddLine(12, line) |
| 305 | + end |
| 306 | + end |
| 307 | + |
| 308 | + -- Separator between sub-sections |
| 309 | + tooltip:AddSeparator(6) |
| 310 | + end |
| 311 | + end |
| 312 | + end |
| 313 | + end |
| 314 | + |
| 315 | + SetDrawLayer(nil, 100) |
| 316 | + tooltip:Draw(rowX, rowY, rowW, rowH, vp) |
| 317 | + SetDrawLayer(nil, 0) |
| 318 | +end |
| 319 | + |
| 320 | +return M |
0 commit comments