Skip to content

Commit 1e502ad

Browse files
committed
Add build comparison tab
1 parent 3acd910 commit 1e502ad

4 files changed

Lines changed: 1519 additions & 2 deletions

File tree

src/Classes/CompareEntry.lua

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
-- Path of Building
2+
--
3+
-- Module: Compare Entry
4+
-- Lightweight Build wrapper for comparison. Loads XML, creates tabs, and runs calculations
5+
-- without setting up the full UI chrome of the primary build.
6+
--
7+
local t_insert = table.insert
8+
local m_min = math.min
9+
local m_max = math.max
10+
11+
local CompareEntryClass = newClass("CompareEntry", "ControlHost", function(self, xmlText, label)
12+
self.ControlHost()
13+
14+
self.label = label or "Comparison Build"
15+
self.buildName = label or "Comparison Build"
16+
self.xmlText = xmlText
17+
18+
-- Default build properties (mirrors Build.lua:Init lines 72-82)
19+
self.viewMode = "TREE"
20+
self.characterLevel = m_min(m_max(main.defaultCharLevel or 1, 1), 100)
21+
self.targetVersion = liveTargetVersion
22+
self.bandit = "None"
23+
self.pantheonMajorGod = "None"
24+
self.pantheonMinorGod = "None"
25+
self.characterLevelAutoMode = main.defaultCharLevel == 1 or main.defaultCharLevel == nil
26+
self.mainSocketGroup = 1
27+
28+
self.spectreList = {}
29+
self.timelessData = {
30+
jewelType = {}, conquerorType = {},
31+
devotionVariant1 = 1, devotionVariant2 = 1,
32+
jewelSocket = {}, fallbackWeightMode = {},
33+
searchList = "", searchListFallback = "",
34+
searchResults = {}, sharedResults = {}
35+
}
36+
37+
-- Shared data (read-only references)
38+
self.latestTree = main.tree[latestTreeVersion]
39+
self.data = data
40+
41+
-- Flags
42+
self.modFlag = false
43+
self.buildFlag = false
44+
self.outputRevision = 1
45+
46+
-- Display stats (same as primary build uses)
47+
self.displayStats, self.minionDisplayStats, self.extraSaveStats = LoadModule("Modules/BuildDisplayStats")
48+
49+
-- Load from XML
50+
if xmlText then
51+
self:LoadFromXML(xmlText)
52+
end
53+
end)
54+
55+
function CompareEntryClass:LoadFromXML(xmlText)
56+
-- Parse the XML (same pattern as Build.lua:LoadDB, line 1834)
57+
local dbXML, errMsg = common.xml.ParseXML(xmlText)
58+
if errMsg then
59+
ConPrintf("CompareEntry: Error parsing XML: %s", errMsg)
60+
return true
61+
end
62+
if not dbXML or not dbXML[1] or dbXML[1].elem ~= "PathOfBuilding" then
63+
ConPrintf("CompareEntry: 'PathOfBuilding' root element missing")
64+
return true
65+
end
66+
67+
-- Load Build section first (same pattern as Build.lua:LoadDB, line 1848)
68+
for _, node in ipairs(dbXML[1]) do
69+
if type(node) == "table" and node.elem == "Build" then
70+
self:LoadBuildSection(node)
71+
break
72+
end
73+
end
74+
75+
-- Check for import link
76+
for _, node in ipairs(dbXML[1]) do
77+
if type(node) == "table" and node.elem == "Import" then
78+
if node.attrib.importLink then
79+
self.importLink = node.attrib.importLink
80+
end
81+
break
82+
end
83+
end
84+
85+
-- Store XML sections for tab loading
86+
self.xmlSectionList = {}
87+
for _, node in ipairs(dbXML[1]) do
88+
if type(node) == "table" then
89+
t_insert(self.xmlSectionList, node)
90+
end
91+
end
92+
93+
-- Version check
94+
if self.targetVersion ~= liveTargetVersion then
95+
self.targetVersion = liveTargetVersion
96+
end
97+
98+
-- Create tabs (same pattern as Build.lua lines 579-590)
99+
-- PartyTab is replaced with a stub providing an empty enemyModList and actor
100+
-- (CalcPerform.lua:1088 accesses build.partyTab.actor for party member buffs)
101+
local partyActor = { Aura = {}, Curse = {}, Warcry = {}, Link = {}, modDB = new("ModDB"), output = {} }
102+
partyActor.modDB.actor = partyActor
103+
self.partyTab = { enemyModList = new("ModList"), actor = partyActor }
104+
self.configTab = new("ConfigTab", self)
105+
self.itemsTab = new("ItemsTab", self)
106+
self.treeTab = new("TreeTab", self)
107+
self.skillsTab = new("SkillsTab", self)
108+
self.calcsTab = new("CalcsTab", self)
109+
110+
-- Set up savers table (same pattern as Build.lua lines 593-606)
111+
self.savers = {
112+
["Config"] = self.configTab,
113+
["Tree"] = self.treeTab,
114+
["TreeView"] = self.treeTab.viewer,
115+
["Items"] = self.itemsTab,
116+
["Skills"] = self.skillsTab,
117+
["Calcs"] = self.calcsTab,
118+
}
119+
self.legacyLoaders = {
120+
["Spec"] = self.treeTab,
121+
}
122+
123+
-- Special rebuild to properly initialise boss placeholders
124+
self.configTab:BuildModList()
125+
126+
-- Load legacy bandit and pantheon choices from build section
127+
for _, control in ipairs({ "bandit", "pantheonMajorGod", "pantheonMinorGod" }) do
128+
self.configTab.input[control] = self[control]
129+
end
130+
131+
-- Load XML sections into tabs (same pattern as Build.lua lines 620-647)
132+
-- Defer passive trees until after items are loaded (jewel socket issue)
133+
local deferredPassiveTrees = {}
134+
for _, node in ipairs(self.xmlSectionList) do
135+
local saver = self.savers[node.elem] or self.legacyLoaders[node.elem]
136+
if saver then
137+
if saver == self.treeTab then
138+
t_insert(deferredPassiveTrees, node)
139+
else
140+
saver:Load(node, "CompareEntry")
141+
end
142+
end
143+
end
144+
for _, node in ipairs(deferredPassiveTrees) do
145+
self.treeTab:Load(node, "CompareEntry")
146+
end
147+
for _, saver in pairs(self.savers) do
148+
if saver.PostLoad then
149+
saver:PostLoad()
150+
end
151+
end
152+
153+
if next(self.configTab.input) == nil then
154+
if self.configTab.ImportCalcSettings then
155+
self.configTab:ImportCalcSettings()
156+
end
157+
end
158+
159+
-- Build calculation output tables (same pattern as Build.lua lines 654-657)
160+
self.calcsTab:BuildOutput()
161+
self.buildFlag = false
162+
end
163+
164+
-- Load build section attributes (same pattern as Build.lua:Load, line 927)
165+
function CompareEntryClass:LoadBuildSection(xml)
166+
self.targetVersion = xml.attrib.targetVersion or legacyTargetVersion
167+
if xml.attrib.viewMode then
168+
self.viewMode = xml.attrib.viewMode
169+
end
170+
self.characterLevel = tonumber(xml.attrib.level) or 1
171+
self.characterLevelAutoMode = xml.attrib.characterLevelAutoMode == "true"
172+
for _, diff in pairs({ "bandit", "pantheonMajorGod", "pantheonMinorGod" }) do
173+
self[diff] = xml.attrib[diff] or "None"
174+
end
175+
self.mainSocketGroup = tonumber(xml.attrib.mainSkillIndex) or tonumber(xml.attrib.mainSocketGroup) or 1
176+
wipeTable(self.spectreList)
177+
for _, child in ipairs(xml) do
178+
if child.elem == "Spectre" then
179+
if child.attrib.id and data.minions[child.attrib.id] then
180+
t_insert(self.spectreList, child.attrib.id)
181+
end
182+
elseif child.elem == "TimelessData" then
183+
self.timelessData.jewelType = { id = tonumber(child.attrib.jewelTypeId) }
184+
self.timelessData.conquerorType = { id = tonumber(child.attrib.conquerorTypeId) }
185+
self.timelessData.devotionVariant1 = tonumber(child.attrib.devotionVariant1) or 1
186+
self.timelessData.devotionVariant2 = tonumber(child.attrib.devotionVariant2) or 1
187+
self.timelessData.jewelSocket = { id = tonumber(child.attrib.jewelSocketId) }
188+
self.timelessData.fallbackWeightMode = { idx = tonumber(child.attrib.fallbackWeightModeIdx) }
189+
self.timelessData.socketFilter = child.attrib.socketFilter == "true"
190+
self.timelessData.socketFilterDistance = tonumber(child.attrib.socketFilterDistance) or 0
191+
self.timelessData.searchList = child.attrib.searchList
192+
self.timelessData.searchListFallback = child.attrib.searchListFallback
193+
end
194+
end
195+
end
196+
197+
function CompareEntryClass:GetOutput()
198+
return self.calcsTab.mainOutput
199+
end
200+
201+
function CompareEntryClass:GetSpec()
202+
return self.spec
203+
end
204+
205+
function CompareEntryClass:Rebuild()
206+
wipeGlobalCache()
207+
self.outputRevision = self.outputRevision + 1
208+
self.calcsTab:BuildOutput()
209+
self.buildFlag = false
210+
end
211+
212+
function CompareEntryClass:SetActiveSpec(index)
213+
if self.treeTab and self.treeTab.SetActiveSpec then
214+
self.treeTab:SetActiveSpec(index)
215+
self:Rebuild()
216+
end
217+
end
218+
219+
function CompareEntryClass:SetActiveItemSet(id)
220+
if self.itemsTab and self.itemsTab.SetActiveItemSet then
221+
self.itemsTab:SetActiveItemSet(id)
222+
self:Rebuild()
223+
end
224+
end
225+
226+
function CompareEntryClass:SetActiveSkillSet(id)
227+
if self.skillsTab and self.skillsTab.SetActiveSkillSet then
228+
self.skillsTab:SetActiveSkillSet(id)
229+
self:Rebuild()
230+
end
231+
end
232+
233+
-- Stub methods that the build interface may call
234+
function CompareEntryClass:RefreshStatList()
235+
-- No sidebar to refresh in comparison entry
236+
end
237+
238+
function CompareEntryClass:RefreshSkillSelectControls()
239+
-- No skill select controls in comparison entry
240+
end
241+
242+
function CompareEntryClass:UpdateClassDropdowns()
243+
-- No class dropdowns in comparison entry
244+
end
245+
246+
function CompareEntryClass:SyncLoadouts()
247+
-- No loadout syncing in comparison entry
248+
end
249+
250+
function CompareEntryClass:OpenSpectreLibrary()
251+
-- No spectre library in comparison entry
252+
end
253+
254+
function CompareEntryClass:AddStatComparesToTooltip(tooltip, baseOutput, compareOutput, header, nodeCount)
255+
-- Reuse the stat comparison logic
256+
local count = 0
257+
if self.calcsTab and self.calcsTab.mainEnv and self.calcsTab.mainEnv.player and self.calcsTab.mainEnv.player.mainSkill then
258+
if self.calcsTab.mainEnv.player.mainSkill.minion and baseOutput.Minion and compareOutput.Minion then
259+
count = count + self:CompareStatList(tooltip, self.minionDisplayStats, self.calcsTab.mainEnv.minion, baseOutput.Minion, compareOutput.Minion, header.."\n^7Minion:", nodeCount)
260+
if count > 0 then
261+
header = "^7Player:"
262+
else
263+
header = header.."\n^7Player:"
264+
end
265+
end
266+
count = count + self:CompareStatList(tooltip, self.displayStats, self.calcsTab.mainEnv.player, baseOutput, compareOutput, header, nodeCount)
267+
end
268+
return count
269+
end
270+
271+
-- Stat comparison (mirrors Build.lua:CompareStatList, line 1733)
272+
function CompareEntryClass:CompareStatList(tooltip, statList, actor, baseOutput, compareOutput, header, nodeCount)
273+
local s_format = string.format
274+
local count = 0
275+
if not actor or not actor.mainSkill then
276+
return 0
277+
end
278+
for _, statData in ipairs(statList) do
279+
if statData.stat and not statData.childStat and statData.stat ~= "SkillDPS" then
280+
local flagMatch = true
281+
if statData.flag then
282+
if type(statData.flag) == "string" then
283+
flagMatch = actor.mainSkill.skillFlags[statData.flag]
284+
elseif type(statData.flag) == "table" then
285+
for _, flag in ipairs(statData.flag) do
286+
if not actor.mainSkill.skillFlags[flag] then
287+
flagMatch = false
288+
break
289+
end
290+
end
291+
end
292+
end
293+
if statData.notFlag then
294+
if type(statData.notFlag) == "string" then
295+
if actor.mainSkill.skillFlags[statData.notFlag] then
296+
flagMatch = false
297+
end
298+
elseif type(statData.notFlag) == "table" then
299+
for _, flag in ipairs(statData.notFlag) do
300+
if actor.mainSkill.skillFlags[flag] then
301+
flagMatch = false
302+
break
303+
end
304+
end
305+
end
306+
end
307+
if flagMatch then
308+
local statVal1 = compareOutput[statData.stat] or 0
309+
local statVal2 = baseOutput[statData.stat] or 0
310+
local diff = statVal1 - statVal2
311+
if statData.stat == "FullDPS" and not compareOutput[statData.stat] then
312+
diff = 0
313+
end
314+
if (diff > 0.001 or diff < -0.001) and (not statData.condFunc or statData.condFunc(statVal1, compareOutput) or statData.condFunc(statVal2, baseOutput)) then
315+
if count == 0 then
316+
tooltip:AddLine(14, header)
317+
end
318+
local color = ((statData.lowerIsBetter and diff < 0) or (not statData.lowerIsBetter and diff > 0)) and colorCodes.POSITIVE or colorCodes.NEGATIVE
319+
local val = diff * ((statData.pc or statData.mod) and 100 or 1)
320+
local valStr = s_format("%+"..statData.fmt, val)
321+
local number, suffix = valStr:match("^([%+%-]?%d+%.%d+)(%D*)$")
322+
if number then
323+
valStr = number:gsub("0+$", ""):gsub("%.$", "") .. suffix
324+
end
325+
valStr = formatNumSep(valStr)
326+
local line = s_format("%s%s %s", color, valStr, statData.label)
327+
if statData.compPercent and statVal1 ~= 0 and statVal2 ~= 0 then
328+
local pc = statVal1 / statVal2 * 100 - 100
329+
line = line .. s_format(" (%+.1f%%)", pc)
330+
end
331+
tooltip:AddLine(14, line)
332+
count = count + 1
333+
end
334+
end
335+
end
336+
end
337+
return count
338+
end
339+
340+
return CompareEntryClass

0 commit comments

Comments
 (0)