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
187 changes: 186 additions & 1 deletion src/Classes/PassiveTreeView.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,154 @@ local b_rshift = bit.rshift
local unseenPathHover = false

local gemTooltip = LoadModule("Classes/GemTooltip")

-- GGG tags keyword references inside popup text as [Id] or [Id|Display]. Tint the ones
-- that map onto a colour PoB already defines, and leave the rest in the body colour.
local keywordBodyColor = "^xA0A080"
local keywordColors = {
Fire = colorCodes.FIRE,
Ignite = colorCodes.FIRE,
Burning = colorCodes.FIRE,
Flammability = colorCodes.FIRE,
Cold = colorCodes.COLD,
Freeze = colorCodes.COLD,
Frozen = colorCodes.COLD,
Chill = colorCodes.COLD,
Lightning = colorCodes.LIGHTNING,
Shock = colorCodes.LIGHTNING,
Chaos = colorCodes.CHAOS,
Strength = colorCodes.STRENGTH,
Dexterity = colorCodes.DEXTERITY,
Intelligence = colorCodes.INTELLIGENCE,
Spirit = colorCodes.SPIRIT,
}

-- Strips GGG's keyword markup, colouring the references PoB has a colour for
local function colorKeywordText(text)
-- GGG also wraps text in font and colour markup, <tag>{text}, which can nest
-- unwrap innermost first, since these nest: <font>{<italic>{<rgb>{text}}}
local prev
repeat
prev = text
text = text:gsub("<[^<>]+>{([^{}]*)}", "%1")
until text == prev
-- then drop any tag left without braces, including the <> from <<Name>>
repeat
prev = text
text = text:gsub("<[^<>]*>", "")
until text == prev
return (text:gsub("%[([^|%]]+)|([^%]]+)%]", function(id, display)
local color = keywordColors[id]
return color and (color .. display .. keywordBodyColor) or display
end):gsub("%[([^|%]]+)%]", function(id)
local color = keywordColors[id]
return color and (color .. id .. keywordBodyColor) or id
end))
end

-- Every GGG keyword popup, indexed by display name. Built on first use, as data is not
-- loaded at module load. Sorted longest first so "Energy Shield Recharge" is matched
-- before "Energy Shield".
local keywordList, keywordByName
local function getKeywords()
if not keywordList then
keywordByName = { }
for id, popup in pairs(data.keywordPopups) do
if popup.name and popup.name ~= "" and popup.description and popup.description ~= "" then
local cur = keywordByName[popup.name]
if not cur or id < cur.id then
keywordByName[popup.name] = { id = id, name = popup.name, description = popup.description }
end
end
end
keywordList = { }
for _, popup in pairs(keywordByName) do
t_insert(keywordList, popup)
end
table.sort(keywordList, function(a, b)
if #a.name ~= #b.name then
return #a.name > #b.name
end
return a.name < b.name
end)
end
return keywordList, keywordByName
end

-- Keywords not worth explaining on a passive node. They sometimes conflict
-- with in-game content rather than passive tree nodes.
local deniedKeywords = { }
for _, name in ipairs({
"Power", -- Trial of Chaos rooms, matches "Power Charge" nodes
"Empowerment", -- map modifier text
"Recently", -- past 4 seconds
"Maximum", -- explains modifier ranges, not a tree mechanic
"Equipment", -- "Equipment are items that can be Equipped"
"Equipped", -- restates that weapons and armour are worn, on 45 nodes
"Possessed", -- Azmeri spirit possession, matches "Tame Beast" nodes
-- Self-explanatory
"Item Armour", "Melee", "Projectile", "Rage", "Spells", "Rune", "Attacks", "Minions", "Resistances", "Totems", "Cold Damage", "Fire Damage", "Lightning Damage", "Chaos Damage", "Physical Damage", "Offering Skills",
-- Monster modifiers. These are map mods and describe the enemy, not your character
"Extra Fire Damage", "Extra Cold Damage", "Extra Lightning Damage", "Extra Chaos Damage",
-- Weapon and off-hand types. The popup just restates what the base item is
"Quarterstaves", "Crossbows", "Spears", "Bows", "Maces", "Wands", "Staves", "Sceptres",
"Daggers", "Claws", "Swords", "Axes", "Flails", "Bucklers", "Shields", "Foci",
"Two-Handed",
}) do
deniedKeywords[name] = true
end

-- Splits the keywords a node mentions into two tiers.
-- "granted" is for stat lines that are nothing but a keyword ("Inevitable Critical Hits",
-- "Grants Unravelling"). The node exists to give you that mechanic, so it is always explained.
-- "mentioned" is for keywords inside a longer line. Common ones like Armour turn up on
-- hundreds of nodes, so those are opt-in and only listed when the option is enabled.
local function findKeywords(lines)
local granted, mentioned, seen = { }, { }, { }
local list, byName = getKeywords()
for _, text in ipairs(lines) do
local whole = byName[text] or byName[text:match("^Grants (.+)$") or ""]
if whole then
if not seen[whole.name] then
seen[whole.name] = true
t_insert(granted, whole)
end
elseif main.showKeywordTooltips and not text:find("^Grants Skill: ") then
-- everything after that colon is a skill's proper name, so a keyword found in it
-- is a coincidence ("Time Freeze", "Into the Breach"). The granted skill already
-- gets its own tooltip in the side panel.
local taken = { }
for _, popup in ipairs(list) do
local init = 1
while true do
local s, e = text:find(popup.name, init, true)
if not s then
break
end
-- skip matches inside a longer word ("Life" in "Lifetap") and inside an
-- already claimed keyword ("Shield" within "Energy Shield")
if not taken[s] and not taken[e]
and (s == 1 or not text:sub(s - 1, s - 1):match("%w"))
and (e == #text or not text:sub(e + 1, e + 1):match("%w")) then
for i = s, e do
taken[i] = true
end
if not seen[popup.name] then
seen[popup.name] = true
if not deniedKeywords[popup.name] then
t_insert(mentioned, popup)
end
end
break
end
init = e + 1
end
end
end
end
return granted, mentioned
end

local JEWEL_RADIUS_TINT_NEUTRAL = { 1, 1, 1, 0.7 }
local JEWEL_RADIUS_TINT_PRIMARY_ONLY = { 1, 0, 0, 0.7 }
local JEWEL_RADIUS_TINT_COMPARE_ONLY = { 0, 1, 0, 0.7 }
Expand Down Expand Up @@ -1217,7 +1365,7 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents)
-- Draw tooltip
SetDrawLayer(nil, 100)
local size = m_floor(node.size * scale)
if self.tooltip:CheckForUpdate(node, self.showStatDifferences, self.tracePath, launch.devModeAlt, build.outputRevision, build.spec.allocMode) then
if self.tooltip:CheckForUpdate(node, self.showStatDifferences, self.tracePath, launch.devModeAlt, build.outputRevision, build.spec.allocMode, IsKeyDown("ALT"), main.showKeywordTooltips) then
self:AddNodeTooltip(self.tooltip, node, build, incSmallPassiveSkillEffect)
end
self.tooltip.center = true
Expand Down Expand Up @@ -1890,9 +2038,34 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build, incSmallPassi
if not (mNode.isAttribute and not mNode.conqueredBy) and (mNode.type == "Normal" or mNode.type == "Notable") and isNodeInARadius(node) then
localIncEffect = processTimeLostModsAndGetLocalEffect(mNode, build)
end
-- Underline the keywords in the stat lines, as the game does, so it is obvious
-- which words have an explanation attached
local granted, mentioned = findKeywords(mNode.sd)
tooltip.underlineWords = nil
for _, popup in ipairs(granted) do
tooltip.underlineWords = tooltip.underlineWords or { }
tooltip.underlineWords[popup.name] = true
end
for _, popup in ipairs(mentioned) do
tooltip.underlineWords = tooltip.underlineWords or { }
tooltip.underlineWords[popup.name] = true
end
for i, line in ipairs(mNode.sd) do
addModInfoToTooltip(mNode, i, line, localIncEffect)
end
tooltip.underlineWords = nil

-- A node whose stat line is nothing but a keyword is explained inline: there is only
-- ever one, and it is the whole point of the node
for _, popup in ipairs(granted) do
tooltip:AddSeparator(10)
tooltip:AddLine(14, colorCodes.MAGIC .. popup.name)
tooltip:AddLine(14, keywordBodyColor .. colorKeywordText(popup.description:gsub("\r", "")))
end
if mentioned[1] and not IsKeyDown("ALT") then
tooltip:AddSeparator(10)
tooltip:AddLine(14, colorCodes.TIP .. "Tip: Hold Alt to explain the keywords in this node")
end
-- add child tooltip for skills
self.skillTooltip:Clear()
self.skillTooltip.maxWidth = 600
Expand All @@ -1911,6 +2084,18 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build, incSmallPassi
end
end
end

-- Keywords the node merely mentions go in the side tooltip, since there can be
-- several long ones and they would otherwise move the stat and allocation numbers
if IsKeyDown("ALT") then
for _, popup in ipairs(mentioned) do
if #self.skillTooltip.lines > 0 then
self.skillTooltip:AddSeparator(10)
end
self.skillTooltip:AddLine(14, colorCodes.MAGIC .. popup.name)
self.skillTooltip:AddLine(14, keywordBodyColor .. colorKeywordText(popup.description:gsub("\r", "")))
end
end
end

if node.containJewelSocket and node.alloc then
Expand Down
42 changes: 40 additions & 2 deletions src/Classes/Tooltip.lua
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,31 @@ function TooltipClass:CheckForUpdate(...)
end
end

-- Underline keywords indicating descriptive text
function TooltipClass:GetUnderlineSpans(text)
if not self.underlineWords then
return nil
end
local plain = StripEscapes(text)
local spans
for word in pairs(self.underlineWords) do
local init = 1
while true do
local s, e = plain:find(word, init, true)
if not s then
break
end
if (s == 1 or not plain:sub(s - 1, s - 1):match("%w"))
and (e == #plain or not plain:sub(e + 1, e + 1):match("%w")) then
spans = spans or { }
t_insert(spans, { prefix = plain:sub(1, s - 1), text = word })
end
init = e + 1
end
end
return spans
end

function TooltipClass:AddLine(size, text, font, background, modLine)
if text then
local fontToUse
Expand All @@ -191,10 +216,10 @@ function TooltipClass:AddLine(size, text, font, background, modLine)
end
end
if activeColour then wrappedLine = wrappedLine .. "^7" end
t_insert(self.lines, { size = size, text = wrappedLine, block = #self.blocks, font = fontToUse, center = self.center, background = background, modLine = modLine })
t_insert(self.lines, { size = size, text = wrappedLine, block = #self.blocks, font = fontToUse, center = self.center, background = background, modLine = modLine, underline = self:GetUnderlineSpans(wrappedLine) })
end
else
t_insert(self.lines, { size = size, text = line, block = #self.blocks, font = fontToUse, center = self.center, background = background, modLine = modLine })
t_insert(self.lines, { size = size, text = line, block = #self.blocks, font = fontToUse, center = self.center, background = background, modLine = modLine, underline = self:GetUnderlineSpans(line) })
end
end
end
Expand Down Expand Up @@ -458,6 +483,7 @@ function TooltipClass:CalculateColumns(ttY, ttX, ttH, ttW, viewPort)
local lineAlign = lineCentered and "CENTER_X" or "LEFT"

local stackEntry = {lineX, y, lineAlign, data.size, font, data.text, background = data.background}
stackEntry.underline = data.underline
if data.modLine then
stackEntry.tooltipLine = data
stackEntry.bounds = { x = x + (H_PAD / 2), y = y, width = ttW - H_PAD, height = data.size + 2 }
Expand Down Expand Up @@ -768,6 +794,18 @@ function TooltipClass:Draw(x, y, w, h, viewPort)

-- Draw text line
DrawString(unpack(line))
if line.underline then
local prevR, prevG, prevB, prevA = GetDrawColor()
local textW = DrawStringWidth(line[4], line[5], line[6])
local baseX = line[3] == "CENTER_X" and line[1] - textW / 2 or line[1]
SetDrawColor(0.65, 0.65, 0.65, 0.8)
for _, span in ipairs(line.underline) do
local preW = DrawStringWidth(line[4], line[5], span.prefix)
local wordW = DrawStringWidth(line[4], line[5], span.text)
DrawImage(nil, baseX + preW, line[2] + line[4], wordW, 1)
end
SetDrawColor(prevR, prevG, prevB, prevA)
end
if line.strikethrough then
local prevR, prevG, prevB, prevA = GetDrawColor()
local textW = DrawStringWidth(line[4], line[5], line[6])
Expand Down
Loading
Loading