Files
PathOfBuilding/src/Export/Scripts/skills.lua
LocalIdentity 14a61e262f Fix exporter
Add new datatype UInt16
Removed old dats
Manually code in SkillTotem loop
Update spec
2024-11-18 21:51:49 +11:00

681 lines
21 KiB
Lua

local skillTypes = {
"Attack",
"Spell",
"Projectile",
"DualWieldOnly",
"Buff",
"Minion",
"Damage",
"Area",
"Duration",
"RequiresShield",
"ProjectileSpeed",
"HasReservation",
"ReservationBecomesCost",
"Trappable",
"Totemable",
"Mineable",
"ElementalStatus",
"MinionsCanExplode",
"Chains",
"Melee",
"MeleeSingleTarget",
"Multicastable",
"TotemCastsAlone",
"Multistrikeable",
"CausesBurning",
"SummonsTotem",
"TotemCastsWhenNotDetached",
"Physical",
"Fire",
"Cold",
"Lightning",
"Triggerable",
"Trapped",
"Movement",
"DamageOverTime",
"RemoteMined",
"Triggered",
"Vaal",
"Aura",
"CanTargetUnusableCorpse",
"RangedAttack",
"Chaos",
"FixedSpeedProjectile",
"ThresholdJewelArea",
"ThresholdJewelProjectile",
"ThresholdJewelDuration",
"ThresholdJewelRangedAttack",
"Channel",
"DegenOnlySpellDamage",
"InbuiltTrigger",
"Golem",
"Herald",
"AuraAffectsEnemies",
"NoRuthless",
"ThresholdJewelSpellDamage",
"Cascadable",
"ProjectilesFromUser",
"MirageArcherCanUse",
"ProjectileSpiral",
"SingleMainProjectile",
"MinionsPersistWhenSkillRemoved",
"ProjectileNumber",
"Warcry",
"Instant",
"Brand",
"DestroysCorpse",
"NonHitChill",
"ChillingArea",
"AppliesCurse",
"CanRapidFire",
"AuraDuration",
"AreaSpell",
"OR",
"AND",
"NOT",
"AppliesMaim",
"CreatesMinion",
"Guard",
"Travel",
"Blink",
"CanHaveBlessing",
"ProjectilesNotFromUser",
"AttackInPlaceIsDefault",
"Nova",
"InstantNoRepeatWhenHeld",
"InstantShiftAttackForLeftMouse",
"AuraNotOnCaster",
"Banner",
"Rain",
"Cooldown",
"ThresholdJewelChaining",
"Slam",
"Stance",
"NonRepeatable",
"OtherThingUsesSkill",
"Steel",
"Hex",
"Mark",
"Aegis",
"Orb",
"KillNoDamageModifiers",
"RandomElement",
"LateConsumeCooldown",
"Arcane",
"FixedCastTime",
"RequiresOffHandNotWeapon",
"Link",
"Blessing",
"ZeroReservation",
"DynamicCooldown",
"Microtransaction",
"OwnerCannotUse",
"ProjectilesNotFired",
"TotemsAreBallistae",
"SkillGrantedBySupport",
"PreventHexTransfer",
"MinionsAreUndamageable",
"InnateTrauma",
"DualWieldRequiresDifferentTypes",
"NoVolley",
"Retaliation",
"NeverExertable",
}
-- This is here to fix name collisions like in the case of Barrage
local fullNameGems = {
["Metadata/Items/Gems/SupportGemBarrage"] = true,
}
local function mapAST(ast)
return "SkillType."..(skillTypes[ast._rowIndex] or ("Unknown"..ast._rowIndex))
end
local weaponClassMap = {
["Claw"] = "Claw",
["Dagger"] = "Dagger",
["Wand"] = "Wand",
["One Hand Sword"] = "One Handed Sword",
["Thrusting One Hand Sword"] = "Thrusting One Handed Sword",
["One Hand Axe"] = "One Handed Axe",
["One Hand Mace"] = "One Handed Mace",
["Bow"] = "Bow",
["Fishing Rod"] = "Fishing Rod",
["Staff"] = "Staff",
["Two Hand Sword"] = "Two Handed Sword",
["Two Hand Axe"] = "Two Handed Axe",
["Two Hand Mace"] = "Two Handed Mace",
["Sceptre"] = "Sceptre",
["Unarmed"] = "None",
}
local skillStatScope = { }
do
local text = convertUTF16to8(getFile("Metadata/StatDescriptions/skillpopup_stat_filters.txt"))
for skillName, scope in text:gmatch('([%w_]+) "Metadata/StatDescriptions/([%w_]+)%.txt"') do
skillStatScope[skillName] = scope
end
for skillName, copyFromSkill in text:gmatch('copy ([%w_]+) ([%w_]+)') do
skillStatScope[skillName] = skillStatScope[copyFromSkill]
end
end
local gems = { }
local trueGemNames = { }
local directiveTable = { }
-- #noGem
-- Disables the gem component of the next skill
directiveTable.noGem = function(state, args, out)
state.noGem = true
end
-- #addSkillTypes <flag>[ <flag>[...]]
-- skill types to be added to the skillTypes flags for this active skill
directiveTable.addSkillTypes = function(state, args, out)
state.addSkillTypes = {}
for flag in args:gmatch("%a+") do
table.insert(state.addSkillTypes, flag)
end
end
-- #skill <GrantedEffectId> [<Display name>]
-- Initialises the skill data and emits the skill header
directiveTable.skill = function(state, args, out)
local grantedId, displayName = args:match("(%w+) (.+)")
if not grantedId then
grantedId = args
displayName = args
end
out:write('skills["', grantedId, '"] = {\n')
local granted = dat("GrantedEffects"):GetRow("Id", grantedId)
if not granted then
ConPrintf('Unknown GE: "'..grantedId..'"')
return
end
local gemEffect = dat("GemEffects"):GetRow("GrantedEffect", granted)
local secondaryEffect
if not gemEffect then
gemEffect = dat("GemEffects"):GetRow("GrantedEffect2", granted)
if gemEffect then
secondaryEffect = true
end
end
local skillGem
if gemEffect then
for gem in dat("SkillGems"):Rows() do
for _, variant in ipairs(gem.GemVariants) do
if gemEffect.Id == variant.Id then
skillGem = gem
local trueGemNameObj = dat("GemEffects"):GetRow("Id", gemEffect.Id)
if trueGemNameObj.Name ~= "" then
trueGemNames[gemEffect.Id] = trueGemNameObj.Name
end
break
end
end
if skillGem then break end
end
end
local skill = { }
state.skill = skill
if skillGem and not state.noGem then
gems[gemEffect.Id] = true
if granted.IsSupport then
out:write('\tname = "', fullNameGems[skillGem.BaseItemType.Id] and skillGem.BaseItemType.Name or skillGem.BaseItemType.Name:gsub(" Support",""), '",\n')
if #gemEffect.Description > 0 then
out:write('\tdescription = "', gemEffect.Description:gsub('\n','\\n'), '",\n')
end
else
out:write('\tname = "', secondaryEffect and granted.ActiveSkill.DisplayName or trueGemNames[gemEffect.Id] or granted.ActiveSkill.DisplayName, '",\n')
-- Hybrid gems (e.g. Vaal gems) use the display name of the active skill e.g. Vaal Summon Skeletons of Sorcery
out:write('\tbaseTypeName = "', granted.ActiveSkill.DisplayName, '",\n')
end
else
if displayName == args and not granted.IsSupport then
displayName = gemEffect and trueGemNames[gemEffect.Id] or granted.ActiveSkill.DisplayName
end
out:write('\tname = "', displayName, '",\n')
out:write('\thidden = true,\n')
end
state.noGem = false
skill.baseFlags = { }
local modMap = { }
skill.mods = { }
skill.levels = { }
local statMap = { }
skill.stats = { }
skill.constantStats = { }
skill.addSkillTypes = state.addSkillTypes
state.addSkillTypes = nil
out:write('\tcolor = ', granted.Attribute, ',\n')
if granted.GrantedEffectStatSets.BaseEffectiveness ~= 1 then
out:write('\tbaseEffectiveness = ', granted.GrantedEffectStatSets.BaseEffectiveness, ',\n')
end
if granted.GrantedEffectStatSets.IncrementalEffectiveness ~= 0 then
out:write('\tincrementalEffectiveness = ', granted.GrantedEffectStatSets.IncrementalEffectiveness, ',\n')
end
if granted.IsSupport then
skill.isSupport = true
out:write('\tsupport = true,\n')
out:write('\trequireSkillTypes = { ')
for _, type in ipairs(granted.SupportTypes) do
out:write(mapAST(type), ', ')
end
out:write('},\n')
out:write('\taddSkillTypes = { ')
skill.isTrigger = false
for _, type in ipairs(granted.AddTypes) do
local typeString = mapAST(type)
if typeString == "SkillType.Triggered" then
skill.isTrigger = true
end
out:write(typeString, ', ')
end
out:write('},\n')
out:write('\texcludeSkillTypes = { ')
for _, type in ipairs(granted.ExcludeTypes) do
out:write(mapAST(type), ', ')
end
out:write('},\n')
if skill.isTrigger then
out:write('\tisTrigger = true,\n')
end
if granted.SupportGemsOnly then
out:write('\tsupportGemsOnly = true,\n')
end
if granted.IgnoreMinionTypes then
out:write('\tignoreMinionTypes = true,\n')
end
if granted.PlusVersionOf then
out:write('\tplusVersionOf = "', granted.PlusVersionOf.Id, '",\n')
end
local weaponTypes = { }
for _, class in ipairs(granted.WeaponRestrictions) do
if weaponClassMap[class.Id] then
weaponTypes[weaponClassMap[class.Id]] = true
end
end
if next(weaponTypes) then
out:write('\tweaponTypes = {\n')
for type in pairs(weaponTypes) do
out:write('\t\t["', type, '"] = true,\n')
end
out:write('\t},\n')
end
out:write('\tstatDescriptionScope = "gem_stat_descriptions",\n')
else
if #granted.ActiveSkill.Description > 0 then
out:write('\tdescription = "', granted.ActiveSkill.Description:gsub('"','\\"'):gsub('\n','\\n'), '",\n')
end
out:write('\tskillTypes = { ')
for _, type in ipairs(granted.ActiveSkill.SkillTypes) do
out:write('[', mapAST(type), '] = true, ')
end
if skill.addSkillTypes then
for _, type in ipairs(skill.addSkillTypes) do
out:write('[SkillType.', type , '] = true, ')
end
end
out:write('},\n')
if granted.ActiveSkill.MinionSkillTypes[1] then
out:write('\tminionSkillTypes = { ')
for _, type in ipairs(granted.ActiveSkill.MinionSkillTypes) do
out:write('[', mapAST(type), '] = true, ')
end
out:write('},\n')
end
local weaponTypes = { }
for _, class in ipairs(granted.ActiveSkill.WeaponRestrictions) do
if weaponClassMap[class.Id] then
weaponTypes[weaponClassMap[class.Id]] = true
end
end
if next(weaponTypes) then
out:write('\tweaponTypes = {\n')
for type in pairs(weaponTypes) do
out:write('\t\t["', type, '"] = true,\n')
end
out:write('\t},\n')
end
out:write('\tstatDescriptionScope = "', skillStatScope[granted.ActiveSkill.Id] or "skill_stat_descriptions", '",\n')
if granted.ActiveSkill.SkillTotem <= 21 then
out:write('\tskillTotemId = ', granted.ActiveSkill.SkillTotem, ',\n')
end
out:write('\tcastTime = ', granted.CastTime / 1000, ',\n')
if granted.CannotBeSupported then
out:write('\tcannotBeSupported = true,\n')
end
end
local statsPerLevel = dat("GrantedEffectStatSetsPerLevel"):GetRowList("GrantedEffectStatSets", granted.GrantedEffectStatSets)
local statMapOrder = {}
local perLevel = dat("GrantedEffectsPerLevel"):GetRowList("GrantedEffect", granted)
if #perLevel ~= #statsPerLevel and #perLevel > 1 and #statsPerLevel > 1 then
ConPrintf("UNKNOWN CASE of Level to Stat rows for '" .. granted.Id .. "'")
end
for indx = 1, math.max(#perLevel, #statsPerLevel) do
local levelRow = perLevel[indx] or perLevel[1]
local statRow = statsPerLevel[indx] or statsPerLevel[1]
local level = { extra = { }, statInterpolation = { }, cost = { } }
level.level = #perLevel == 1 and statRow.GemLevel or levelRow.Level
level.extra.levelRequirement = #perLevel == 1 and statRow.PlayerLevelReq or levelRow.PlayerLevelReq
for i, cost in ipairs(levelRow.CostTypes) do
level.cost[cost["Resource"]] = levelRow.CostAmounts[i]
end
if levelRow.ManaReservationFlat ~= 0 then
level.extra.manaReservationFlat = levelRow.ManaReservationFlat
end
if levelRow.ManaReservationPercent ~= 0 then
level.extra.manaReservationPercent = levelRow.ManaReservationPercent / 100
end
if levelRow.LifeReservationFlat ~= 0 then
level.extra.lifeReservationFlat = levelRow.LifeReservationFlat
end
if levelRow.LifeReservationPercent ~= 0 then
level.extra.lifeReservationPercent = levelRow.LifeReservationPercent / 100
end
if levelRow.CostMultiplier ~= 100 then
level.extra.manaMultiplier = levelRow.CostMultiplier - 100
end
if levelRow.AttackSpeedMultiplier and levelRow.AttackSpeedMultiplier ~= 0 then
level.extra.attackSpeedMultiplier = levelRow.AttackSpeedMultiplier
end
if levelRow.AttackTime ~= 0 then
level.extra.attackTime = levelRow.AttackTime
end
if levelRow.Cooldown and levelRow.Cooldown ~= 0 then
level.extra.cooldown = levelRow.Cooldown / 1000
end
if levelRow.PvPDamageMultiplier ~= 0 then
level.extra.PvPDamageMultiplier = levelRow.PvPDamageMultiplier
end
if levelRow.StoredUses ~= 0 then
level.extra.storedUses = levelRow.StoredUses
end
if levelRow.VaalSouls ~= 0 then
level.cost.Soul = levelRow.VaalSouls
end
if levelRow.VaalStoredUses ~= 0 then
level.extra.vaalStoredUses = levelRow.VaalStoredUses
end
if levelRow.SoulGainPreventionDuration ~= 0 then
level.extra.soulPreventionDuration = levelRow.SoulGainPreventionDuration / 1000
end
-- stat based level info
if statRow.DamageEffectiveness ~= 0 then
level.extra.damageEffectiveness = statRow.DamageEffectiveness / 10000 + 1
end
if statRow.AttackCritChance ~= 0 then
level.extra.critChance = statRow.AttackCritChance / 100
end
if statRow.OffhandCritChance ~= 0 then
level.extra.critChance = statRow.OffhandCritChance / 100
end
if statRow.BaseMultiplier and statRow.BaseMultiplier ~= 0 then
level.extra.baseMultiplier = statRow.BaseMultiplier / 10000 + 1
end
level.statInterpolation = statRow.StatInterpolations
local resolveInterpolation = false
local injectConstantValuesIntoEachLevel = false
local statMapOrderIndex = 1
for i, stat in ipairs(statRow.FloatStats) do
if not statMap[stat.Id] or indx == 1 then
statMap[stat.Id] = #skill.stats + 1
table.insert(skill.stats, { id = stat.Id })
if indx == 1 then
table.insert(statMapOrder, stat.Id)
else
print(displayName .. ": stat missing from earlier levels: ".. stat.Id)
end
elseif statMapOrder[statMapOrderIndex] ~= stat.Id then
-- add missing stats
while statMapOrderIndex < #statMapOrder and statMapOrder[statMapOrderIndex] ~= stat.Id do
table.insert(level, 0)
if #level.statInterpolation < #statMapOrder then
table.insert(level.statInterpolation, statMapOrderIndex, "0")
end
statMapOrderIndex = statMapOrderIndex + 1
end
end
statMapOrderIndex = statMapOrderIndex + 1
if resolveInterpolation then
table.insert(level, statRow.BaseResolvedValues[i])
level.statInterpolation[statMapOrderIndex] = 1
else
table.insert(level, statRow.FloatStatsValues[i] / math.max(statRow.InterpolationBases[i].Value, 0.00001) )
end
end
if injectConstantValuesIntoEachLevel then
for i, stat in ipairs(granted.GrantedEffectStatSets.ConstantStats) do
if not statMap[stat.Id] then
statMap[stat.Id] = #skill.stats + #skill.constantStats + 1
table.insert(skill.stats, { id = stat.Id })
if indx == 1 then
table.insert(statMapOrder, stat.Id)
else
print(displayName .. ": stat missing from earlier levels: ".. stat.Id)
end
elseif statMapOrder[statMapOrderIndex] ~= stat.Id then
-- add missing stats
while statMapOrderIndex < #statMapOrder and statMapOrder[statMapOrderIndex] ~= stat.Id do
table.insert(level, 0)
if #level.statInterpolation < #statMapOrder then
table.insert(level.statInterpolation, statMapOrderIndex, "0")
end
statMapOrderIndex = statMapOrderIndex + 1
end
end
statMapOrderIndex = statMapOrderIndex + 1
table.insert(level, granted.GrantedEffectStatSets.ConstantStatsValues[i])
table.insert(level.statInterpolation, #statRow.FloatStats + 1, 1)
end
end
for i, stat in ipairs(statRow.AdditionalStats) do
if not statMap[stat.Id] then
statMap[stat.Id] = #skill.stats + 1
table.insert(skill.stats, { id = stat.Id })
if indx == 1 then
table.insert(statMapOrder, stat.Id)
else
print(displayName .. ": stat missing from earlier levels: ".. stat.Id)
end
elseif statMapOrder[statMapOrderIndex] ~= stat.Id then
-- add missing stats
while statMapOrderIndex < #statMapOrder and statMapOrder[statMapOrderIndex] ~= stat.Id do
table.insert(level, 0)
if #level.statInterpolation < #statMapOrder then
table.insert(level.statInterpolation, statMapOrderIndex, "0")
end
statMapOrderIndex = statMapOrderIndex + 1
end
end
statMapOrderIndex = statMapOrderIndex + 1
table.insert(level, statRow.AdditionalStatsValues[i])
end
for i, stat in ipairs(statRow.AdditionalBooleanStats) do
if not statMap[stat.Id] then
statMap[stat.Id] = #skill.stats + 1
table.insert(skill.stats, { id = stat.Id })
end
end
table.insert(skill.levels, level)
end
for i, stat in ipairs(granted.GrantedEffectStatSets.ImplicitStats) do
if not statMap[stat.Id] then
statMap[stat.Id] = #skill.stats + 1
table.insert(skill.stats, { id = stat.Id })
end
end
for i, stat in ipairs(granted.GrantedEffectStatSets.ConstantStats) do
table.insert(skill.constantStats, { stat.Id, granted.GrantedEffectStatSets.ConstantStatsValues[i] })
end
if not skill.qualityStats then
skill.qualityStats = { }
for i, qualityStatsRow in ipairs(dat("GrantedEffectQualityStats"):GetRowList("GrantedEffect", granted)) do
skill.qualityStats[i] = { }
for j, stat in ipairs(qualityStatsRow.GrantedStats) do
table.insert(skill.qualityStats[i], { stat.Id, qualityStatsRow.StatValues[j] / 1000 })
--ConPrintf("[%d] %s %s", i, granted.ActiveSkill.DisplayName, stat.Id)
end
end
end
end
-- #flags <flag>[ <flag>[...]]
-- Sets the base flags for this active skill
directiveTable.flags = function(state, args, out)
local skill = state.skill
for flag in args:gmatch("%a+") do
table.insert(skill.baseFlags, flag)
end
end
-- #baseMod <mod definition>
-- Adds a base modifier to the skill
directiveTable.baseMod = function(state, args, out)
local skill = state.skill
table.insert(skill.mods, args)
end
-- #mods
-- Emits the skill modifiers
directiveTable.mods = function(state, args, out)
local skill = state.skill
if not args:match("noBaseFlags") then
if not skill.isSupport then
out:write('\tbaseFlags = {\n')
for _, flag in ipairs(skill.baseFlags) do
out:write('\t\t', flag, ' = true,\n')
end
out:write('\t},\n')
end
end
if not args:match("noBaseMods") then
if next(skill.mods) ~= nil then
out:write('\tbaseMods = {\n')
for _, mod in ipairs(skill.mods) do
out:write('\t\t', mod, ',\n')
end
out:write('\t},\n')
end
end
if not args:match("noQualityStats") then
if next(skill.qualityStats) ~= nil then
out:write('\tqualityStats = {\n')
for i, alternates in ipairs(skill.qualityStats) do
if i == 1 then
out:write('\t\tDefault = {\n')
else
local value = i - 1
out:write('\t\tAlternate' .. value .. ' = {\n')
end
for _, stat in ipairs(alternates) do
out:write('\t\t\t{ "', stat[1], '", ', stat[2], ' },\n')
end
out:write('\t\t},\n')
end
out:write('\t},\n')
end
end
if not args:match("noStats") then
if next(skill.constantStats) ~= nil then
-- write out constant stats that don't change per level
out:write('\tconstantStats = {\n')
for i, stat in ipairs(skill.constantStats) do
out:write('\t\t{ "', stat[1], '", ', stat[2], ' },\n')
end
out:write('\t},\n')
end
-- write out per level stats
out:write('\tstats = {\n')
for _, stat in ipairs(skill.stats) do
out:write('\t\t"', stat.id, '",\n')
end
out:write('\t},\n')
end
if not args:match("noLevels") then
out:write('\tlevels = {\n')
for index, level in ipairs(skill.levels) do
out:write('\t\t[', level.level, '] = { ')
for _, statVal in ipairs(level) do
out:write(tostring(statVal), ', ')
end
for k, v in pairs(level.extra) do
out:write(k, ' = ', tostring(v), ', ')
end
if next(level.statInterpolation) ~= nil then
out:write('statInterpolation = { ')
for _, type in ipairs(level.statInterpolation) do
out:write(type, ', ')
end
out:write('}, ')
end
if next(level.cost) ~= nil then
out:write('cost = { ')
for k, v in pairs(level.cost) do
out:write(k, ' = ', tostring(v), ', ')
end
out:write('}, ')
end
out:write('},\n')
end
out:write('\t},\n')
end
out:write('}')
state.skill = nil
end
for _, name in pairs({"act_str","act_dex","act_int","other","glove","minion","spectre","sup_str","sup_dex","sup_int"}) do
processTemplateFile(name, "Skills/", "../Data/Skills/", directiveTable)
end
local out = io.open("../Data/Gems.lua", "w")
out:write('-- This file is automatically generated, do not edit!\n')
out:write('-- Gem data (c) Grinding Gear Games\n\nreturn {\n')
for skillGem in dat("SkillGems"):Rows() do
for _, gemEffect in ipairs(skillGem.GemVariants) do
if gems[gemEffect.Id] then
out:write('\t["', "Metadata/Items/Gems/SkillGem" .. gemEffect.Id, '"] = {\n')
out:write('\t\tname = "', fullNameGems[skillGem.BaseItemType.Id] and skillGem.BaseItemType.Name or trueGemNames[gemEffect.Id] or skillGem.BaseItemType.Name:gsub(" Support",""), '",\n')
-- Hybrid gems (e.g. Vaal gems) use the display name of the active skill e.g. Vaal Summon Skeletons of Sorcery
if not skillGem.IsSupport then
out:write('\t\tbaseTypeName = "', gemEffect.GrantedEffect.ActiveSkill.DisplayName, '",\n')
end
out:write('\t\tgameId = "', skillGem.BaseItemType.Id, '",\n')
out:write('\t\tvariantId = "', gemEffect.Id, '",\n')
out:write('\t\tgrantedEffectId = "', gemEffect.GrantedEffect.Id, '",\n')
if gemEffect.GrantedEffect2 then
out:write('\t\tsecondaryGrantedEffectId = "', gemEffect.GrantedEffect2.Id, '",\n')
end
if #gemEffect.SecondarySupportName > 0 then
out:write('\t\tsecondaryEffectName = "', gemEffect.SecondarySupportName, '",\n')
end
if skillGem.IsVaalGem then
out:write('\t\tvaalGem = true,\n')
end
local tagNames = { }
out:write('\t\ttags = {\n')
for _, tag in ipairs(gemEffect.Tags) do
out:write('\t\t\t', tag.Id, ' = true,\n')
if #tag.Name > 0 then
table.insert(tagNames, tag.Name)
end
end
out:write('\t\t},\n')
out:write('\t\ttagString = "', table.concat(tagNames, ", "), '",\n')
out:write('\t\treqStr = ', skillGem.Str, ',\n')
out:write('\t\treqDex = ', skillGem.Dex, ',\n')
out:write('\t\treqInt = ', skillGem.Int, ',\n')
local naturalMaxLevel = #dat("ItemExperiencePerLevel"):GetRowList("ItemExperienceType", skillGem.GemLevelProgression)
out:write('\t\tnaturalMaxLevel = ', naturalMaxLevel > 0 and naturalMaxLevel or 1, ',\n')
out:write('\t},\n')
end
end
end
out:write('}')
out:close()
print("Skill data exported.")