Skip to content
Open
12 changes: 12 additions & 0 deletions src/Classes/GemSelectControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,18 @@ function GemSelectClass:OnKeyDown(key, doubleClick)
self:ScrollSelIntoView()
end
end
elseif key == "RIGHTBUTTON" and IsKeyDown("SHIFT") then
-- Shift+Right-Click: edit the per-gem author note for the PoE2 .build export.
local gemList = self.skillsTab.displayGroup and self.skillsTab.displayGroup.gemList
local gemInstance = gemList and gemList[self.index]
if gemInstance then
local title = "Note: " .. ((gemInstance.nameSpec and gemInstance.nameSpec ~= "") and gemInstance.nameSpec or "Gem")
main:OpenNoteEditPopup(title, gemInstance.note, function(text)
gemInstance.note = text
self.skillsTab.build.modFlag = true
end)
end
return self
elseif key == "RETURN" or key == "RIGHTBUTTON" then
self.dropped = true
self:UpdateSortCache()
Expand Down
6 changes: 6 additions & 0 deletions src/Classes/GemTooltip.lua
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ function GemTooltip.AddGemTooltip(tooltip, build, gemInstance, options)
tooltip:AddLine(fontSizeBig, colorCodes.UNIQUE .. line, "FONTIN SC ITALIC")
end
end
-- Author note (Shift+Right-Click to set/edit) emitted into the PoE2 .build export.
tooltip:AddSeparator(10)
tooltip:AddLine(14, colorCodes.TIP.."Shift + Right-Click to add a build note (PoE2 .build export)")
if gemInstance.note and gemInstance.note ~= "" then
tooltip:AddLine(14, "^7Note: "..gemInstance.note)
end
end

return GemTooltip
36 changes: 36 additions & 0 deletions src/Classes/ImportTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,42 @@ local ImportTabClass = newClass("ImportTab", "ControlHost", "Control", function(
end
end

-- Path of Exile 2 BuildPlanner export
local BuildExportPoE2 = require("Modules/BuildExportPoE2")
self.controls.sectionPoE2Export = new("SectionControl", {"TOPLEFT",self.controls.sectionBuild,"BOTTOMLEFT",true}, {0, 18, 650, 252}, "Export to in-game build planner")
self.controls.poe2ExportDesc = new("LabelControl", {"TOPLEFT",self.controls.sectionPoE2Export,"TOPLEFT"}, {6, 14, 0, 16}, "^7Save this build as a .build file the in-game build planner can load.")
self.controls.poe2ExportDesc2 = new("LabelControl", {"TOPLEFT",self.controls.poe2ExportDesc,"BOTTOMLEFT"}, {0, 2, 0, 14}, "^8Tree specs, item sets and skill sets are exported as level-bracketed loadouts.")
self.controls.poe2ExportDesc3 = new("LabelControl", {"TOPLEFT",self.controls.poe2ExportDesc2,"BOTTOMLEFT"}, {0, 2, 0, 14}, "^8Edit each set's level range in its Manage popup.")

self.controls.buildPlannerBuildName = new("EditControl", {"TOPLEFT",self.controls.poe2ExportDesc3,"BOTTOMLEFT"}, {0, 8, 200, 20}, self.build.buildName or "New Build", "Build name", nil, nil)
self.controls.buildPlannerAuthorName = new("EditControl", {"LEFT",self.controls.buildPlannerBuildName,"RIGHT"}, {8, 0, 200, 20}, "Author", "Author name", nil, nil)
self.controls.buildPlannerDescLabel = new("LabelControl", {"TOPLEFT",self.controls.buildPlannerBuildName,"BOTTOMLEFT"}, {0, 8, 0, 16}, "^7Description:")
self.controls.buildPlannerDescription = new("EditControl", {"TOPLEFT",self.controls.buildPlannerDescLabel,"BOTTOMLEFT"}, {0, 8, 560, 64}, "", nil, "^%C\t\n", nil, nil, 16)
self.controls.poe2ExportPath = new("EditControl", {"TOPLEFT",self.controls.buildPlannerDescription,"BOTTOMLEFT"}, {0, 8, 408, 20}, main.userPath .. "../My Games/Path of Exile 2/BuildPlanner/" .. self.build.buildName .. ".build", "Path", nil, 260)
self.controls.poe2ExportSave = new("ButtonControl", {"TOPLEFT",self.controls.poe2ExportPath,"BOTTOMLEFT"}, {0, 8, 80, 20}, "Save", function()
local function doWrite()
local ok, err = BuildExportPoE2.WriteFile(self.build, self.controls.poe2ExportPath.buf, {
name = self.controls.buildPlannerBuildName.buf,
author = self.controls.buildPlannerAuthorName.buf,
description = self.controls.buildPlannerDescription.buf,
})
if not ok then
main:OpenMessagePopup("Error", "Couldn't save the build file:\n"..err.."\nMake sure the save folder exists and is writable.")
end
end
-- Confirm overwrite if the file already exists.
local existing = io.open(self.controls.poe2ExportPath.buf, "r")
if existing then
existing:close()
main:OpenConfirmPopup("Overwrite?", "A file already exists at:\n" .. self.controls.poe2ExportPath.buf .. "\n\nOverwrite it?", "Overwrite", doWrite)
else
doWrite()
end
end)
self.controls.poe2ExportSave.enabled = function()
return self.controls.poe2ExportPath.buf and self.controls.poe2ExportPath.buf ~= ""
end

-- validate the status of the api the first time
self:RefreshAuthStatus()
end)
Expand Down
9 changes: 9 additions & 0 deletions src/Classes/ItemSetListControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ function ItemSetListClass:CreateItemSet()
end)
controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function()
self.itemSetService:NewItemSet(controls.edit.buf)
if self.levelRange then
self.levelRange:LoadSet(self.itemsTab.activeItemSet)
end
main:ClosePopup()
end)
controls.save.enabled = false
Expand Down Expand Up @@ -129,6 +132,9 @@ end
function ItemSetListClass:OnSelClick(index, itemSetId, doubleClick)
if doubleClick and itemSetId ~= self.itemsTab.activeItemSetId then
self.itemsTab:SetActiveItemSet(itemSetId)
if self.levelRange then
self.levelRange:LoadSet(self.itemsTab.activeItemSet)
end
self.itemsTab:AddUndoState()
end
end
Expand All @@ -140,6 +146,9 @@ function ItemSetListClass:OnSelDelete(index, itemSetId)
"Are you sure you want to delete '" ..
(itemSet.title or "Default") .. "'?\nThis will not delete any items used by the set.", "Delete", function()
self.itemSetService:DeleteItemSet(itemSetId, index)
if self.levelRange then
self.levelRange:LoadSet(self.itemsTab.activeItemSet)
end
self.selIndex = nil
self.selValue = nil
end)
Expand Down
27 changes: 25 additions & 2 deletions src/Classes/ItemSlotControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ local ItemSlotClass = newClass("ItemSlotControl", "DropDownControl", function(se
self.selItemId = 0
self.slotName = slotName
self.slotNum = tonumber(slotName:match("%d+$") or slotName:match("%d+"))
if data.buildFileInventorySlotMap[slotName] then
self.controls.noteButton = new("ButtonControl", {"LEFT",self,"RIGHT"}, {2, 0, 20, 20}, "~", function()
main:OpenNoteEditPopup(self.slotName, self.note or "", function(note)
self.note = note
itemsTab:PopulateSlots()
itemsTab:AddUndoState()
itemsTab.build.buildFlag = true
end)
end)
self.controls.noteButton.tooltipText = function() return self.note or "Add a note for this item slot" end
end
if slotName:match("Flask") then
self.controls.activate = new("CheckBoxControl", {"RIGHT",self,"LEFT"}, {-2, 0, 20}, nil, function(state)
self.active = state
Expand Down Expand Up @@ -60,7 +71,9 @@ local ItemSlotClass = newClass("ItemSlotControl", "DropDownControl", function(se
self.tooltipFunc = function(tooltip, mode, index, itemId)
local item = itemsTab.items[self.items[index]]
-- not selControl.ListControl allows hover when All Items or Unique/Rare DB Sections are in focus
if main.popups[1] or mode == "OUT" or not item or (not self.dropped and itemsTab.selControl and itemsTab.selControl ~= self.controls.activate and not itemsTab.selControl.ListControl) then
if main.popups[1] or mode == "OUT" or not item
or self:GetMouseOverControl() == self.controls.noteButton -- Note button has its own tooltip
or (not self.dropped and itemsTab.selControl and itemsTab.selControl ~= self.controls.activate and not itemsTab.selControl.ListControl) then
tooltip:Clear(true)
elseif tooltip:CheckForUpdate(item, launch.devModeAlt, itemsTab.build.outputRevision) then
itemsTab:AddItemTooltip(tooltip, item, self)
Expand Down Expand Up @@ -102,6 +115,9 @@ function ItemSlotClass:Populate()
if not self.selItemId or not self.itemsTab.items[self.selItemId] or not self.itemsTab:IsItemValidForSlot(self.itemsTab.items[self.selItemId], self.slotName) then
self:SetSelItemId(0)
end
if not self.nodeId then
self.itemsTab.activeItemSet[self.slotName].note = self.note
end

-- Update Jewel Sockets
local jewelSocketCount = 0
Expand Down Expand Up @@ -167,10 +183,17 @@ function ItemSlotClass:Draw(viewPort)
end

function ItemSlotClass:OnKeyDown(key)
if not self:IsShown() or not self:IsEnabled() then
if not self:IsShown() then
return
end
local mOverControl = self:GetMouseOverControl()
-- Notes don't care if the item slot is enabled or not
if mOverControl and mOverControl == self.controls.noteButton then
return mOverControl:OnKeyDown(key)
end
if not self:IsEnabled() then
return
end
if mOverControl and mOverControl == self.controls.activate then
return mOverControl:OnKeyDown(key)
end
Expand Down
22 changes: 15 additions & 7 deletions src/Classes/ItemsTab.lua
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ local ItemsTabClass = newClass("ItemsTab", "UndoHandler", "ControlHost", "Contro
if main.portraitMode then
self.controls.itemList = new("ItemListControl", {"TOPRIGHT",self.lastSlot,"BOTTOMRIGHT"}, {0, 0, 360, 308}, self, true)
else
self.controls.itemList = new("ItemListControl", {"TOPLEFT",self.controls.setManage,"TOPRIGHT"}, {20, 20, 360, 308}, self, true)
self.controls.itemList = new("ItemListControl", {"TOPLEFT",self.controls.setManage,"TOPRIGHT"}, {40, 20, 360, 308}, self, true)
end

-- Database selector
Expand Down Expand Up @@ -1123,13 +1123,16 @@ function ItemsTabClass:Load(xml, dbFileName)
elseif node.elem == "ItemSet" then
local itemSet = self:CreateItemSet(tonumber(node.attrib.id), node.attrib.title or "Default")
itemSet.useSecondWeaponSet = node.attrib.useSecondWeaponSet == "true"
itemSet.levelMin = tonumber(node.attrib.levelMin)
itemSet.levelMax = tonumber(node.attrib.levelMax)
for _, child in ipairs(node) do
if child.elem == "Slot" then
local slotName = child.attrib.name or ""
if itemSet[slotName] then
itemSet[slotName].selItemId = tonumber(child.attrib.itemId)
itemSet[slotName].active = child.attrib.active == "true"
itemSet[slotName].pbURL = child.attrib.itemPbURL or ""
itemSet[slotName].note = child.attrib.note
end
elseif child.elem == "SocketIdURL" then
local id = tonumber(child.attrib.nodeId)
Expand Down Expand Up @@ -1211,11 +1214,11 @@ function ItemsTabClass:Save(xml)
end
for _, itemSetId in ipairs(self.itemSetOrderList) do
local itemSet = self.itemSets[itemSetId]
local child = { elem = "ItemSet", attrib = { id = tostring(itemSetId), title = itemSet.title, useSecondWeaponSet = tostring(itemSet.useSecondWeaponSet) } }
local child = { elem = "ItemSet", attrib = { id = tostring(itemSetId), title = itemSet.title, useSecondWeaponSet = tostring(itemSet.useSecondWeaponSet), levelMin = tostring(itemSet.levelMin), levelMax = tostring(itemSet.levelMax) } }
for slotName, slot in pairs(self.slots) do
if not slot.parentSlot or itemSet[slotName].selItemId ~= 0 then
if not slot.nodeId then
t_insert(child, { elem = "Slot", attrib = { name = slotName, itemId = tostring(itemSet[slotName].selItemId), itemPbURL = itemSet[slotName].pbURL or "", active = itemSet[slotName].active and "true" }})
t_insert(child, { elem = "Slot", attrib = { name = slotName, itemId = tostring(itemSet[slotName].selItemId), itemPbURL = itemSet[slotName].pbURL or "", active = itemSet[slotName].active and "true", note = itemSet[slotName].note }})
else
if self.build.spec.allocNodes[slot.nodeId] then
t_insert(child, { elem = "SocketIdURL", attrib = { name = slotName, nodeId = tostring(slot.nodeId), itemPbURL = itemSet[slot.nodeId] and itemSet[slot.nodeId].pbURL or ""}})
Expand Down Expand Up @@ -1366,7 +1369,7 @@ function ItemsTabClass:Draw(viewPort, inputEvents)
if main.portraitMode then
self.controls.itemList:SetAnchor("TOPRIGHT", self.lastSlot, "BOTTOMRIGHT", 0, 40)
else
self.controls.itemList:SetAnchor("TOPLEFT", self.controls.setManage, "TOPRIGHT", 20, 20)
self.controls.itemList:SetAnchor("TOPLEFT", self.controls.setManage, "TOPRIGHT", 40, 20)
end
self.controls.craftDisplayItem:SetAnchor("TOPLEFT", main.portraitMode and self.controls.setManage or self.controls.itemList, "TOPRIGHT", 20, main.portraitMode and 0 or -20)
self.anchorDisplayItem:SetAnchor("TOPLEFT", main.portraitMode and self.controls.setManage or self.controls.itemList, "TOPRIGHT", 20, main.portraitMode and 0)
Expand All @@ -1393,7 +1396,7 @@ function ItemsTabClass:CreateItemSet(itemSetId, name)
end
for slotName, slot in pairs(self.slots) do
if not slot.nodeId then
itemSet[slotName] = { selItemId = 0 }
itemSet[slotName] = { selItemId = 0, note = slot.note }
end
end
self.itemSets[itemSet.id] = itemSet
Expand Down Expand Up @@ -1454,10 +1457,12 @@ function ItemsTabClass:SetActiveItemSet(itemSetId, deferSync)
-- Update the previous set
prevSet[slotName].selItemId = slot.selItemId
prevSet[slotName].active = slot.active
prevSet[slotName].note = slot.note
end
-- Equip the incoming set's item
slot.selItemId = curSet[slotName].selItemId
slot.active = curSet[slotName].active
slot.note = curSet[slotName].note
if slot.controls.activate then
slot.controls.activate.state = slot.active
end
Expand Down Expand Up @@ -2333,10 +2338,13 @@ function ItemsTabClass:OpenItemSetManagePopup()
controls.sharedList = new("SharedItemSetListControl", nil, {155, 50, 300, 200}, self)
controls.setList.dragTargetList = { controls.sharedList }
controls.sharedList.dragTargetList = { controls.setList }
controls.close = new("ButtonControl", nil, {0, 260, 90, 20}, "Done", function()
controls.levelRange = new("LevelRangeControl", nil, {-155, 260, 0, 16}, self.activeItemSet)
controls.setList.levelRange = controls.levelRange

controls.close = new("ButtonControl", nil, {0, 290, 90, 20}, "Done", function()
main:ClosePopup()
end)
main:OpenPopup(630, 290, "Manage Item Sets", controls)
main:OpenPopup(630, 320, "Manage Item Sets", controls)
end

-- Opens the item crafting popup
Expand Down
47 changes: 47 additions & 0 deletions src/Classes/LevelRangeControl.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-- Path of Building
--
-- Class: Level Range
-- Level Range controls to be reused for sets and the .build file
--

local LevelRangeClass = newClass("LevelRangeControl", "ControlHost", "Control", function(self, anchor, rect, set)
self.ControlHost()
self.Control(anchor, rect)
self.set = set
self.controls = {}
self.controls.minLevel = new("EditControl", {"TOPLEFT",self,"TOPLEFT"}, {0, 0, 150, 20}, set.levelMin or 0, "Level Min", "%D", 3, function(buf)
self.set.levelMin = tonumber(buf)
end)
self.controls.minLevel:SetPlaceholder("0")
self.controls.maxLevel = new("EditControl", {"TOPLEFT",self.controls.minLevel,"TOPRIGHT"}, {10, 0, 150, 20}, set.levelMax or 100, "Level Max", "%D", 3, function(buf)
self.set.levelMax = tonumber(buf)
end)
self.controls.maxLevel:SetPlaceholder("100")
end)

function LevelRangeClass:LoadSet(set)
self.set = set
self.controls.minLevel.buf = tostring(set.levelMin or "")
self.controls.maxLevel.buf = tostring(set.levelMax or "")
end

function LevelRangeClass:IsMouseOver()
if not self:IsShown() then
return false
end
return self:IsMouseInBounds() or self:GetMouseOverControl()
end

function LevelRangeClass:OnKeyDown(key, doubleClick)
if not self:IsShown() or not self:IsEnabled() then
return
end
local mOverControl = self:GetMouseOverControl()
if mOverControl and mOverControl.OnKeyDown then
return mOverControl:OnKeyDown(key, doubleClick)
end
end

function LevelRangeClass:Draw()
self:DrawControls()
end
33 changes: 32 additions & 1 deletion src/Classes/PassiveSpec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,17 @@ function PassiveSpecClass:Init(treeVersion, convert)

-- Keys are node IDs, values are the replacement node
self.hashOverrides = { }

-- Author notes attached to allocated nodes (Shift+Right-Click on a node to
-- set one). Keyed by node id; emitted into the PoE2 .build export as the
-- node's additional_text.
self.nodeNotes = { }
end

function PassiveSpecClass:Load(xml, dbFileName)
self.title = xml.attrib.title
self.levelMin = tonumber(xml.attrib.levelMin)
self.levelMax = tonumber(xml.attrib.levelMax)
local weaponSets = {}
local url
for _, node in pairs(xml) do
Expand Down Expand Up @@ -143,6 +150,17 @@ function PassiveSpecClass:Load(xml, dbFileName)
for nodeId in node.attrib.nodes:gmatch("%d+") do
weaponSets[tonumber(nodeId)] = weaponSet
end
elseif node.elem == "Notes" then
for _, child in ipairs(node) do
if child.elem == "Note" and child.attrib.nodeId then
local nid = tonumber(child.attrib.nodeId)
-- Note text lives in the element body (preserves newlines, no XML attribute escaping headaches).
local text = type(child[1]) == "string" and child[1] or child.attrib.text
if nid and text and text ~= "" then
self.nodeNotes[nid] = text
end
end
end
end
end
end
Expand Down Expand Up @@ -259,7 +277,9 @@ function PassiveSpecClass:Save(xml)
ascendancyInternalId = tostring(ascendancyInternalId),
secondaryAscendClassId = tostring(self.curSecondaryAscendClassId),
nodes = table.concat(allocNodeIdList, ","),
masteryEffects = table.concat(masterySelections, ",")
masteryEffects = table.concat(masterySelections, ","),
levelMin = self.levelMin and tostring(self.levelMin) or nil,
levelMax = self.levelMax and tostring(self.levelMax) or nil,
}
t_insert(xml, {
-- Legacy format
Expand Down Expand Up @@ -309,6 +329,17 @@ function PassiveSpecClass:Save(xml)
end
t_insert(xml, overrides)

-- Per-node author notes (Shift+Right-Click on a node). Stored as element
-- body text so multi-line notes survive without XML attribute escaping.
local notesElem = { elem = "Notes" }
local hasNotes = false
for nodeId, note in pairs(self.nodeNotes) do
if note and note ~= "" then
hasNotes = true
t_insert(notesElem, { elem = "Note", attrib = { nodeId = tostring(nodeId) }, [1] = note })
end
end
if hasNotes then t_insert(xml, notesElem) end
end

function PassiveSpecClass:PostLoad()
Expand Down
9 changes: 9 additions & 0 deletions src/Classes/PassiveSpecListControl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ function PassiveSpecListClass:RenameSpec(spec, popupTitle, addOnName)
t_insert(self.list, spec)
self.selIndex = #self.list
self.selValue = spec
if self.levelRange then
self.levelRange:LoadSet(spec)
end
end
self:UpdateItemsTabPassiveTreeDropdown()
self.treeTab.build:SyncLoadouts()
Expand Down Expand Up @@ -90,6 +93,9 @@ end
function PassiveSpecListClass:OnSelClick(index, spec, doubleClick)
if doubleClick and index ~= self.treeTab.activeSpec then
self.treeTab:SetActiveSpec(index)
if self.levelRange then
self.levelRange:LoadSet(self.treeTab.specList[index])
end
end
end

Expand All @@ -101,6 +107,9 @@ function PassiveSpecListClass:OnSelDelete(index, spec)
self.selValue = nil
if index == self.treeTab.activeSpec then
self.treeTab:SetActiveSpec(m_max(1, index - 1))
if self.levelRange then
self.levelRange:LoadSet(self.treeTab.specList[self.treeTab.activeSpec])
end
else
self.treeTab.activeSpec = isValueInArray(self.list, self.treeTab.build.spec)
end
Expand Down
Loading