LLocalIdentityPartial 0.4 export
4f2e9d9d创建于 2025年12月13日历史提交
if not loadStatFile then
	dofile("statdesc.lua")
end
loadStatFile("passive_skill_stat_descriptions.csd")

-- This table lists errors in the ggpk dat files
local datErrors = {
	["templar_notable_minimum_frenzy_charge"] = {
		["match"] = {
			["Name"] = "Powerful Faith",
		},
		["replace"] = {
			["Id"] = "templar_notable_minimum_power_charge",
		},
	},
	["templar_notable_minimum_power_charge"] = {
		["match"] = {
			["Name"] = "Frenzied Faith",
		},
		["replace"] = {
			["Id"] = "templar_notable_minimum_frenzy_charge",
		},
	},
	["karui_notable_add_physical_taken_as_fire"] = {
		["match"] = {
			["Id"] = "karui_notable_add_physical_taken_as_fire",
		},
		["replace"] = {
			["Id"] = "karui_notable_add_rage_on_melee_hit",
		},
	},
	["karui_notable_add_faster_burn"] = {
		["match"] = {
			["Id"] = "karui_notable_add_faster_burn",
		},
		["replace"] = {
			["Id"] = "karui_notable_add_faster_ignite",
		},
	},
	["maraketh_notable_add_ailment_avoid"] = {
		["match"] = {
			["Id"] = "maraketh_notable_add_ailment_avoid",
		},
		["replace"] = {
			["Id"] = "maraketh_notable_add_stun_avoid",
		},
	},
	["maraketh_notable_add_flask_effect"] = {
		["match"] = {
			["Id"] = "maraketh_notable_add_flask_effect",
		},
		["replace"] = {
			["Id"] = "maraketh_notable_add_alchemists_genius",
		},
	},
}

local fixDatErrors = function(row)
	if datErrors[row.Id] then
		for field, value in pairs(datErrors[row.Id].match) do
			if row[field] ~= value then return end
		end
		for field, value in pairs(datErrors[row.Id].replace) do
			row[field] = value
		end
	end
end

local out = io.open("../Data/TimelessJewelData/LegionPassives.lua", "w")

local stats = dat("Stats")
local alternatePassiveSkillDat = dat("AlternatePassiveSkills")
local alternatePassiveAdditionsDat = dat("AlternatePassiveAdditions")

local LEGION_PASSIVE_GROUP = 1e9

---@type fun(thing:string|table|number):string
function stringify(thing)
	if type(thing) == 'string' then
		return thing
	elseif type(thing) == 'number' then
		return ""..thing;
	elseif type(thing) == 'table' then
		local s = "{";
		for k,v in pairsSortByKey(thing) do
			s = s.."\n\t"
			if type(k) == 'number' then
				s = s.."["..k.."] = "
			else
				s = s.."[\""..k.."\"] = "
			end
			if type(v) == 'string' then
				s = s.."\""..stringify(v).."\", "
			else
				if type(v) == "boolean" then
					v = v and "true" or "false"
				end
				val = stringify(v)..", "
				if type(v) == "table" then
					val = string.gsub(val, "\n", "\n\t")
				end
				s = s..val;
			end
		end
		return s.."\n}"
	end
end

function parseStats(datFileRow, legionPassive)
	local descOrders = {}
	for idx,statKey in ipairs(datFileRow.StatsKeys) do
		local refRow = type(statKey) == "number" and statKey + 1 or statKey._rowIndex
		local statId = stats:ReadCell(refRow, 1)
		local range = datFileRow["Stat"..idx]

		local stat = {}
		stat[statId] = {
			["min"] = range[1],
			["max"] = range[2],
			["index"] = idx
		}
		-- Describing stats here to get the orders
		local statLines, orders = describeStats(stat)
		stat[statId].statOrder = orders[1] or 99999
		legionPassive.stats[statId] = stat[statId]
		for i, line in ipairs(statLines) do
			table.insert(legionPassive.sd, line)
			descOrders[line] = orders[i]
		end
	end
	-- Have to re-sort since we described the stats earlier
	table.sort(legionPassive.sd, function(a, b) return descOrders[a] < descOrders[b] end)
	local sortedStats = {}
	for stat in pairsSortByKey(legionPassive.stats) do
		table.insert(sortedStats, stat)
	end
	-- Finally get what we want, sorted stats by order
	table.sort(sortedStats, function(a, b) return legionPassive.stats[a].statOrder < legionPassive.stats[b].statOrder  end)
	legionPassive.sortedStats = sortedStats
end

---@type table <string, table> @this is the structure used to generate the final data file Data/TimelessJewelData/LegionPassives
local data = { }
data.nodes = { }
data.groups = { }
data.additions = { }
local ksCount = -1

for i=1, alternatePassiveSkillDat.rowCount do
	---@type table<string, boolean|string|number>
	local datFileRow = {}
	for j=1,#alternatePassiveSkillDat.cols-1 do
		local key = alternatePassiveSkillDat.spec[j].name
		datFileRow[key] = alternatePassiveSkillDat:ReadCell(i, j)
	end
	fixDatErrors(datFileRow)
	---@type table<string, boolean|string|number|table>
	local legionPassiveNode = {}
	-- id
	legionPassiveNode.id = datFileRow.Id
	-- icon
	legionPassiveNode.icon = datFileRow.DDSIcon
	-- is keystone
	legionPassiveNode.ks = isValueInTable(datFileRow.PassiveType, 4) and true or false
	if legionPassiveNode.ks then
		ksCount = ksCount + 1
	end
	-- is notable
	legionPassiveNode['not'] = isValueInTable(datFileRow.PassiveType, 3) and true or false
	-- node name
	legionPassiveNode.dn = datFileRow.Name
	-- is mastery wheel
	legionPassiveNode.m = false
	-- self explanatory
	legionPassiveNode.isJewelSocket = false
	legionPassiveNode.isMultipleChoice = false
	legionPassiveNode.isMultipleChoiceOption = false
	legionPassiveNode.passivePointsGranted = 0
	-- class starting node
	legionPassiveNode.spc = {}
	-- display text
	legionPassiveNode.sd = {}
	legionPassiveNode.stats = {}

	parseStats(datFileRow, legionPassiveNode)

	if legionPassiveNode.id == "vaal_keystone_2_v2" then -- Immortal Ambition needs to be manually added
        legionPassiveNode.sd = {
            [1] = "Energy Shield starts at zero",
            [2] = "Cannot Recharge or Regenerate Energy Shield",
            [3] = "Lose 5% of Energy Shield per second",
            [4] = "Life Leech effects are not removed when Unreserved Life is Filled",
            [5] = "Life Leech effects Recover Energy Shield instead while on Full Life"
        }
    end

	-- Node group, legion nodes don't use it, so we set it arbitrarily
	legionPassiveNode.g = LEGION_PASSIVE_GROUP
	-- 
	-- group orbit distance
	legionPassiveNode.o = legionPassiveNode.ks and 4 or 3
	legionPassiveNode.oidx = legionPassiveNode.ks and ksCount * 3 or math.floor(math.random() * 1e5)
	-- attributes granted 
	legionPassiveNode.sa = 0
	legionPassiveNode.da = 0
	legionPassiveNode.ia = 0
	-- connected nodes
	legionPassiveNode.out = {}
	legionPassiveNode["in"] = {}

	data.nodes[i] = legionPassiveNode
end

data.groups[LEGION_PASSIVE_GROUP] = {
    ["x"] = -6500,
    ["y"] = -6500,
    ["oo"] = {},
    ["n"] = {}
}

for k,v in ipairs(data.nodes) do
	table.insert(data.groups[LEGION_PASSIVE_GROUP].n, k)
end

for i=1, alternatePassiveAdditionsDat.rowCount do
	---@type table<string, boolean|string|number>
	local datFileRow = {};
	for j=1,#alternatePassiveAdditionsDat.cols-1 do
		local key = alternatePassiveAdditionsDat.spec[j].name
		datFileRow[key] = alternatePassiveAdditionsDat:ReadCell(i, j)
	end
	fixDatErrors(datFileRow)

	---@type table<string, boolean|string|number|table>
	local legionPassiveAddition = {}

	-- id
	legionPassiveAddition.id = datFileRow.Id
	-- Additions have no name, so we construct one for the UI (also, Lua patterns are too limiting :( )
	legionPassiveAddition.dn = string.gsub(string.gsub(string.gsub(datFileRow.Id, "_", " "), "^%w* ", ""), "^%w* ", "")
	legionPassiveAddition.dn = legionPassiveAddition.dn:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)
	-- stat descriptions
	legionPassiveAddition.sd = {}
	legionPassiveAddition.stats = {}

	parseStats(datFileRow, legionPassiveAddition)
	data.additions[i] = legionPassiveAddition
end

str = stringify(data)

out:write("-- This file is automatically generated, do not edit!\n-- Item data (c) Grinding Gear Games\n\n")
out:write("return "..str)
out:close()

print("Legion passives exported.")