From 42074816a51c1706a4c6ea78136f0d332f4e1de2 Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Thu, 6 Aug 2026 13:23:06 +1000 Subject: [PATCH 1/8] Release 2.67.1 --- New to Path of Building --- * Add support for pseudo stats in trader (vaisest) --- Fixed Crashes --- * Fix crash when double-clicking and dragging an item on small window sizes (vaisest) * Fix crash from equipping Quivers and Shields that grant skills (LocalIdentity) --- Fixed Calculations --- * Fix Chip Away not using a global limit (Trifall) * Fix Foulgrasp support not increasing Brand limit (Trifall) * Fix Block chance rounding with Mana-Infused Staff (LocalIdentity) --- Fixed Behaviours --- * Fix harvest Weapon Range enchant not appearing properly (vaisest) * Fix "Buy Similar" searches for punctuated unique names (mcagnion) * Fix missing modifier controls on imported items (vaisest) * Fix file sort not using folder name when determining the sort order of builds (xoxorwr) * Prevent recursion from stopping on file errors in build list (xoxorwr) --- manifest.xml | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/manifest.xml b/manifest.xml index fde84da4a8..6dccb496a4 100644 --- a/manifest.xml +++ b/manifest.xml @@ -5,7 +5,7 @@ - + @@ -127,7 +127,7 @@ - + @@ -146,11 +146,11 @@ - + - + @@ -189,7 +189,7 @@ - + @@ -220,7 +220,7 @@ - + @@ -237,7 +237,7 @@ - + @@ -247,7 +247,7 @@ - + @@ -255,7 +255,7 @@ - + @@ -268,11 +268,11 @@ - + - + @@ -294,7 +294,7 @@ - + @@ -342,7 +342,7 @@ - + @@ -359,7 +359,7 @@ - + @@ -376,21 +376,21 @@ - + - + - + - + - + @@ -398,9 +398,9 @@ - + - + From d20fc7eb6f6286b3b776036779141585962e7228 Mon Sep 17 00:00:00 2001 From: Andy Li <32141296+andyli00@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:18:38 -0400 Subject: [PATCH 2/8] fix unblinking eye not applying to arcane might attacks (#10155) --- src/Modules/CalcOffence.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Modules/CalcOffence.lua b/src/Modules/CalcOffence.lua index 164ff71666..77a0b0288b 100644 --- a/src/Modules/CalcOffence.lua +++ b/src/Modules/CalcOffence.lua @@ -608,6 +608,13 @@ function calcs.offence(env, actor, activeSkill) end end end + if skillModList:Flag(nil, "EvasionAppliesToSpellDamage") then + -- The Unblinking Eye evasion rating to spell damage conversion + for i, value in ipairs(skillModList:Tabulate("INC", { }, "Evasion")) do + local mod = value.mod + skillModList:NewMod("Damage", mod.type, mod.value, mod.source, ModFlag.Spell, mod.keywordFlags, unpack(mod)) + end + end if skillModList:Flag(nil, "SpellDamageAppliesToAttacks") or skillModList:Flag(skillCfg, "SpellDamageAppliesToAttacks") then -- Spell Damage conversion from Crown of Eyes, Kinetic Bolt, and the Wandslinger notable local multiplier = (skillModList:Max(skillCfg, "ImprovedSpellDamageAppliesToAttacks") or 100) / 100 @@ -642,13 +649,6 @@ function calcs.offence(env, actor, activeSkill) skillModList:NewMod("Damage", mod.type, mod.value, mod.source, bor(ModFlag.Bow, ModFlag.Hit), mod.keywordFlags, unpack(mod)) end end - if skillModList:Flag(nil, "EvasionAppliesToSpellDamage") then - -- The Unblinking Eye evasion rating to spell damage conversion - for i, value in ipairs(skillModList:Tabulate("INC", { }, "Evasion")) do - local mod = value.mod - skillModList:NewMod("Damage", mod.type, mod.value, mod.source, ModFlag.Spell, mod.keywordFlags, unpack(mod)) - end - end if skillModList:Flag(nil, "ClawDamageAppliesToUnarmed") then -- Claw Damage conversion from Rigwald's Curse for i, value in ipairs(skillModList:Tabulate("INC", { flags = bor(ModFlag.Claw, ModFlag.Hit), keywordFlags = KeywordFlag.Hit }, "Damage")) do From 1fb392b0d0cf0534044c89875e93888e3c01fee0 Mon Sep 17 00:00:00 2001 From: cup Date: Fri, 7 Aug 2026 23:54:22 +1000 Subject: [PATCH 3/8] Fix asset casing for Foulborn icon (#10160) --- src/Assets/{BreachIcon.png => breachicon.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Assets/{BreachIcon.png => breachicon.png} (100%) diff --git a/src/Assets/BreachIcon.png b/src/Assets/breachicon.png similarity index 100% rename from src/Assets/BreachIcon.png rename to src/Assets/breachicon.png From 7cc4390855fb5ccaf89ae83858f435505c5ec98f Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:55:11 +0300 Subject: [PATCH 4/8] Also fix for inventory (#10149) --- src/Classes/ItemListControl.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Classes/ItemListControl.lua b/src/Classes/ItemListControl.lua index acb48e8e13..226be64fe6 100644 --- a/src/Classes/ItemListControl.lua +++ b/src/Classes/ItemListControl.lua @@ -313,6 +313,9 @@ function ItemListClass:OnSelClick(index, itemId, doubleClick) self.itemsTab.build.buildFlag = true end elseif doubleClick then + -- disallow dragging since if the cursor is outside the selection after + -- the second click, the item will be stuck onto the cursor + self.selDragging = false local newItem = new("Item", item:BuildRaw()) newItem.id = item.id self.itemsTab:SetDisplayItem(newItem) From b4bd7fd926339236b5d0a0a7f9e49bdd27b70a93 Mon Sep 17 00:00:00 2001 From: Wires77 Date: Fri, 7 Aug 2026 08:57:26 -0500 Subject: [PATCH 5/8] Fix Scornful Herald not counting buffs as affecting you (#10158) * Fixes #10157: Fix Scornful Herald not counting buffs as affecting you * Simpler fix of making it have 100% less buff effect Made it use less buff effect instead so it would show up in a breakdown if we made one in the future --------- Co-authored-by: LocalIdentity --- spec/System/TestSkills_spec.lua | 15 +++++++++++++++ src/Data/Skills/sup_int.lua | 3 ++- src/Export/Skills/sup_int.txt | 3 ++- src/Modules/CalcPerform.lua | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/spec/System/TestSkills_spec.lua b/spec/System/TestSkills_spec.lua index 23b6b864ec..af8896f64c 100644 --- a/spec/System/TestSkills_spec.lua +++ b/spec/System/TestSkills_spec.lua @@ -151,6 +151,21 @@ describe("TestSkills", function() assert.True(preAdrenalineMaxStages < build.calcsTab.mainEnv.player.activeSkillList[1].skillModList:Sum("BASE", nil, "Multiplier:BlightMaxStages")) end) + it("calculates added resistance from heralds even when using scornful herald", function() + build.skillsTab:PasteSocketGroup("Cyclone 20/0 1\n") + build.skillsTab:PasteSocketGroup("Herald of Thunder 20/0 1\nScornful Herald 20/0 1\n") + build.itemsTab:CreateDisplayItemFromRaw([[Circle of Regret + Topaz Ring + {tags:resistance}+(50-60)% to Lightning Resistance while affected by Herald of Thunder + ]]) + build.itemsTab:AddDisplayItem() + + runCallback("OnFrame") + + assert.are.equals(0, build.calcsTab.mainEnv.player.modDB:Sum("BASE", nil, "LightningMin")) + assert.are.equals(-5, build.calcsTab.mainEnv.player.modDB:Sum("BASE", nil, "LightningResist")) + end) + it("calculates Wintertide Brand average damage for attached brands and Wintertide's End", function() local function getAverageDamageMultiplier() for _, mod in ipairs(build.calcsTab.mainEnv.player.mainSkill.skillModList) do diff --git a/src/Data/Skills/sup_int.lua b/src/Data/Skills/sup_int.lua index 1d1a3ad039..aedbe089e8 100644 --- a/src/Data/Skills/sup_int.lua +++ b/src/Data/Skills/sup_int.lua @@ -5968,7 +5968,8 @@ skills["SupportScornfulHerald"] = { statDescriptionScope = "gem_stat_descriptions", statMap = { ["herald_no_buff_effect"] = { - flag("DisableBuff", { type = "SkillType", skillType = SkillType.Herald }), + mod("BuffEffect", "MORE", nil, 0, 0, { type = "SkillType", skillType = SkillType.Herald }), + value = -100, }, }, qualityStats = { diff --git a/src/Export/Skills/sup_int.txt b/src/Export/Skills/sup_int.txt index f69e71fd77..f2c4c31028 100644 --- a/src/Export/Skills/sup_int.txt +++ b/src/Export/Skills/sup_int.txt @@ -828,7 +828,8 @@ local skills, mod, flag, skill = ... #skill SupportScornfulHerald statMap = { ["herald_no_buff_effect"] = { - flag("DisableBuff", { type = "SkillType", skillType = SkillType.Herald }), + mod("BuffEffect", "MORE", nil, 0, 0, { type = "SkillType", skillType = SkillType.Herald }), + value = -100, }, }, #mods diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 6fb9a2f984..65b8a15b3e 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -2263,7 +2263,7 @@ function calcs.perform(env, skipEHP) -- Also nothing :/ elseif buff.type == "GlobalDB" then modDB:AddList(buff.modList) -- Allows a skill mod to affect other skills through modDB - elseif buff.type == "Buff" and not skillModList:Flag(skillCfg, "DisableBuff") then + elseif buff.type == "Buff" then if env.mode_buffs and (not activeSkill.skillFlags.totem or buff.allowTotemBuff) then local skillCfg = buff.activeSkillBuff and skillCfg local modStore = buff.activeSkillBuff and skillModList or modDB From f9decb4c41d0b8d47384f70dfbdd8d08a5b7a01e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:25:40 +1000 Subject: [PATCH 6/8] Release 2.67.2 (#10163) * Prepare release 2.67.2 * Changelog --------- Co-authored-by: LocalIdentity Co-authored-by: LocalIdentity --- CHANGELOG.md | 15 +++++++++++++++ changelog.txt | 11 +++++++++++ manifest.xml | 2 +- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb162f6f0..3785ccfddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [v2.67.2](https://github.com/PathOfBuildingCommunity/PathOfBuilding-PoE2/tree/v2.67.2) (2026/08/07) + +[Full Changelog](https://github.com/PathOfBuildingCommunity/PathOfBuilding/compare/v2.67.1...v2.67.2) + + +## What's Changed +### Fixed Calculations +- Fix Scornful Herald not counting buffs as affecting you [\#10158](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/10158) ([Wires77](https://github.com/Wires77)) +- Fix The Unblinking Eye increased evasion not applying to Attacks with Arcane Might [\#10155](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/10155) ([andyli00](https://github.com/andyli00)) +### Fixed Behaviours +- Fix Foulborn icon display on Linux [\#10160](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/10160) ([cupkax](https://github.com/cupkax)) +- Fix double clicking and dragging items causing issues [\#10149](https://github.com/PathOfBuildingCommunity/PathOfBuilding/pull/10149) ([vaisest](https://github.com/vaisest)) + + + ## [v2.67.1](https://github.com/PathOfBuildingCommunity/PathOfBuilding-PoE2/tree/v2.67.1) (2026/08/06) [Full Changelog](https://github.com/PathOfBuildingCommunity/PathOfBuilding/compare/v2.67.0...v2.67.1) diff --git a/changelog.txt b/changelog.txt index e1f57e551f..c9fb0b0754 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,14 @@ +VERSION[2.67.2][2026/08/07] + +--- Fixed Calculations --- +* Fix Scornful Herald not counting buffs as affecting you (Wires77) +* Fix The Unblinking Eye increased evasion not applying to Attacks with Arcane Might (andyli00) + +--- Fixed Behaviours --- +* Fix Foulborn icon display on Linux (cupkax) +* Fix double-clicking and dragging items causing issues (vaisest) + + VERSION[2.67.1][2026/08/06] --- New to Path of Building --- diff --git a/manifest.xml b/manifest.xml index 6dccb496a4..0d1f08b410 100644 --- a/manifest.xml +++ b/manifest.xml @@ -1,6 +1,6 @@ - + From aed56a06a001ea13e86615f1d0fa2c1eaa89b7db Mon Sep 17 00:00:00 2001 From: LocalIdentity Date: Sat, 8 Aug 2026 00:29:15 +1000 Subject: [PATCH 7/8] Release 2.67.2 --- Fixed Calculations --- * Fix Scornful Herald not counting buffs as affecting you (Wires77) * Fix The Unblinking Eye increased evasion not applying to Attacks with Arcane Might (andyli00) --- Fixed Behaviours --- * Fix Foulborn icon display on Linux (cupkax) * Fix double-clicking and dragging items causing issues (vaisest) --- manifest.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/manifest.xml b/manifest.xml index 0d1f08b410..f1b269dca2 100644 --- a/manifest.xml +++ b/manifest.xml @@ -5,7 +5,7 @@ - + @@ -38,7 +38,7 @@ - + @@ -147,7 +147,7 @@ - + @@ -271,7 +271,7 @@ - + @@ -383,8 +383,8 @@ - - + + From cd67897683804c0c3f18970464da1d281593e931 Mon Sep 17 00:00:00 2001 From: vaisest <4550061+vaisest@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:11:51 +0300 Subject: [PATCH 8/8] Add breakdowns to the build side bar on mouseover --- src/Classes/CalcBreakdownControl.lua | 52 +++++-- src/Classes/CalcsTab.lua | 4 +- src/Classes/TextListControl.lua | 31 +++- src/Data/Global.lua | 3 +- src/Modules/Build.lua | 215 ++++++++++++++++++++++++++- src/Modules/BuildDisplayStats.lua | 92 ++++++++---- src/Modules/CalcPerform.lua | 2 +- src/Modules/CalcSections.lua | 52 ++++++- src/Modules/CalcSetup.lua | 1 + src/Modules/Calcs.lua | 1 + 10 files changed, 403 insertions(+), 50 deletions(-) diff --git a/src/Classes/CalcBreakdownControl.lua b/src/Classes/CalcBreakdownControl.lua index 6983b4b941..205a7ec073 100644 --- a/src/Classes/CalcBreakdownControl.lua +++ b/src/Classes/CalcBreakdownControl.lua @@ -24,7 +24,14 @@ local CalcBreakdownClass = newClass("CalcBreakdownControl", "Control", "ControlH self.rangeGuide:Load("Assets/range_guide.png") self.uiOverlay = NewImageHandle() self.uiOverlay:Load("Assets/game_ui_small.png") + self.borderThickness = 2 self.controls.scrollBar = new("ScrollBarControl", {"RIGHT",self,"RIGHT"}, {-2, 0, 18, 0}, 80, "VERTICAL", true) + -- keep the scroll bar inside the border + self.controls.scrollBar.x = function() + return -self.borderThickness + end + self.pinnedColour = { 0.25, 1, 0.25 } + self.borderColour = { 0.33, 0.66, 0.33 } end) function CalcBreakdownClass:IsMouseOver() @@ -34,21 +41,34 @@ function CalcBreakdownClass:IsMouseOver() return self:IsMouseInBounds() or self:GetMouseOverControl() end -function CalcBreakdownClass:SetBreakdownData(displayData, pinned) +function CalcBreakdownClass:GetActor() + local env = self.calcsTab[self.envName or "calcsEnv"] + local actor = self.calcsTab.input.showMinion and env.minion or env.player + if self.forceActor then + actor = env[self.forceActor] + end + return actor, env +end +---@param displayData any +---@param pinned any +---@param forceActor "player"|"minion"|nil +function CalcBreakdownClass:SetBreakdownData(displayData, pinned, forceActor) self.pinned = pinned if displayData == self.sourceData then return end self.sourceData = displayData + self.forceActor = forceActor self.shown = false if not displayData then return end -- Build list of sections + local actor, env = self:GetActor() self.sectionList = wipeTable(self.sectionList) for _, sectionData in ipairs(displayData) do - if self.calcsTab:CheckFlag(sectionData) then + if self.calcsTab:CheckFlag(sectionData, actor, env.player) then if sectionData.breakdown then self:AddBreakdownSection(sectionData) elseif sectionData.modName then @@ -57,7 +77,11 @@ function CalcBreakdownClass:SetBreakdownData(displayData, pinned) end end if #self.sectionList == 0 then - self.calcsTab:ClearDisplayStat() + if self.clearDisplayFunc then + self.clearDisplayFunc() + else + self.calcsTab:ClearDisplayStat() + end return end @@ -116,7 +140,7 @@ end -- Add sections based on the breakdown data generated by the Calcs module function CalcBreakdownClass:AddBreakdownSection(sectionData) - local actor = self.calcsTab.input.showMinion and self.calcsTab.calcsEnv.minion or self.calcsTab.calcsEnv.player + local actor = self:GetActor() local breakdown local ns, name = sectionData.breakdown:match("^(%a+)%.(%a+)$") if ns then @@ -269,7 +293,7 @@ end -- Add a table section showing a list of modifiers function CalcBreakdownClass:AddModSection(sectionData, modList) - local actor = self.calcsTab.input.showMinion and self.calcsTab.calcsEnv.minion or self.calcsTab.calcsEnv.player + local actor = self:GetActor() local build = self.calcsTab.build -- Build list of modifiers to display @@ -663,12 +687,13 @@ function CalcBreakdownClass:Draw(viewPort) local scrollBar = self.controls.scrollBar local width = self.contentWidth local height = self.contentHeight + local borderThickness = self.borderThickness if self.contentHeight > viewPort.height then -- Content won't fit the screen height, so set the scrollbar width = self.contentWidth + scrollBar.width height = viewPort.height - scrollBar.height = height - 4 - scrollBar:SetContentDimension(self.contentHeight - 4, viewPort.height - 4) + scrollBar.height = height - borderThickness * 2 + scrollBar:SetContentDimension(self.contentHeight - borderThickness * 2, viewPort.height - borderThickness * 2) else scrollBar:SetContentDimension(0, 0) end @@ -685,15 +710,14 @@ function CalcBreakdownClass:Draw(viewPort) -- Draw background SetDrawLayer(nil, 10) SetDrawColor(0, 0, 0, 0.9) - DrawImage(nil, x + 2, y + 2, width - 4, height - 4) + DrawImage(nil, x + borderThickness, y + borderThickness, width - borderThickness * 2, height - borderThickness * 2) -- Draw border (this is put in sub layer 11 so it draws over the contents, in case they don't fit the screen) SetDrawLayer(nil, 11) if self.pinned then - SetDrawColor(0.25, 1, 0.25) + SetDrawColor(unpack(self.pinnedColour)) else - SetDrawColor(0.33, 0.66, 0.33) + SetDrawColor(unpack(self.borderColour)) end - local borderThickness = 2 DrawImage(nil, x, y, width, borderThickness) DrawImage(nil, x, y + height - borderThickness, width, borderThickness) DrawImage(nil, x, y, borderThickness, height) @@ -735,7 +759,11 @@ function CalcBreakdownClass:OnKeyDown(key, doubleClick) if key:match("BUTTON") then if not mOver then -- Mouse click outside the control, hide the breakdown - self.calcsTab:ClearDisplayStat() + if self.clearDisplayFunc then + self.clearDisplayFunc() + else + self.calcsTab:ClearDisplayStat() + end self.shown = false return end diff --git a/src/Classes/CalcsTab.lua b/src/Classes/CalcsTab.lua index f621c7de75..d8c17c5a91 100644 --- a/src/Classes/CalcsTab.lua +++ b/src/Classes/CalcsTab.lua @@ -386,7 +386,7 @@ function CalcsTabClass:SetDisplayStat(displayData, pin) self.controls.breakdown:SetBreakdownData(displayData, pin) end -function CalcsTabClass:CheckFlag(obj, actor) +function CalcsTabClass:CheckFlag(obj, actor, player) actor = actor or (self.input.showMinion and self.calcsEnv.minion or self.calcsEnv.player) local skillFlags = actor.mainSkill.skillFlags if obj.flag and not skillFlags[obj.flag] then @@ -399,7 +399,7 @@ function CalcsTabClass:CheckFlag(obj, actor) end end end - if obj.playerFlag and not self.calcsEnv.player.mainSkill.skillFlags[obj.playerFlag] then + if obj.playerFlag and not (player or self.calcsEnv.player).mainSkill.skillFlags[obj.playerFlag] then return end if obj.notFlag and skillFlags[obj.notFlag] then diff --git a/src/Classes/TextListControl.lua b/src/Classes/TextListControl.lua index 7302a153b9..ea5029dd66 100644 --- a/src/Classes/TextListControl.lua +++ b/src/Classes/TextListControl.lua @@ -42,11 +42,37 @@ function TextListClass:Draw(viewPort) local lineY = -scrollBar.offset for _, lineInfo in ipairs(self.list) do if lineInfo[colIndex] then - DrawString(lineInfo.x or colInfo.x, lineY, lineInfo.align or colInfo.align, lineInfo.height, lineInfo.font or "VAR", lineInfo[colIndex]) + local textX = lineInfo.x or colInfo.x + local align = lineInfo.align or colInfo.align + DrawString(textX, lineY, align, lineInfo.height, lineInfo.font or "VAR", lineInfo[colIndex]) + if lineInfo.underline and lineInfo.underline[colIndex] then + local width = DrawStringWidth(lineInfo.height, "VAR", StripEscapes(lineInfo[colIndex])) + -- note: not fully handled. this is currently only used for + -- the side bar stats + if align == "RIGHT_X" then + textX = textX - width + end + SetDrawColor(0.5, 0.5, 0.5) + DrawImage(nil, textX, lineY + lineInfo.height, width, 1) + end end lineY = lineY + lineInfo.height end end + -- determine which line the user is hovering over + self.hoveredLine = nil + local cursorX, cursorY = GetCursorPos() + if cursorX >= x + 2 and cursorX < x + width - 18 and cursorY >= y + 2 and cursorY < y + height - 2 then + local rowY = y - scrollBar.offset + 2 + -- suboptimal. should do binary search if this causes performance problems + for _, lineInfo in ipairs(self.list) do + if cursorY >= rowY and cursorY < rowY + lineInfo.height then + self.hoveredLine = { line = lineInfo, x = x, y = rowY, width = width } + break + end + rowY = rowY + lineInfo.height + end + end SetViewport() end @@ -54,6 +80,9 @@ function TextListClass:OnKeyDown(key, doubleClick) if not self:IsShown() or not self:IsEnabled() then return end + if key == "LEFTBUTTON" and self.onClick then + self.onClick(self.hoveredLine) + end local mOverControl = self:GetMouseOverControl() if mOverControl and mOverControl.OnKeyDown then return mOverControl:OnKeyDown(key) diff --git a/src/Data/Global.lua b/src/Data/Global.lua index f7d5e469e3..c675233afc 100644 --- a/src/Data/Global.lua +++ b/src/Data/Global.lua @@ -87,8 +87,9 @@ function updateColorCode(code, color) end function hexToRGB(hex) + hex = hex:gsub("%^x", "") -- Remove "^x" prefix hex = hex:gsub("0x", "") -- Remove "0x" prefix - hex = hex:gsub("#","") -- Remove '#' if present + hex = hex:gsub("#", "") -- Remove '#' if present if #hex ~= 6 then return nil end diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 6ad4078eba..61f898a935 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -28,8 +28,8 @@ end ---matchFlags --- Compares the skill flags table against the line flag settings --- Required enabling flags check takes precedence over disabling flags check ----@param reqFlags table containing the required flags ----@param notFlags table containing the disabling flags +---@param reqFlags table|string containing the required flags +---@param notFlags table|string containing the disabling flags ---@param flags table containing the flags to match against local function matchFlags(reqFlags, notFlags, flags) if type(reqFlags) == "string" then @@ -566,6 +566,9 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin local warnHeight = main.showWarnings and #self.controls.warnings.lines > 0 and 18 or 0 return main.screenH - main.mainBarHeight - 4 - y - warnHeight end + function self.controls.statBox.onClick(hoveredLine) + self:SetDisplayStat(hoveredLine, true) + end self.controls.warnings = new("Control",{"TOPLEFT",self.controls.statBox,"BOTTOMLEFT",true}, {0, 0, 0, 18}) self.controls.warnings.lines = {} self.controls.warnings.width = function(control) @@ -603,6 +606,14 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.treeTab = new("TreeTab", self) self.skillsTab = new("SkillsTab", self) self.calcsTab = new("CalcsTab", self) + self.controls.breakdown = new("CalcBreakdownControl", self.calcsTab) + self.controls.breakdown.pinnedColour = hexToRGB(colorCodes.CUSTOM) or error("failed to set breakdown pin colour") + self.controls.breakdown.envName = "mainEnv" + self.controls.breakdown.clearDisplayFunc = function() + self:ClearDisplayStat() + end + -- ensure that we don't refer to outdated calc tab sections + self.breakdownIndex = nil self.compareTab = new("CompareTab", self) -- Used for pined calcs panes self.overlayPanes = { } @@ -1244,6 +1255,10 @@ function buildMode:OnFrame(inputEvents) self.buildFlag = false self.skillsTab:UpdateSocketGroups() self.calcsTab:BuildOutput() + if self.controls.breakdown.pinned and self.breakdownInputs then + self.controls.breakdown:SetBreakdownData() + self.controls.breakdown:SetBreakdownData(unpack(self.breakdownInputs)) + end self:RefreshStatList() self.configTab.calcFunc, self.configTab.calcBase = self.calcsTab:GetMiscCalculator(self) end @@ -1318,7 +1333,52 @@ function buildMode:OnFrame(inputEvents) DrawImage(nil, sideBarWidth - 4, 32, 4, main.screenH - 32) + local hovered = self.controls.statBox and self.controls.statBox.hoveredLine + self:SetDisplayStat(hovered, false) self:DrawControls(main.viewPort) + if self.controls.breakdown.pinned and self.breakdownInputs and not self.selControl then + self:SelectControl(self.controls.breakdown) + end +end + +function buildMode:SetDisplayStat(hovered, pin) + if not hovered or (not pin and self.controls.breakdown.pinned) then + -- if this is a hover popup, clear when the user moved mouse out of frame + if not self.controls.breakdown.pinned then + self:ClearDisplayStat() + end + return + end + local key = hovered.line.breakdown + -- user clicked a second time: clear + if pin and self.controls.breakdown.pinned then + self:ClearDisplayStat() + return + -- avoid being able to pin empty breakdowns + elseif pin and not self.controls.breakdown.pinned and not key then + return + end + + self.sidebarBreakdownData = self:GetSidebarBreakdown(key, hovered.line.modNames, hovered.line.ignoredSections) + self.sidebarBreakdownData.x = hovered.x - 1 + self.sidebarBreakdownData.y = hovered.y + self.sidebarBreakdownData.width = hovered.width + if hovered.line.breakdownColour then + self.controls.breakdown.borderColour = hovered.line.breakdownColour + end + local breakdownInputs = { self.sidebarBreakdownData, pin, hovered.line.actorName } + -- avoid rebuilding the sidebar every frame + if tableDeepEquals(breakdownInputs, self.breakdownInputs) then + return + end + self.breakdownInputs = breakdownInputs + self.controls.breakdown:SetBreakdownData(unpack(breakdownInputs)) +end + +function buildMode:ClearDisplayStat() + self.controls.breakdown:SetBreakdownData() + self.breakdownInputs = nil + self.sidebarBreakdownData = nil end -- Opens the game version conversion popup @@ -1685,8 +1745,133 @@ function buildMode:FormatStat(statData, statVal, overCapStatVal, colorOverride) return valStr end +-- lazily index the calcs tab sections so that we can look up the matching +-- sections by breakdown name +function buildMode:BuildBreakdownIndex() + if self.breakdownIndex then + return self.breakdownIndex + end + local index = {} + local sectionList = self.calcsTab.sectionList + for _, section in ipairs(sectionList) do + if section.subSection then + for _, subSection in ipairs(section.subSection) do + -- cells for each mod name + local modSectionsByName = {} + local breakdownCells = {} + for _, row in ipairs(subSection.data) do + for _, colData in ipairs(row) do + for _, cell in ipairs(colData) do + if cell.breakdown and not breakdownCells[cell.breakdown] then + breakdownCells[cell.breakdown] = colData + end + if cell.modName then + -- inherit flags from parent row + local copy = copyTable(row, true) + for k, v in pairs(cell) do + copy[k] = v + end + local names = type(cell.modName) == "table" and cell.modName or { cell.modName } + for _, name in ipairs(names) do + modSectionsByName[name] = modSectionsByName[name] or {} + t_insert(modSectionsByName[name], copy) + end + end + end + end + end + for key, colData in pairs(breakdownCells) do + -- ensure that we only set each breakdown once + if not index[key] then + index[key] = { colData = colData, modSectionsByName = modSectionsByName } + end + end + end + end + end + self.breakdownIndex = index + return index +end + +---@param key string A breakdown key +---@param modNames string[]? A modName key. Required if the mod name is different from the breakdown key. E.g. breakdown has Time while the mod has Speed +---@param ignoredSections table? +function buildMode:GetSidebarBreakdown(key, modNames, ignoredSections) + local entry = self:BuildBreakdownIndex()[key] + if not entry then + local output = { { breakdown = key } } + -- it's possible for calc sections to not have a breakdown, while still + -- having a mod list. this handles e.g. spell suppression chance + if modNames then + table.insert(output, { modName = modNames }) + end + return output + end + local displayData = {} + -- this will include all of the hover information for the selected breakdown + for _, cell in ipairs(entry.colData) do + t_insert(displayData, copyTable(cell)) + end + -- sometimes the breakdown doesn't include the mod names. for + -- example life has its mod tables separated to the inc and more breakdowns. + if modNames then + for _, modKey in ipairs(modNames) do + if entry.modSectionsByName[modKey] then + for _, statDisplay in ipairs(entry.modSectionsByName[modKey]) do + t_insert(displayData, statDisplay) + end + end + end + end + + local visitedSections = {} + -- remove sections that have been manually excluded, or ones that have + -- already been added. it's probably not guaranteed that the sections with + -- the same label always contain the exact same contents, but this gets rid + -- of pointless repeated sections + for i = #displayData, 1, -1 do + local label = displayData[i].label + if label then + if visitedSections[label] or (ignoredSections and ignoredSections[label]) then + table.remove(displayData, i) + end + visitedSections[label] = true + end + end + if #displayData == 0 then + return { { breakdown = key } } + end + return displayData +end + +---@param actorName string +function buildMode:GetStatBreakdownKey(statData, actorName) + local env = self.calcsTab.mainEnv[actorName] + local breakdown = env and env.breakdown + if not breakdown then + return nil + end + -- note that this intentionally doesn't check if the breakdown exists so + -- that we can still display mod lists + if statData.breakdown then + return statData.breakdown + end + if statData.childStat then + local parent = breakdown[statData.stat] + if parent and parent[statData.childStat] then + return statData.stat .. "." .. statData.childStat + end + end + if breakdown[statData.stat] then + return statData.stat + end + return nil +end -- Add stat list for given actor -function buildMode:AddDisplayStatList(statList, actor) +---@param statList DisplayStat[] +---@param actor any +---@param actorName string +function buildMode:AddDisplayStatList(statList, actor, actorName) local statBoxList = self.controls.statBox.list for index, statData in ipairs(statList) do if matchFlags(statData.flag, statData.notFlag, actor.mainSkill.skillFlags) then @@ -1753,10 +1938,30 @@ function buildMode:AddDisplayStatList(statList, actor) formattedStat = formattedStat .. "^x808080 (" .. suffix .. ")" end end + local breakdown = self:GetStatBreakdownKey(statData, actorName) + local ignoredSections + if statData.ignoredSections then + ignoredSections = {} + for _, label in ipairs(statData.ignoredSections) do + ignoredSections[label] = true + end + end + -- SimpleGraphic also has these short codes which cannot be easily translated to hex + local colourCodeMap = { + ["^1"] = { 0.917, 0, 0 }, + ["^7"] = { 1, 1, 1 }, + } + local breakdownColour = labelColor ~= "^7" and hexToRGB(labelColor) or colourCodeMap[labelColor] t_insert(statBoxList, { height = 16, labelColor..statData.label..":", formattedStat, + breakdown = breakdown, + modNames = statData.modNames, + underline = { false, not not breakdown }, + actorName = actorName, + ignoredSections = ignoredSections, + breakdownColour = breakdownColour, }) end end @@ -1861,7 +2066,7 @@ function buildMode:RefreshStatList() t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, "^8" .. self.calcsTab.mainEnv.minion.mainSkill.infoMessage2}) end end - self:AddDisplayStatList(self.minionDisplayStats, self.calcsTab.mainEnv.minion) + self:AddDisplayStatList(self.minionDisplayStats, self.calcsTab.mainEnv.minion, "minion") t_insert(statBoxList, { height = 10 }) t_insert(statBoxList, { height = 18, "^7Player:" }) end @@ -1869,7 +2074,7 @@ function buildMode:RefreshStatList() t_insert(statBoxList, { height = 16, "^7Skill disabled:" }) t_insert(statBoxList, { height = 14, align = "CENTER_X", x = 140, self.calcsTab.mainEnv.player.mainSkill.disableReason }) end - self:AddDisplayStatList(self.displayStats, self.calcsTab.mainEnv.player) + self:AddDisplayStatList(self.displayStats, self.calcsTab.mainEnv.player, "player") self:InsertItemWarnings() self:EstimatePlayerProgress() end diff --git a/src/Modules/BuildDisplayStats.lua b/src/Modules/BuildDisplayStats.lua index 3bdfe08f8b..ad156644df 100644 --- a/src/Modules/BuildDisplayStats.lua +++ b/src/Modules/BuildDisplayStats.lua @@ -6,10 +6,46 @@ -- This defines the stats in the side bar, and also which stats show in node/item comparisons -- This may be user-customisable in the future +---@param value number +---@param output table Actor output values +---@return "Immune"|"Guard"|nil suffix local function maxHitSuffix(value, output) - return value == math.huge and "Immune" or output.GuardSkillActive and "Guard" + return value == math.huge and "Immune" or output.GuardSkillActive and "Guard" or nil end +---@alias DisplayStatCondFunc fun(value: any, output: table):boolean? Show the row only when this returns truthy (usually receives the value and the actor output; the label-only variant receives output as its first arg) +---@alias DisplayStatWarnFunc fun(value: any, output: table):string|boolean|nil Returns a warning string to surface, or falsy for none + +---@class DisplayStat One sidebar stat row (also drives node/item comparisons). An empty `{}` is a separator. +---@field stat? string Output value name, e.g. "Life", "TotalDPS", "MainHand" +---@field childStat? string Nested output field when `stat` is a table, e.g. "Accuracy" for MainHand.Accuracy +---@field label? string Display label shown before the value +---@field fmt? string C-style format spec applied to the value, e.g. "d", ".1f", ".2f%%", "+d%%" +---@field breakdown? string Override key for the sidebar breakdown. Should be set if the stat/childStat names don't match up with the breakdown names, or if the breakdown is conditional, but the cell isn't. +---@field modNames string[]? Modifier names to show in the breakdown popup. This should only be used if the breakdown itself doesn't list the mods. E.g. Life doesn't. +---@field ignoredSections string[]? A list of breakdown sections which shouldn't be shown. Useful to avoid duplicated info. +---@field color? string Colour escape code for the label (e.g. colorCodes.LIFE) +---@field flag? string Only show when the main skill has this skill flag +---@field notFlag? string Hide when the main skill has this skill flag +---@field condFunc? DisplayStatCondFunc Extra condition gating whether the row is shown +---@field warnFunc? DisplayStatWarnFunc Produces a warning message for the warnings panel +---@field warnColor? boolean Colour the value red when `warnFunc` returns truthy +---@field suffix? string|fun(value:any, output:table):string|nil Parenthesised suffix appended after the value +---@field suffixCondFunc? fun(value:any, output:table):boolean? Gates whether `suffix` is shown +---@field overCapStat? string Output name of the "over cap" counterpart shown alongside (e.g. resistances) +---@field overCapStatCondFunc? fun(value:any, output:table):boolean? Gates whether the over-cap value is shown +---@field compactValue? boolean Allow compact formatting (e.g. 712.3K) when the user setting is on +---@field compPercent? boolean Include a percentage delta in node/item comparisons +---@field lowerIsBetter? boolean Treat a decrease as an improvement in comparisons +---@field pc? boolean Multiply the value by 100 (fractional value shown as a percentage) +---@field mod? boolean Value is a modifier: scale by 100 and subtract 100 (shows the +/- portion) +---@field hideStat? boolean Compute/consider the stat but don't render a sidebar row +---@field chaosInoc? boolean Use the muted "immune" colour instead of red for a zero/negative value +---@field pool? string Associated resource pool name (e.g. "ManaUnreserved") for cost rows +---@field labelStat? string Output name rendered inline for a label-only row +---@field val? string Static parenthesised note for a label-only row + +---@type DisplayStat[] local displayStats = { { stat = "ActiveMinionLimit", label = "Active Minion Limit", fmt = "d" }, { stat = "AverageHit", label = "Average Hit", fmt = ".1f", compactValue = true, compPercent = true }, @@ -18,8 +54,9 @@ local displayStats = { { stat = "AverageDamage", label = "Average Damage", fmt = ".1f", compactValue = true, compPercent = true, flag = "monsterExplode", condFunc = function(v,o) return o.HitChance ~= 100 end }, { stat = "AverageBurstDamage", label = "Average Burst Damage", fmt = ".1f", compactValue = true, compPercent = true, condFunc = function(v,o) return o.AverageBurstHits and o.AverageBurstHits > 1 and v > 0 end }, { stat = "PvpAverageDamage", label = "PvP Average Damage", fmt = ".1f", compactValue = true, compPercent = true, flag = "attackPvP" }, - { stat = "Speed", label = "Attack Rate", fmt = ".2f", compPercent = true, flag = "attack", condFunc = function(v,o) return v > 0 and (o.TriggerTime or 0) == 0 end }, - { stat = "Speed", label = "Cast Rate", fmt = ".2f", compPercent = true, flag = "spell", condFunc = function(v,o) return v > 0 and (o.TriggerTime or 0) == 0 end }, + { stat = "Speed", label = "Attack Rate", fmt = ".2f", compPercent = true, flag = "attack", condFunc = function(v, o) return v > 0 and (o.TriggerTime or 0) == 0 end, breakdown = "MainHand.Speed", modNames = { "Speed" }, ignoredSections = { "Inc. Cast Speed", "More Cast Speed", } }, + { stat = "KineticFusilladeMaxEffectiveAPS", label = "Max Eff. KF Attack Rate", fmt = ".2f", compPercent = true, flag = "attack", condFunc = function(v) return v > 0 end }, + { stat = "Speed", label = "Cast Rate", fmt = ".2f", compPercent = true, flag = "spell", condFunc = function(v, o) return v > 0 and (o.TriggerTime or 0) == 0 end, modNames = { "Speed" } }, { stat = "Speed", label = "Effective Trigger Rate", fmt = ".2f", compPercent = true, notFlag = "skipEffectiveRate", condFunc = function(v,o) return (o.TriggerTime or 0) ~= 0 end }, { stat = "WarcryCastTime", label = "Cast Time", fmt = ".2fs", compPercent = true, lowerIsBetter = true, flag = "warcry" }, { stat = "HitSpeed", label = "Hit Rate", fmt = ".2f", compPercent = true, condFunc = function(v,o) return not o.TriggerTime end }, @@ -30,11 +67,12 @@ local displayStats = { { stat = "MineLayingTime", label = "Mine Throwing Time", fmt = ".2fs", compPercent = true, lowerIsBetter = true, }, { stat = "TrapThrowCount", label = "Avg. Traps per Throw", fmt = ".2f"}, { stat = "MineThrowCount", label = "Avg. Mines per Throw", fmt = ".2f"}, - { stat = "TotemPlacementTime", label = "Totem Placement Time", fmt = ".2fs", compPercent = true, lowerIsBetter = true, condFunc = function(v,o) return not o.TriggerTime end }, - { stat = "PreEffectiveCritChance", label = "Crit Chance", fmt = ".2f%%", flag = "hit" }, - { stat = "CritChance", label = "Effective Crit Chance", fmt = ".2f%%", flag = "hit", condFunc = function(v,o) return v ~= o.PreEffectiveCritChance end }, - { stat = "CritMultiplier", label = "Crit Multiplier", fmt = "d%%", pc = true, condFunc = function(v,o) return (o.CritChance or 0) > 0 end }, - { stat = "HitChance", label = "Hit Chance", fmt = ".0f%%", flag = "attack" }, + { stat = "TotemPlacementTime", label = "Totem Placement Time", fmt = ".2fs", compPercent = true, lowerIsBetter = true, condFunc = function(v, o) return not o.TriggerTime end }, + { stat = "PreEffectiveCritChance", label = "Crit Chance", fmt = ".2f%%", flag = "hit", breakdown = "CritChance" }, + { stat = "CritChance", label = "Effective Crit Chance", fmt = ".2f%%", flag = "hit", notFlag = "spell", condFunc = function(v, o) return v ~= o.PreEffectiveCritChance end, breakdown = "MainHand.CritChance" }, + { stat = "CritChance", label = "Effective Crit Chance", fmt = ".2f%%", flag = "hit", notFlag = "attack", condFunc = function(v, o) return v ~= o.PreEffectiveCritChance end, breakdown = "CritChance" }, + { stat = "CritMultiplier", label = "Crit Multiplier", fmt = "d%%", pc = true, condFunc = function(v, o) return (o.CritChance or 0) > 0 end, breakdown = "CritMultiplier" }, + { stat = "HitChance", label = "Hit Chance", fmt = ".0f%%", flag = "attack", breakdown = "MainHand.AccuracyHitChance" }, { stat = "HitChance", label = "Hit Chance", fmt = ".0f%%", condFunc = function(v,o) return o.enemyHasSpellBlock end }, { stat = "TotalDPS", label = "Hit DPS", fmt = ".1f", compactValue = true, compPercent = true, flag = "notAverage" }, { stat = "PvpTotalDPS", label = "PvP Hit DPS", fmt = ".1f", compactValue = true, compPercent = true, flag = "notAveragePvP" }, @@ -74,7 +112,7 @@ local displayStats = { { stat = "SealCooldown", label = "Seal Gain Frequency", fmt = ".2fs", lowerIsBetter = true }, { stat = "SealMax", label = "Max Number of Seals", fmt = "d" }, { stat = "TimeMaxSeals", label = "Time to Gain Max Seals", fmt = ".2fs", lowerIsBetter = true }, - { stat = "AreaOfEffectRadiusMetres", label = "AoE Radius", fmt = ".1fm" }, + { stat = "AreaOfEffectRadiusMetres", label = "AoE Radius", fmt = ".1fm", breakdown = "AreaOfEffectMod" }, { stat = "BrandAttachmentRangeMetre", label = "Attachment Range", fmt = ".1fm", flag = "brand" }, { stat = "BrandTicks", label = "Activations per Brand", fmt = "d", flag = "brand" }, { stat = "ManaCost", label = "Mana Cost", fmt = "d", color = colorCodes.MANA, pool = "ManaUnreserved", compPercent = true, lowerIsBetter = true, condFunc = function(v,o) return o.ManaHasCost end }, @@ -92,11 +130,11 @@ local displayStats = { { stat = "RagePerSecondCost", label = "Rage Cost per second", fmt = ".2f", color = colorCodes.RAGE, pool = "Rage", compPercent = true, lowerIsBetter = true, condFunc = function(v,o) return o.RagePerSecondHasCost end }, { stat = "SoulCost", label = "Soul Cost", fmt = "d", color = colorCodes.RAGE, pool = "Soul", compPercent = true, lowerIsBetter = true, condFunc = function(v,o) return o.SoulHasCost end }, { }, - { stat = "Str", label = "Strength", color = colorCodes.STRENGTH, fmt = "d" }, + { stat = "Str", label = "Strength", color = colorCodes.STRENGTH, fmt = "d", breakdown = "Str" }, { stat = "ReqStr", label = "Strength Required", color = colorCodes.STRENGTH, fmt = "d", lowerIsBetter = true, condFunc = function(v,o) return v > o.Str end, warnFunc = function(v,o) return "You do not meet the Strength requirement of " .. (o.ReqStrItem.source == "Item" and o.ReqStrItem.sourceItem.name or o.ReqStrItem.source == "Gem" and o.ReqStrItem.sourceGem.nameSpec) end }, - { stat = "Dex", label = "Dexterity", color = colorCodes.DEXTERITY, fmt = "d" }, + { stat = "Dex", label = "Dexterity", color = colorCodes.DEXTERITY, fmt = "d", breakdown = "Dex" }, { stat = "ReqDex", label = "Dexterity Required", color = colorCodes.DEXTERITY, fmt = "d", lowerIsBetter = true, condFunc = function(v,o) return v > o.Dex end, warnFunc = function(v,o) return "You do not meet the Dexterity requirement of " .. (o.ReqDexItem.source == "Item" and o.ReqDexItem.sourceItem.name or o.ReqDexItem.source == "Gem" and o.ReqDexItem.sourceGem.nameSpec) end }, - { stat = "Int", label = "Intelligence", color = colorCodes.INTELLIGENCE, fmt = "d" }, + { stat = "Int", label = "Intelligence", color = colorCodes.INTELLIGENCE, fmt = "d", breakdown = "Int" }, { stat = "ReqInt", label = "Intelligence Required", color = colorCodes.INTELLIGENCE, fmt = "d", lowerIsBetter = true, condFunc = function(v,o) return v > o.Int end, warnFunc = function(v,o) return "You do not meet the Intelligence requirement of " .. (o.ReqIntItem.source == "Item" and o.ReqIntItem.sourceItem.name or o.ReqIntItem.source == "Gem" and o.ReqIntItem.sourceGem.nameSpec) end }, { stat = "Omni", label = "Omniscience", color = colorCodes.RARE, fmt = "d" }, { stat = "ReqOmni", label = "Omniscience Required", color = colorCodes.RARE, fmt = "d", lowerIsBetter = true, condFunc = function(v,o) return v > (o.Omni or 0) end, warnFunc = function(v,o) return "You do not meet the Omniscience requirement of " .. (o.ReqOmniItem.source == "Item" and o.ReqOmniItem.sourceItem.name or o.ReqOmniItem.source == "Gem" and o.ReqOmniItem.sourceGem.nameSpec) end }, @@ -114,27 +152,29 @@ local displayStats = { { }, { stat = "MainHand", childStat = "Accuracy", label = "MH Accuracy", fmt = "d", condFunc = function(v,o) return o.PreciseTechnique end, warnFunc = function(v,o) return v < o.Life and "You do not have enough Accuracy for Precise Technique" end, warnColor = true }, { stat = "OffHand", childStat = "Accuracy", label = "OH Accuracy", fmt = "d", condFunc = function(v,o) return o.PreciseTechnique end, warnFunc = function(v,o) return v < o.Life and "You do not have enough Accuracy for Precise Technique" end, warnColor = true }, - { stat = "Life", label = "Total Life", fmt = "d", compactValue = true, color = colorCodes.LIFE, compPercent = true }, + { stat = "Life", label = "Total Life", fmt = "d", compactValue = true, color = colorCodes.LIFE, compPercent = true, modNames = { "Life" }, ignoredSections = { "Base from Gear", "Inc. from Tree" } }, { stat = "Spec:LifeInc", label = "%Inc Life from Tree", fmt = "d%%", color = colorCodes.LIFE, condFunc = function(v,o) return v > 0 and o.Life > 1 end }, { stat = "LifeUnreserved", label = "Unreserved Life", fmt = "d", color = colorCodes.LIFE, condFunc = function(v,o) return v < o.Life end, compPercent = true, warnFunc = function(v) return v <= 0 and "Your unreserved Life is below 1" end }, { stat = "LifeRecoverable", label = "Life Recoverable", fmt = "d", color = colorCodes.LIFE, condFunc = function(v,o) return v < o.LifeUnreserved end, }, { stat = "LifeUnreservedPercent", label = "Unreserved Life", fmt = "d%%", color = colorCodes.LIFE, condFunc = function(v,o) return v < 100 end }, - { stat = "LifeRegenRecovery", label = "Life Regen", fmt = ".1f", color = colorCodes.LIFE, condFunc = function(v,o) return o.LifeRecovery <= 0 and o.LifeRegenRecovery ~= 0 end }, - { stat = "LifeRegenRecovery", label = "Life Recovery", fmt = ".1f", color = colorCodes.LIFE, condFunc = function(v,o) return o.LifeRecovery > 0 and o.LifeRegenRecovery ~= 0 end }, + { stat = "LifeRegenRecovery", label = "Life Regen", fmt = ".1f", color = colorCodes.LIFE, condFunc = function(v, o) return o.LifeRecovery <= 0 and o.LifeRegenRecovery ~= 0 end, breakdown = "LifeRegenRecovery" }, + { stat = "LifeRegenRecovery", label = "Life Recovery", fmt = ".1f", color = colorCodes.LIFE, condFunc = function(v, o) return o.LifeRecovery > 0 and o.LifeRegenRecovery ~= 0 end, breakdown = "LifeRegenRecovery" }, { stat = "LifeRecharge", label = "Life Recharge", fmt = ".1f", color = colorCodes.LIFE, condFunc = function(v,o) return v > 0 end }, - { stat = "LifeLeechGainRate", label = "Life Leech/On Hit Rate", fmt = ".1f", color = colorCodes.LIFE, compPercent = true }, + { stat = "LifeLeechGainRate", label = "Life Leech/On Hit Rate", fmt = ".1f", color = colorCodes.LIFE, compPercent = true, breakdown = "LifeLeech" }, { stat = "LifeLeechGainPerHit", label = "Life Leech/Gain per Hit", fmt = ".1f", color = colorCodes.LIFE, compPercent = true }, { }, - { stat = "Mana", label = "Total Mana", fmt = "d", compactValue = true, color = colorCodes.MANA, compPercent = true }, + { stat = "Mana", label = "Total Mana", fmt = "d", compactValue = true, color = colorCodes.MANA, compPercent = true, modNames = { "Mana" }, ignoredSections = { "Base from Gear", "Inc. from Tree" } }, { stat = "Spec:ManaInc", label = "%Inc Mana from Tree", color = colorCodes.MANA, fmt = "d%%" }, { stat = "ManaUnreserved", label = "Unreserved Mana", fmt = "d", color = colorCodes.MANA, condFunc = function(v,o) return v < o.Mana end, compPercent = true, warnFunc = function(v) return v < 0 and "Your unreserved Mana is negative" end }, - { stat = "ManaUnreservedPercent", label = "Unreserved Mana", fmt = "d%%", color = colorCodes.MANA, condFunc = function(v,o) return v < 100 end }, + { stat = "ManaUnreservedPercent", label = "Unreserved Mana", fmt = "d%%", color = colorCodes.MANA, condFunc = function(v, o) return v < 100 end, breakdown = "ManaReserved" }, { stat = "ManaRegenRecovery", label = "Mana Regen", fmt = ".1f", color = colorCodes.MANA, condFunc = function(v,o) return o.ManaRecovery <= 0 and o.ManaRegenRecovery ~= 0 end }, { stat = "ManaRegenRecovery", label = "Mana Recovery", fmt = ".1f", color = colorCodes.MANA, condFunc = function(v,o) return o.ManaRecovery > 0 and o.ManaRegenRecovery ~= 0 end }, { stat = "ManaLeechGainRate", label = "Mana Leech/On Hit Rate", fmt = ".1f", color = colorCodes.MANA, compPercent = true }, { stat = "ManaLeechGainPerHit", label = "Mana Leech/Gain per Hit", fmt = ".1f", color = colorCodes.MANA, compPercent = true }, { }, - { stat = "EnergyShield", label = "Energy Shield", fmt = "d", compactValue = true, color = colorCodes.ES, compPercent = true }, + -- the breakdown for armour, es and ev doesn't have anything relevant, and + -- we are better off ignoring it and just showing relevant mods + { stat = "EnergyShield", label = "Energy Shield", fmt = "d", compactValue = true, color = colorCodes.ES, compPercent = true, breakdown = "", modNames = { "EnergyShield", "Defences" } }, { stat = "EnergyShieldRecoveryCap", label = "Recoverable ES", color = colorCodes.ES, fmt = "d", condFunc = function(v,o) return o.CappingES end }, { stat = "Spec:EnergyShieldInc", label = "%Inc ES from Tree", color = colorCodes.ES, fmt = "d%%" }, { stat = "EnergyShieldRegenRecovery", label = "ES Regen", color = colorCodes.ES, fmt = ".1f", condFunc = function(v,o) return o.EnergyShieldRecovery <= 0 and o.EnergyShieldRegenRecovery ~= 0 end }, @@ -153,21 +193,21 @@ local displayStats = { { stat = "NetManaRegen", label = "Net Mana Recovery", fmt = "+.1f", color = colorCodes.MANA }, { stat = "NetEnergyShieldRegen", label = "Net ES Recovery", fmt = "+.1f", color = colorCodes.ES }, { }, - { stat = "Evasion", label = "Evasion rating", fmt = "d", color = colorCodes.EVASION, compPercent = true }, + { stat = "Evasion", label = "Evasion rating", fmt = "d", color = colorCodes.EVASION, compPercent = true, breakdown = "", modNames = { "Evasion", "ArmourAndEvasion", "Defences" } }, { stat = "Spec:EvasionInc", label = "%Inc Evasion from Tree", color = colorCodes.EVASION, fmt = "d%%" }, { stat = "MeleeEvadeChance", label = "Evade Chance", fmt = "d%%", color = colorCodes.EVASION, condFunc = function(v,o) return v > 0 and o.MeleeEvadeChance == o.ProjectileEvadeChance end }, { stat = "MeleeEvadeChance", label = "Melee Evade Chance", fmt = "d%%", color = colorCodes.EVASION, condFunc = function(v,o) return v > 0 and o.MeleeEvadeChance ~= o.ProjectileEvadeChance end }, { stat = "ProjectileEvadeChance", label = "Projectile Evade Chance", fmt = "d%%", color = colorCodes.EVASION, condFunc = function(v,o) return v > 0 and o.MeleeEvadeChance ~= o.ProjectileEvadeChance end }, { }, - { stat = "Armour", label = "Armour", fmt = "d", compPercent = true }, + { stat = "Armour", label = "Armour", fmt = "d", compPercent = true, breakdown = "", modNames = { "Armour", "ArmourAndEvasion", "Defences" } }, { stat = "Spec:ArmourInc", label = "%Inc Armour from Tree", fmt = "d%%" }, { stat = "PhysicalDamageReduction", label = "Phys. Damage Reduction", fmt = "d%%", condFunc = function() return true end }, { }, - { stat = "EffectiveBlockChance", label = "Block Chance", fmt = ".3f%%", overCapStat = "BlockChanceOverCap" }, - { stat = "EffectiveSpellBlockChance", label = "Spell Block Chance", fmt = ".3f%%", overCapStat = "SpellBlockChanceOverCap" }, + { stat = "EffectiveBlockChance", label = "Block Chance", fmt = ".3f%%", overCapStat = "BlockChanceOverCap", breakdown = "BlockChance" }, + { stat = "EffectiveSpellBlockChance", label = "Spell Block Chance", fmt = ".3f%%", overCapStat = "SpellBlockChanceOverCap", breakdown = "SpellBlockChance" }, { stat = "AttackDodgeChance", label = "Attack Dodge Chance", fmt = "d%%", overCapStat = "AttackDodgeChanceOverCap" }, { stat = "SpellDodgeChance", label = "Spell Dodge Chance", fmt = "d%%", overCapStat = "SpellDodgeChanceOverCap" }, - { stat = "EffectiveSpellSuppressionChance", label = "Spell Suppression Chance", fmt = ".2f%%", overCapStat = "SpellSuppressionChanceOverCap" }, + { stat = "EffectiveSpellSuppressionChance", label = "Spell Suppression Chance", fmt = ".2f%%", overCapStat = "SpellSuppressionChanceOverCap", breakdown = "", modNames = { "SpellSuppressionChance" } }, { }, { stat = "FireResist", label = "Fire Resistance", fmt = "d%%", color = colorCodes.FIRE, condFunc = function() return true end, overCapStat = "FireResistOverCap"}, { stat = "FireResistOverCap", label = "Fire Res. Over Max", fmt = "d%%", hideStat = true }, @@ -206,6 +246,7 @@ local displayStats = { { }, { stat = "SkillDPS", label = "Skill DPS", compactValue = true, condFunc = function() return true end }, } +---@type DisplayStat[] local minionDisplayStats = { { stat = "AverageDamage", label = "Average Damage", fmt = ".1f", compactValue = true, compPercent = true }, { stat = "Speed", label = "Attack/Cast Rate", fmt = ".2f", compPercent = true, condFunc = function(v,o) return v > 0 and (o.TriggerTime or 0) == 0 end }, @@ -229,7 +270,7 @@ local minionDisplayStats = { { stat = "ReservationDPS", label = "Reservation DPS", fmt = ".1f", compactValue = true, compPercent = true, condFunc = function(v,o) return (o.ReservationDPS or 0) > 0 end }, { stat = "CombinedDPS", label = "Combined DPS", fmt = ".1f", compactValue = true, compPercent = true, condFunc = function(v,o) return v ~= ((o.TotalDPS or 0) + (o.TotalDot or 0)) and v ~= o.WithImpaleDPS and v ~= o.WithPoisonDPS and v ~= o.WithIgniteDPS and v ~= o.WithBleedDPS end}, { stat = "Cooldown", label = "Skill Cooldown", fmt = ".3fs", notFlag = "skipEffectiveRate", lowerIsBetter = true }, - { stat = "Life", label = "Total Life", fmt = ".1f", compactValue = true, color = colorCodes.LIFE, compPercent = true }, + { stat = "Life", label = "Total Life", fmt = ".1f", compactValue = true, color = colorCodes.LIFE, compPercent = true, modNames = { "Life" }, ignoredSections = { "Base from Gear", "Inc. from Tree" } }, { stat = "LifeRegenRecovery", label = "Life Recovery", fmt = ".1f", color = colorCodes.LIFE }, { stat = "LifeLeechGainRate", label = "Life Leech/On Hit Rate", fmt = ".1f", color = colorCodes.LIFE, compPercent = true }, { stat = "EnergyShield", label = "Energy Shield", fmt = "d", compactValue = true, color = colorCodes.ES, compPercent = true }, @@ -237,6 +278,7 @@ local minionDisplayStats = { { stat = "EnergyShieldLeechGainRate", label = "ES Leech/On Hit Rate", fmt = ".1f", color = colorCodes.ES, compPercent = true }, } -- Extra stats saved to the xml if not already saved there, mostly for 3rd party tools +---@type string[] local extraSaveStats = { "PowerCharges", "PowerChargesMax", diff --git a/src/Modules/CalcPerform.lua b/src/Modules/CalcPerform.lua index 65b8a15b3e..0a39189049 100644 --- a/src/Modules/CalcPerform.lua +++ b/src/Modules/CalcPerform.lua @@ -1477,7 +1477,7 @@ function calcs.perform(env, skipEHP) output.LootRarityMagicEnemies = (lootRarityMagicEnemies > 0) and lootRarityMagicEnemies + modDB:Sum("INC", nil, "LootRarity") or 0 local breakdown = nil - if env.mode == "CALCS" then + if env.buildBreakdown then -- Initialise breakdown module breakdown = LoadModule(calcs.breakdownModule, modDB, output, env.player) env.player.breakdown = breakdown diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 599b5974c7..1ad632a28d 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -53,7 +53,53 @@ local rageCost = { "RageCost", "Cost", "RageCostNoMult", "RageCostEfficiency", "CostEfficiency" } --- format {width, id, group, color, subsection:{default hidden, label, data:{}}} +---@class CalcSectionEntry A single description which is in the shown breakdown popup +---@field breakdown? string Key indicating which output breakdown should be displayed +---@field modName? string|string[] +---@field modType? "BASE"|"INC"|"MORE"|"OVERRIDE" +---@field cfg? string Config source +---@field enemy? boolean Whether the enemy modDB should be used +---@field actor? string Which actor should be used +---@field modSource? string Restrict mods to e.g. items +---@field label? string Heading shown above the resulting table +---@field control? table Injected by calcs for e.g. the skill details +---@field controlName? string + +---@class CalcSectionColumn : CalcSectionEntry One column cell: a format and descriptions for the popup +---@field [integer] CalcSectionEntry +---@field format? string Value format string (e.g. "{0:output:Life}") or literal header text + +---@class CalcSectionRow One row of a subsection +---@field [integer] CalcSectionColumn Column cells +---@field label? string Row label +---@field haveOutput? string Only show the row when this output value is non-zero +---@field flag? string Only show when the skill has this flag +---@field flagList? string[] Only show when the skill has all of these flags +---@field playerFlag? string Only show when the player skill has this flag +---@field notFlag? string Hide when the skill has this flag +---@field notFlagList? string[] Hide when the skill has any of these flags + +---@class CalcSectionData A subsection's rows plus subsection configuration values +---@field [integer] CalcSectionRow The rows +---@field extra? string Summary text shown beside the header (e.g. "{0:output:Life}") +---@field colWidth? integer Fixed width for value columns in this subsection +---@field flag? string Applied to the whole subsection (via subSection[1].data.flag) +---@field notFlag? string + +---@class CalcSectionSubsection +---@field defaultCollapsed boolean +---@field label string +---@field data CalcSectionData + +---@class CalcSection +---@field [1] integer Width in columns +---@field [2] string Section id +---@field [3] integer Group +---@field [4] string Colour escape code +---@field [5] CalcSectionSubsection[] Subsections +---@field [6] fun()? Optional update function + +---@type CalcSection[] return { { 3, "HitDamage", 1, colorCodes.OFFENCE, {{ defaultCollapsed = false, label = "Skill Hit Damage", data = { extra = "{output:DisplayDamage}", @@ -658,8 +704,8 @@ return { { label = "OH DMG Mod.", bgCol = colorCodes.OFFHANDBG, flag = "weapon2Attack", haveOutput = "OffHand.ImpaleModifier", { format = "{3:output:OffHand.ImpaleModifier}", modType = "MORE", { breakdown = "OffHand.ImpaleModifier" }, }, }, - { label = "Impale DPS", flag = "impale", flag = "notAverage", { format = "{1:output:ImpaleDPS}", { breakdown = "ImpaleDPS" }, }, }, - { label = "Impale Damage", flag = "impale", flag = "showAverage", { format = "{1:output:ImpaleDPS}", { breakdown = "ImpaleDPS" }, }, }, + { label = "Impale DPS", flag = "notAverage", { format = "{1:output:ImpaleDPS}", { breakdown = "ImpaleDPS" }, }, }, + { label = "Impale Damage", flag = "showAverage", { format = "{1:output:ImpaleDPS}", { breakdown = "ImpaleDPS" }, }, }, } } } }, { 1, "SkillTypeStats", 1, colorCodes.OFFENCE, {{ defaultCollapsed = false, label = "Skill type-specific Stats", data = { diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 4c7561e616..6c3bcb617b 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -396,6 +396,7 @@ function calcs.initEnv(build, mode, override, specEnv) env.configPlaceholder = build.configTab.placeholder env.calcsInput = build.calcsTab.input env.mode = mode + env.buildBreakdown = mode == "MAIN" or mode == "CALCS" env.spec = override.spec or build.spec env.override = override env.classId = env.spec.curClassId diff --git a/src/Modules/Calcs.lua b/src/Modules/Calcs.lua index 53c162160d..e2001f65c4 100644 --- a/src/Modules/Calcs.lua +++ b/src/Modules/Calcs.lua @@ -390,6 +390,7 @@ end -- Process active skill function calcs.buildActiveSkill(env, mode, skill, targetUUID, limitedProcessingFlags) local fullEnv, _, _, _ = calcs.initEnv(env.build, mode, env.override) + fullEnv.buildBreakdown = false -- env.limitedSkills contains a map of uuids that should be limited in calculation -- this is in order to prevent infinite recursion loops