Path of Exile Wiki

Please consider helping keep the wiki up to date. Check the to-do list of updates needed for version 3.14.0.

Game data exports will becoming later as the technical changes in addition to regular changes take some more time.

READ MORE

Path of Exile Wiki
m (Protected "Module:Skill": High traffic page ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite)))
(Set flag on gems with alternative quality)
(45 intermediate revisions by 3 users not shown)
Line 1: Line 1:
-- ----------------------------------------------------------------------------
+
-------------------------------------------------------------------------------
-- Includes
+
--
  +
-- Module:Skill
-- ----------------------------------------------------------------------------
 
  +
--
  +
-- This module implements Template:Skill and Template:Skill progression
  +
-------------------------------------------------------------------------------
   
 
local getArgs = require('Module:Arguments').getArgs
 
local getArgs = require('Module:Arguments').getArgs
 
local m_util = require('Module:Util')
 
local m_util = require('Module:Util')
local m_game = require('Module:Game')
+
local m_cargo = require('Module:Cargo')
  +
  +
local m_game = mw.loadData('Module:Game')
  +
  +
-- The cfg table contains all localisable strings and configuration, to make it
  +
-- easier to port this module to another wiki.
  +
local cfg = mw.loadData('Module:Skill/config')
   
 
local mwlanguage = mw.language.getContentLanguage()
 
local mwlanguage = mw.language.getContentLanguage()
   
  +
local i18n = cfg.i18n
--- define here to avoid errors
 
  +
local tables = {}
 
local data = {}
 
local data = {}
local map = {}
 
 
-- TODO:
 
-- skill_id field link to data page
 
-- aspects: % mana cost
 
 
-- ----------------------------------------------------------------------------
 
-- i18n
 
-- ----------------------------------------------------------------------------
 
 
local i18n = {
 
skill_icon = 'File:%s skill icon.png',
 
skill_screenshot = 'File:%s skill screenshot.jpg',
 
 
infobox = {
 
skill_id = 'Skill Id',
 
active_skill_name = 'Name',
 
skill_icon = 'Icon',
 
cast_time = 'Cast Time',
 
item_class_restrictions = 'Item Class<br>Restrictions',
 
projectile_speed = 'Projectile Speed',
 
radius = 'Radius',
 
radius_secondary = 'Radius 2',
 
radius_tertiary = 'Radius 3',
 
level_requirement = 'Level Req.',
 
mana_multiplier = 'Mana Multiplier',
 
critical_strike_chance = 'Critical Strike Chance',
 
mana_cost = 'Mana Cost',
 
mana_reserved = 'Mana Reserved',
 
damage_effectiveness = 'Damage Effectiveness',
 
stored_uses = 'Stored Uses',
 
cooldown = 'Cooldown',
 
vaal_souls_requirement = 'Vaal Souls',
 
vaal_stored_uses = 'Vaal Stored Uses',
 
vaal_soul_gain_prevention_time = 'Soul Gain Prevention',
 
damage_multiplier = 'Damage Multiplier',
 
},
 
 
args = {
 
-- skills
 
skill_id = 'skill_id',
 
is_support_gem = 'is_support_gem',
 
support_gem_letter = 'support_gem_letter',
 
cast_time = 'cast_time',
 
gem_description = 'gem_description',
 
active_skill_name = 'active_skill_name',
 
skill_icon = 'skill_icon',
 
item_class_restriction = 'item_class_restriction',
 
projectile_speed = 'projectile_speed',
 
stat_text = 'stat_text',
 
quality_stat_text = 'quality_stat_text',
 
has_percentage_mana_cost = 'has_percentage_mana_cost',
 
has_reservation_mana_cost = 'has_reservation_mana_cost',
 
radius = 'radius',
 
radius_description = 'radius_description',
 
radius_secondary = 'radius_secondary',
 
radius_secondary_description = 'radius_secondary_description',
 
radius_tertiary = 'radius_tertiary',
 
radius_tertiary_description = 'radius_tertiary_description',
 
skill_screenshot = 'skill_screenshot',
 
 
-- skill_levels
 
level_requirement = 'level_requirement',
 
dexterity_requirement = 'dexterity_requirement',
 
intelligence_requirement = 'intelligence_requirement',
 
strength_requirement = 'strength_requirement',
 
mana_multiplier = 'mana_multiplier',
 
critical_strike_chance = 'critical_strike_chance',
 
mana_cost = 'mana_cost',
 
damage_effectiveness = 'damage_effectiveness',
 
stored_uses = 'stored_uses',
 
cooldown = 'cooldown',
 
vaal_souls_requirement = 'vaal_souls_requirement',
 
vaal_stored_uses = 'vaal_stored_uses',
 
vaal_soul_gain_prevention_time = 'vaal_soul_gain_prevention_time',
 
damage_multiplier = 'damage_multiplier',
 
experience = 'experience',
 
stat_text = 'stat_text',
 
quality_stat_text = 'quality_stat_text',
 
 
-- skill_stats_per_level
 
id = 'id',
 
value = 'value',
 
 
-- prefixes
 
prefix_level = 'level',
 
prefix_static = 'static_',
 
prefix_quality_stat = 'quality_',
 
infix_stat = 'stat'
 
},
 
}
 
   
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
Line 108: Line 27:
 
local h = {}
 
local h = {}
   
function h.map_to_arg(tpl_args, frame, properties, prefix_in, map, level)
+
function h.map_to_arg(tpl_args, frame, properties, prefix_in, map, level, set_name, set_id)
if map.order then
+
if map.fields then
for _, key in ipairs(map.order) do
+
for key, row in pairs(map.fields) do
row = map.fields[key]
 
 
if row.name then
 
if row.name then
 
local val = tpl_args[prefix_in .. row.name]
 
local val = tpl_args[prefix_in .. row.name]
Line 120: Line 38:
 
val = row.default
 
val = row.default
 
end
 
end
if val ~= nil then
+
if val ~= nil then
 
if level ~= nil then
 
if level ~= nil then
tpl_args.skill_levels[level][key] = val
+
if set_name then
  +
tpl_args.skill_levels[level][set_name] = tpl_args.skill_levels[level][set_name] or {}
  +
tpl_args.skill_levels[level][set_name][set_id] = tpl_args.skill_levels[level][set_name][set_id] or {}
  +
tpl_args.skill_levels[level][set_name][set_id][key] = val
  +
else
  +
tpl_args.skill_levels[level][key] = val
  +
end
  +
 
-- Nuke variables since they're remapped to skill_levels
 
-- Nuke variables since they're remapped to skill_levels
 
tpl_args[prefix_in .. row.name] = nil
 
tpl_args[prefix_in .. row.name] = nil
 
else
 
else
tpl_args[row.name] = val
+
if set_name then
  +
tpl_args[set_name] = tpl_args[set_name] or {}
  +
tpl_args[set_name][set_id] = tpl_args[set_name][set_id] or {}
  +
tpl_args[set_name][set_id][key] = val
  +
  +
-- Nuke variables since they're remapped to [set_name]
  +
tpl_args[prefix_in .. row.name] = nil
  +
else
  +
tpl_args[key] = val
  +
end
 
end
 
end
 
properties[row.field] = val
 
properties[row.field] = val
  +
  +
-- Deprecated parameters
  +
if val and row.deprecated then
  +
tpl_args.has_deprecated_parameters = true
  +
if tpl_args.test then -- Log when testing
  +
tpl_args.deprecated_parameters = tpl_args.deprecated_parameters or {}
  +
tpl_args.deprecated_parameters[#tpl_args.deprecated_parameters+1] = {row.name, val}
  +
end
  +
end
 
end
 
end
else
 
error(string.format('Programming error, missing field %s from order keys', key))
 
 
end
 
end
 
end
 
end
Line 137: Line 78:
 
end
 
end
   
function h.stats(tpl_args, frame, prefix_in, level)
+
function h.costs(tpl_args, frame, prefix_in, level)
  +
tpl_args.skill_costs = tpl_args.skill_costs or {}
for _, stat_type in ipairs(map.stats) do
 
  +
for i=1, #tpl_args.skill_costs do
local type_prefix = string.format('%s%s%s', prefix_in, stat_type.prefix_in, i18n.args.infix_stat)
 
  +
local cost_prefix = string.format('%s%s%d_', prefix_in, i18n.parameters.skill.cost, i) -- level<level>_cost<i>_
tpl_args.skill_levels[level][stat_type.out] = {}
 
for i=1, 8 do
+
local cost = {
  +
amount = tpl_args[cost_prefix .. tables.skill_level_costs.fields.amount.name], --level<level>_cost<i>_amount
local stat_id_key = string.format('%s%s_%s', type_prefix, i, i18n.args.id)
 
  +
}
local stat_val_key = string.format('%s%s_%s', type_prefix, i, i18n.args.value)
 
local stat = {
+
if cost.amount ~= nil then
id = tpl_args[stat_id_key],
+
local properties = {
value = tonumber(tpl_args[stat_val_key]),
+
_table = tables.skill_level_costs.table,
  +
[tables.skill_level_costs.fields.set_id.field] = i,
  +
[tables.skill_level_costs.fields.level.field] = level,
 
}
 
}
  +
h.map_to_arg(tpl_args, frame, properties, cost_prefix, tables.skill_level_costs, level, 'costs', i)
if stat.id ~= nil and stat.value ~= nil then
 
tpl_args.skill_levels[level][stat_type.out][#tpl_args.skill_levels[level][stat_type.out]+1] = stat
+
if not tpl_args.test then
+
m_cargo.store(frame, properties)
m_util.cargo.store(frame, {
 
_table = map.skill_stats_per_level.table,
 
[map.skill_stats_per_level.fields.level.field] = level,
 
[map.skill_stats_per_level.fields.id.field] = stat.id,
 
[map.skill_stats_per_level.fields.value.field] = stat.value,
 
[map.skill_stats_per_level.fields.is_quality_stat.field] = stat_type.quality,
 
})
 
 
-- Nuke variables since they're remapped to skill levels
 
tpl_args[stat_id_key] = nil
 
tpl_args[stat_val_key] = nil
 
 
end
 
end
 
end
 
end
Line 167: Line 99:
 
end
 
end
   
function h.na(tr)
+
function h.stats(tpl_args, frame, prefix_in, level)
  +
for i=1, cfg.max_stats_per_level do
tr
 
  +
local stat_prefix = string.format('%s%s%d_', prefix_in, i18n.parameters.skill.stat, i) -- level<level>_stat<i>_
:tag('td')
 
:attr('class', 'table-na')
+
local stat = {
  +
id = tpl_args[stat_prefix .. tables.skill_stats_per_level.fields.id.name], --level<level>_stat<i>_id
:wikitext('N/A')
 
  +
value = tpl_args[stat_prefix .. tables.skill_stats_per_level.fields.value.name], --level<level>_stat<i>_value
:done()
 
  +
}
  +
if stat.id ~= nil and stat.value ~= nil then
  +
local properties = {
  +
_table = tables.skill_stats_per_level.table,
  +
[tables.skill_stats_per_level.fields.level.field] = level,
  +
}
  +
h.map_to_arg(tpl_args, frame, properties, stat_prefix, tables.skill_stats_per_level, level, 'stats', i)
  +
tpl_args.skill_levels.has_stats = true
  +
if not tpl_args.test then
  +
m_cargo.store(frame, properties)
  +
end
  +
end
  +
end
 
end
 
end
   
function h.int_value_or_na(tpl_args, frame, tr, value, pdata)
+
function h.int_value_or_na(tpl_args, frame, tblrow, value, tmap)
 
value = tonumber(value)
 
value = tonumber(value)
 
if value == nil then
 
if value == nil then
h.na(tr)
+
tblrow:node(m_util.html.td.na())
 
else
 
else
value = mwlanguage:formatNum(value)
+
-- value = mwlanguage:formatNum(value) -- Removed for now. lang:formatNum() returns a string, which causes issues for formatting
if pdata.fmt ~= nil then
+
if tmap.fmt ~= nil then
if type(pdata.fmt) == 'string' then
+
if type(tmap.fmt) == 'string' then
value = string.format(pdata.fmt, value)
+
value = string.format(tmap.fmt, value)
elseif type(pdata.fmt) == 'function' then
+
elseif type(tmap.fmt) == 'function' then
value = string.format(pdata.fmt(tpl_args, frame), value)
+
value = string.format(tmap.fmt(tpl_args, frame) or '%s', value)
 
end
 
end
 
end
 
end
tr
+
tblrow
 
:tag('td')
 
:tag('td')
 
:wikitext(value)
 
:wikitext(value)
Line 210: Line 155:
 
function h.display.factory.value(args)
 
function h.display.factory.value(args)
 
return function (tpl_args, frame)
 
return function (tpl_args, frame)
args.fmt = args.fmt or map.static.fields[args.key].fmt
+
args.fmt = args.fmt or tables.static.fields[args.key].fmt
 
local value = tpl_args[args.key]
 
local value = tpl_args[args.key]
 
if args.fmt and value then
 
if args.fmt and value then
Line 222: Line 167:
 
function h.display.factory.range_value(args)
 
function h.display.factory.range_value(args)
 
return function (tpl_args, frame)
 
return function (tpl_args, frame)
local value = {
+
local value = {}
min = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[1][args.key],
+
if args.set_name and args.set_id then
  +
-- Guard against index errors
max = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.key],
 
  +
tpl_args.skill_levels[0][args.set_name] = tpl_args.skill_levels[0][args.set_name] or {}
}
 
  +
tpl_args.skill_levels[0][args.set_name][args.set_id] = tpl_args.skill_levels[0][args.set_name][args.set_id] or {}
  +
tpl_args.skill_levels[1][args.set_name] = tpl_args.skill_levels[1][args.set_name] or {}
  +
tpl_args.skill_levels[1][args.set_name][args.set_id] = tpl_args.skill_levels[1][args.set_name][args.set_id] or {}
  +
tpl_args.skill_levels[tpl_args.max_level][args.set_name] = tpl_args.skill_levels[tpl_args.max_level][args.set_name] or {}
  +
tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id] = tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id] or {}
  +
  +
value.min = tpl_args.skill_levels[0][args.set_name][args.set_id][args.key] or tpl_args.skill_levels[1][args.set_name][args.set_id][args.key]
  +
value.max = tpl_args.skill_levels[0][args.set_name][args.set_id][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id][args.key]
  +
else
  +
value.min = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[1][args.key]
  +
value.max = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.key]
  +
end
  +
 
-- property not set for this skill
 
-- property not set for this skill
 
if value.min == nil or value.max == nil then
 
if value.min == nil or value.max == nil then
 
return
 
return
 
end
 
end
  +
 
  +
local map = args.map or tables.progression
 
return m_util.html.format_value(tpl_args, frame, value, {
 
return m_util.html.format_value(tpl_args, frame, value, {
fmt=args.fmt or map.progression.fields[args.key].fmt,
+
fmt=args.fmt or map.fields[args.key].fmt,
no_color=true,
+
color=false,
 
})
 
})
 
end
 
end
Line 254: Line 213:
   
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
  +
-- Cargo tables
-- Data
 
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
   
  +
tables.static = {
 
map.stats = {
 
{
 
prefix_in = '',
 
out = 'stats',
 
quality = false,
 
},
 
{
 
prefix_in = i18n.args.prefix_quality_stat,
 
out = 'quality_stats',
 
quality = true,
 
},
 
}
 
 
map.static = {
 
 
table = 'skill',
 
table = 'skill',
order = {'skill_id', 'cast_time', 'gem_description', 'active_skill_name', 'skill_icon', 'item_class_restriction', 'projectile_speed', 'stat_text', 'quality_stat_text', 'has_percentage_mana_cost', 'has_reservation_mana_cost', 'radius', 'radius_secondary', 'radius_tertiary', 'radius_description', 'radius_secondary_description', 'radius_tertiary_description', 'skill_screenshot'},
 
 
fields = {
 
fields = {
 
-- GrantedEffects.dat
 
-- GrantedEffects.dat
 
skill_id = {
 
skill_id = {
name = i18n.args.skill_id,
+
name = i18n.parameters.skill.skill_id,
 
field = 'skill_id',
 
field = 'skill_id',
 
type = 'String',
 
type = 'String',
Line 284: Line 228:
 
-- Active Skills.dat
 
-- Active Skills.dat
 
cast_time = {
 
cast_time = {
name = i18n.args.cast_time,
+
name = i18n.parameters.skill.cast_time,
 
field = 'cast_time',
 
field = 'cast_time',
 
type = 'Float',
 
type = 'Float',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
fmt = '%ss',
+
fmt = '%.2f ' .. m_game.units.seconds.short_lower,
 
},
 
},
 
gem_description = {
 
gem_description = {
name = i18n.args.gem_description,
+
name = i18n.parameters.skill.gem_description,
 
field = 'description',
 
field = 'description',
 
type = 'Text',
 
type = 'Text',
Line 297: Line 241:
 
},
 
},
 
active_skill_name = {
 
active_skill_name = {
name = i18n.args.active_skill_name,
+
name = i18n.parameters.skill.active_skill_name,
 
field = 'active_skill_name',
 
field = 'active_skill_name',
 
type = 'String',
 
type = 'String',
Line 303: Line 247:
 
},
 
},
 
skill_icon = {
 
skill_icon = {
name = i18n.args.skill_icon,
+
name = i18n.parameters.skill.skill_icon,
 
field = 'skill_icon',
 
field = 'skill_icon',
 
type = 'Page',
 
type = 'Page',
 
func = function(tpl_args, frame)
 
func = function(tpl_args, frame)
 
if tpl_args.active_skill_name then
 
if tpl_args.active_skill_name then
return string.format(i18n.skill_icon, tpl_args.active_skill_name)
+
return string.format(i18n.files.skill_icon, tpl_args.active_skill_name)
 
end
 
end
 
end,
 
end,
 
},
 
},
item_class_restriction = {
+
item_class_id_restriction = {
name = i18n.args.item_class_restriction,
+
name = i18n.parameters.skill.item_class_id_restriction,
field = 'item_class_restriction',
+
field = 'item_class_id_restriction',
 
type = 'List (,) of String',
 
type = 'List (,) of String',
 
func = function(tpl_args, frame, value)
 
func = function(tpl_args, frame, value)
Line 322: Line 266:
 
value = m_util.string.split(value, ', ')
 
value = m_util.string.split(value, ', ')
 
for _, v in ipairs(value) do
 
for _, v in ipairs(value) do
local result = m_util.table.find_in_nested_array{value=v,key='full', tbl=m_game.constants.item.class}
+
if m_game.constants.item.classes[v] == nil then
if result == nil then
+
error(string.format(i18n.errors.skill.invalid_item_class_id, v))
error(string.format('Invalid item class: %s', v))
 
 
end
 
end
 
end
 
end
 
return value
 
return value
  +
end,
  +
},
  +
item_class_restriction = {
  +
name = i18n.parameters.skill.item_class_restriction,
  +
field = 'item_class_restriction',
  +
type = 'List (,) of String',
  +
func = function(tpl_args, frame, value)
  +
if tpl_args.item_class_id_restriction == nil then
  +
return
  +
end
  +
-- This function makes a localized list based on ids
  +
local item_classes = {}
  +
for _, v in ipairs(tpl_args.item_class_id_restriction) do
  +
item_classes[#item_classes+1] = m_game.constants.item.classes[v].full
  +
end
  +
  +
return item_classes
 
end,
 
end,
 
},
 
},
 
-- Projectiles.dat - manually mapped to the skills
 
-- Projectiles.dat - manually mapped to the skills
 
projectile_speed = {
 
projectile_speed = {
name = i18n.args.projectile_speed,
+
name = i18n.parameters.skill.projectile_speed,
 
field = 'projectile_speed',
 
field = 'projectile_speed',
 
type = 'Integer',
 
type = 'Integer',
Line 339: Line 299:
 
-- Misc data derieved from stats
 
-- Misc data derieved from stats
 
stat_text = {
 
stat_text = {
name = i18n.args.stat_text,
+
name = i18n.parameters.skill.stat_text,
 
field = 'stat_text',
 
field = 'stat_text',
 
type = 'Text',
 
type = 'Text',
Line 345: Line 305:
 
},
 
},
 
quality_stat_text = {
 
quality_stat_text = {
name = i18n.args.quality_stat_text,
+
name = i18n.parameters.skill.quality_stat_text,
 
field = 'quality_stat_text',
 
field = 'quality_stat_text',
 
type = 'Text',
 
type = 'Text',
Line 351: Line 311:
 
},
 
},
 
-- Misc data currently not from game data
 
-- Misc data currently not from game data
has_percentage_mana_cost = {
 
name = i18n.args.has_percentage_mana_cost,
 
field = 'has_percentage_mana_cost',
 
type = 'Boolean',
 
func = h.cast.wrap(m_util.cast.boolean),
 
default = false,
 
},
 
has_reservation_mana_cost = {
 
name = i18n.args.has_reservation_mana_cost,
 
field = 'has_reservation_mana_cost',
 
type = 'Boolean',
 
func = h.cast.wrap(m_util.cast.boolean),
 
default = false,
 
},
 
 
radius = {
 
radius = {
name = i18n.args.radius,
+
name = i18n.parameters.skill.radius,
 
field = 'radius',
 
field = 'radius',
 
type = 'Integer',
 
type = 'Integer',
Line 372: Line 318:
 
},
 
},
 
radius_description = {
 
radius_description = {
name = i18n.args.radius_description,
+
name = i18n.parameters.skill.radius_description,
 
field = 'radius_description',
 
field = 'radius_description',
 
type = 'Text',
 
type = 'Text',
func = nil,
+
func = h.cast.wrap(m_util.cast.text),
 
},
 
},
 
radius_secondary = {
 
radius_secondary = {
name = i18n.args.radius_secondary,
+
name = i18n.parameters.skill.radius_secondary,
 
field = 'radius_secondary',
 
field = 'radius_secondary',
 
type = 'Integer',
 
type = 'Integer',
Line 384: Line 330:
 
},
 
},
 
radius_secondary_description = {
 
radius_secondary_description = {
name = i18n.args.radius_secondary_description,
+
name = i18n.parameters.skill.radius_secondary_description,
 
field = 'radius_secondary_description',
 
field = 'radius_secondary_description',
 
type = 'Text',
 
type = 'Text',
func = nil,
+
func = h.cast.wrap(m_util.cast.text),
 
},
 
},
-- not sure if any skill actually has 3 radius componets
+
radius_tertiary = { -- not sure if any skill actually has 3 radius componets
  +
name = i18n.parameters.skill.radius_tertiary,
radius_tertiary = {
 
name = i18n.args.radius_tertiary,
 
 
field = 'radius_tertiary',
 
field = 'radius_tertiary',
 
type = 'Integer',
 
type = 'Integer',
Line 397: Line 342:
 
},
 
},
 
radius_tertiary_description = {
 
radius_tertiary_description = {
name = i18n.args.radius_tertiary_description,
+
name = i18n.parameters.skill.radius_tertiary_description,
 
field = 'radius_tertiary_description',
 
field = 'radius_tertiary_description',
 
type = 'Text',
 
type = 'Text',
func = nil,
+
func = h.cast.wrap(m_util.cast.text),
},
 
-- Set manually
 
max_level = {
 
field = 'max_level',
 
type = 'Integer',
 
},
 
html = {
 
field = 'html',
 
type = 'Text',
 
 
},
 
},
 
skill_screenshot = {
 
skill_screenshot = {
name = i18n.args.skill_screenshot,
+
name = i18n.parameters.skill.skill_screenshot,
 
field = 'skill_screenshot',
 
field = 'skill_screenshot',
 
type = 'Page',
 
type = 'Page',
Line 420: Line 356:
 
ss = string.format('File:%s', tpl_args.skill_screenshot_file)
 
ss = string.format('File:%s', tpl_args.skill_screenshot_file)
 
elseif tpl_args.skill_screenshot ~= nil then
 
elseif tpl_args.skill_screenshot ~= nil then
ss = string.format(i18n.skill_screenshot, tpl_args.skill_screenshot)
+
ss = string.format(i18n.files.skill_screenshot, tpl_args.skill_screenshot)
 
elseif tpl_args.active_skill_name then
 
elseif tpl_args.active_skill_name then
 
-- When this parameter is set manually, we assume/expect it to be exist, but otherwise it probably doesn't and we don't need dead links in that case
 
-- When this parameter is set manually, we assume/expect it to be exist, but otherwise it probably doesn't and we don't need dead links in that case
ss = string.format(i18n.skill_screenshot, tpl_args.active_skill_name)
+
ss = string.format(i18n.files.skill_screenshot, tpl_args.active_skill_name)
 
page = mw.title.new(ss)
 
page = mw.title.new(ss)
 
if page == nil or not page.exists then
 
if page == nil or not page.exists then
Line 431: Line 367:
 
return ss
 
return ss
 
end,
 
end,
  +
},
  +
-- Set programmatically
  +
max_level = {
  +
name = nil,
  +
field = 'max_level',
  +
type = 'Integer',
  +
func = h.cast.wrap(m_util.cast.number),
  +
},
  +
html = {
  +
name = nil,
  +
field = 'html',
  +
type = 'Text',
  +
func = nil,
  +
},
  +
-- Deprecated
  +
has_percentage_mana_cost = {
  +
name = i18n.parameters.skill.has_percentage_mana_cost,
  +
field = 'has_percentage_mana_cost',
  +
type = 'Boolean',
  +
func = h.cast.wrap(m_util.cast.boolean),
  +
default = false,
  +
deprecated = true,
  +
},
  +
has_reservation_mana_cost = {
  +
name = i18n.parameters.skill.has_reservation_mana_cost,
  +
field = 'has_reservation_mana_cost',
  +
type = 'Boolean',
  +
func = h.cast.wrap(m_util.cast.boolean),
  +
default = false,
  +
deprecated = true,
 
},
 
},
 
},
 
},
 
}
 
}
   
map.progression = {
+
tables.progression = {
 
table = 'skill_levels',
 
table = 'skill_levels',
order = {'level_requirement', 'dexterity_requirement', 'intelligence_requirement', 'strength_requirement', 'mana_multiplier', 'critical_strike_chance', 'mana_cost', 'damage_effectiveness', 'stored_uses', 'cooldown', 'vaal_souls_requirement', 'vaal_stored_uses', 'vaal_soul_gain_prevention_time', 'damage_multiplier', 'experience', 'stat_text', 'quality_stat_text'},
 
 
fields = {
 
fields = {
 
level = {
 
level = {
Line 444: Line 409:
 
type = 'Integer',
 
type = 'Integer',
 
func = nil,
 
func = nil,
header = nil,
 
 
},
 
},
 
level_requirement = {
 
level_requirement = {
name = i18n.args.level_requirement,
+
name = i18n.parameters.skill.level_requirement,
 
field = 'level_requirement',
 
field = 'level_requirement',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('[[Image:Level_up_icon_small.png|link=|Lvl.]]', 'Required Level', 'nounderline'),
 
 
},
 
},
 
dexterity_requirement = {
 
dexterity_requirement = {
name = i18n.args.dexterity_requirement,
+
name = i18n.parameters.skill.dexterity_requirement,
 
field = 'dexterity_requirement',
 
field = 'dexterity_requirement',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('[[Image:DexterityIcon_small.png|link=|dexterity]]', 'Required Dexterity', 'nounderline'),
 
 
},
 
},
 
strength_requirement = {
 
strength_requirement = {
name = i18n.args.strength_requirement,
+
name = i18n.parameters.skill.strength_requirement,
 
field = 'strength_requirement',
 
field = 'strength_requirement',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('[[Image:StrengthIcon_small.png|link=|strength]]', 'Required Strength', 'nounderline'),
 
 
},
 
},
 
intelligence_requirement = {
 
intelligence_requirement = {
name = i18n.args.intelligence_requirement,
+
name = i18n.parameters.skill.intelligence_requirement,
 
field = 'intelligence_requirement',
 
field = 'intelligence_requirement',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('[[Image:IntelligenceIcon_small.png|link=|intelligence]]', 'Required Intelligence', 'nounderline'),
 
 
},
 
},
 
mana_multiplier = {
 
mana_multiplier = {
name = i18n.args.mana_multiplier,
+
name = i18n.parameters.skill.mana_multiplier,
 
field = 'mana_multiplier',
 
field = 'mana_multiplier',
 
type = 'Float',
 
type = 'Float',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = 'Mana<br>Multiplier',
 
 
fmt = '%s%%',
 
fmt = '%s%%',
 
},
 
},
 
critical_strike_chance = {
 
critical_strike_chance = {
name = i18n.args.critical_strike_chance,
+
name = i18n.parameters.skill.critical_strike_chance,
 
field = 'critical_strike_chance',
 
field = 'critical_strike_chance',
 
type = 'Float',
 
type = 'Float',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = 'Critical<br>Strike<br>Chance',
 
 
fmt = '%s%%',
 
fmt = '%s%%',
},
 
mana_cost = {
 
name = i18n.args.mana_cost,
 
field = 'mana_cost',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
header = function (skill_data)
 
if skill_data["skill.has_reservation_mana_cost"] then
 
return 'Mana<br>Reserved'
 
else
 
return 'Mana<br>Cost'
 
end
 
end,
 
fmt = function (tpl_args, frame)
 
if tpl_args.has_percentage_mana_cost then
 
str = '%s%%'
 
else
 
str = '%s'
 
end
 
 
return str
 
end,
 
 
},
 
},
 
damage_effectiveness = {
 
damage_effectiveness = {
name = i18n.args.damage_effectiveness,
+
name = i18n.parameters.skill.damage_effectiveness,
 
field = 'damage_effectiveness',
 
field = 'damage_effectiveness',
 
type = 'Float',
 
type = 'Float',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = 'Damage<br>Effectiveness',
 
 
fmt = '%s%%',
 
fmt = '%s%%',
 
},
 
},
 
stored_uses = {
 
stored_uses = {
name = i18n.args.stored_uses,
+
name = i18n.parameters.skill.stored_uses,
 
field = 'stored_uses',
 
field = 'stored_uses',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = 'Stored<br>Uses',
 
 
},
 
},
 
cooldown = {
 
cooldown = {
name = i18n.args.cooldown,
+
name = i18n.parameters.skill.cooldown,
 
field = 'cooldown',
 
field = 'cooldown',
 
type = 'Float',
 
type = 'Float',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = 'Cooldown',
+
fmt = '%.2f ' .. m_game.units.seconds.short_lower,
fmt = '%ss',
 
 
},
 
},
 
vaal_souls_requirement = {
 
vaal_souls_requirement = {
name = i18n.args.vaal_souls_requirement,
+
name = i18n.parameters.skill.vaal_souls_requirement,
 
field = 'vaal_souls_requirement',
 
field = 'vaal_souls_requirement',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = 'Vaal<br>souls',
 
 
},
 
},
 
vaal_stored_uses = {
 
vaal_stored_uses = {
name = i18n.args.vaal_stored_uses,
+
name = i18n.parameters.skill.vaal_stored_uses,
 
field = 'vaal_stored_uses',
 
field = 'vaal_stored_uses',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = 'Stored<br>Uses',
+
header = i18n.progression.vaal_stored_uses,
 
},
 
},
 
vaal_soul_gain_prevention_time = {
 
vaal_soul_gain_prevention_time = {
name = i18n.args.vaal_soul_gain_prevention_time,
+
name = i18n.parameters.skill.vaal_soul_gain_prevention_time,
 
field = 'vaal_soul_gain_prevention_time',
 
field = 'vaal_soul_gain_prevention_time',
 
type = 'Float',
 
type = 'Float',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = 'Soul<br>Prevention<br>Time',
+
fmt = '%i ' .. m_game.units.seconds.short_lower,
fmt = '%ss',
 
 
},
 
},
 
damage_multiplier = {
 
damage_multiplier = {
name = i18n.args.damage_multiplier,
+
name = i18n.parameters.skill.damage_multiplier,
 
field = 'damage_multiplier',
 
field = 'damage_multiplier',
 
type = 'Float',
 
type = 'Float',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = m_util.html.abbr('Damage<br>Multiplier', 'Deals x% of Base Damage'),
 
 
fmt = '%s%%',
 
fmt = '%s%%',
  +
},
  +
attack_speed_multiplier = {
  +
name = i18n.parameters.skill.attack_speed_multiplier,
  +
field = 'attack_speed_multiplier',
  +
type = 'Integer',
  +
func = h.cast.wrap(m_util.cast.number),
  +
fmt = '%s%%',
  +
},
  +
duration = {
  +
name = i18n.parameters.skill.duration,
  +
field = 'duration',
  +
type = 'Float',
  +
func = h.cast.wrap(m_util.cast.number),
  +
fmt = '%.2f ' .. m_game.units.seconds.short_lower,
 
},
 
},
 
-- from gem experience, optional
 
-- from gem experience, optional
 
experience = {
 
experience = {
name = i18n.args.experience,
+
name = i18n.parameters.skill.experience,
 
field = 'experience',
 
field = 'experience',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
hide = true,
 
 
},
 
},
 
stat_text = {
 
stat_text = {
name = i18n.args.stat_text,
+
name = i18n.parameters.skill.stat_text,
 
field = 'stat_text',
 
field = 'stat_text',
 
type = 'Text',
 
type = 'Text',
func = nil,
+
func = h.cast.wrap(m_util.cast.text),
hide = true,
 
 
},
 
},
quality_stat_text = {
+
-- Deprecated
name = i18n.args.quality_stat_text,
+
mana_cost = {
field = 'quality_stat_text',
+
name = i18n.parameters.skill.mana_cost,
type = 'Text',
+
field = 'mana_cost',
  +
type = 'Integer',
  +
func = h.cast.wrap(m_util.cast.number),
  +
deprecated = true,
  +
},
  +
}
  +
}
  +
  +
tables.skill_costs = {
  +
table = 'skill_costs',
  +
fields = {
  +
set_id = {
  +
name = nil,
  +
field = 'set_id',
  +
type = 'Integer',
 
func = nil,
 
func = nil,
hide = true,
+
},
  +
type = {
  +
name = i18n.parameters.skill.cost_type,
  +
field = 'type',
  +
type = 'String',
  +
func = function(tpl_args, frame, value)
  +
if value == nil then
  +
return nil
  +
end
  +
if m_game.constants.skill.cost_types[value] == nil then
  +
error(string.format(i18n.errors.skill.invalid_cost_type, value))
  +
end
  +
return value
  +
end,
  +
},
  +
is_reservation = {
  +
name = i18n.parameters.skill.cost_is_reservation,
  +
field = 'is_reservation',
  +
type = 'Boolean',
  +
func = h.cast.wrap(m_util.cast.boolean),
  +
default = false,
 
},
 
},
 
}
 
}
 
}
 
}
   
  +
tables.skill_level_costs = {
data.progression_display_order = {'level_requirement', 'dexterity_requirement', 'strength_requirement', 'intelligence_requirement', 'mana_multiplier', 'critical_strike_chance', 'mana_cost', 'damage_effectiveness', 'stored_uses', 'cooldown', 'vaal_souls_requirement', 'vaal_stored_uses', 'vaal_soul_gain_prevention_time', 'damage_multiplier'}
 
  +
table = 'skill_level_costs',
  +
fields = {
  +
set_id = {
  +
name = nil,
  +
field = 'set_id',
  +
type = 'Integer',
  +
func = nil,
  +
},
  +
level = {
  +
name = nil,
  +
field = 'level',
  +
type = 'Integer',
  +
func = nil,
  +
},
  +
amount = {
  +
name = i18n.parameters.skill.cost_amount,
  +
field = 'amount',
  +
type = 'Integer',
  +
func = h.cast.wrap(m_util.cast.number),
  +
},
  +
},
  +
}
   
map.skill_stats_per_level = {
+
tables.skill_stats_per_level = {
 
table = 'skill_stats_per_level',
 
table = 'skill_stats_per_level',
 
fields = {
 
fields = {
 
level = {
 
level = {
  +
name = nil,
 
field = 'level',
 
field = 'level',
 
type = 'Integer',
 
type = 'Integer',
  +
func = nil,
 
},
 
},
 
id = {
 
id = {
  +
name = i18n.parameters.skill.stat_id,
 
field = 'id',
 
field = 'id',
 
type = 'String',
 
type = 'String',
  +
func = h.cast.wrap(m_util.cast.text),
 
},
 
},
 
value = {
 
value = {
  +
name = i18n.parameters.skill.stat_value,
 
field = 'value',
 
field = 'value',
 
type = 'Integer',
 
type = 'Integer',
  +
func = h.cast.wrap(m_util.cast.number),
 
},
 
},
  +
},
is_quality_stat = {
 
  +
}
field = 'is_quality_stat',
 
  +
type = 'Boolean',
 
  +
tables.skill_quality = {
  +
table = 'skill_quality',
  +
fields = {
  +
set_id = {
  +
field = 'set_id',
  +
type = 'Integer',
  +
},
  +
weight = {
  +
field = 'weight',
  +
type = 'Integer',
  +
},
  +
stat_text = {
  +
field = 'stat_text',
  +
type = 'String',
 
},
 
},
  +
},
  +
}
  +
  +
tables.skill_quality_stats = {
  +
table = 'skill_quality_stats',
  +
fields = {
  +
set_id = {
  +
field = 'set_id',
  +
type = 'Integer',
  +
},
  +
id = {
  +
field = 'id',
  +
type = 'String',
  +
},
  +
value = {
  +
field = 'value',
  +
type = 'Integer',
  +
},
  +
},
  +
}
  +
  +
-- ----------------------------------------------------------------------------
  +
-- Data
  +
-- ----------------------------------------------------------------------------
  +
  +
data.skill_progression_table = {
  +
{
  +
field = 'level',
  +
header = i18n.progression.level,
  +
},
  +
{
  +
field = 'level_requirement',
  +
header = i18n.progression.level_requirement,
  +
},
  +
{
  +
field = 'dexterity_requirement',
  +
header = i18n.progression.dexterity_requirement,
  +
},
  +
{
  +
field = 'strength_requirement',
  +
header = i18n.progression.strength_requirement,
  +
},
  +
{
  +
field = 'intelligence_requirement',
  +
header = i18n.progression.intelligence_requirement,
  +
},
  +
{
  +
field = 'mana_multiplier',
  +
header = i18n.progression.mana_multiplier,
  +
fmt = '%s%%',
  +
},
  +
{
  +
field = 'critical_strike_chance',
  +
header = i18n.progression.critical_strike_chance,
  +
fmt = '%s%%',
  +
},
  +
{ -- Also supports deprecated method of specifying mana cost and reservation
  +
field = 'mana_cost',
  +
header = function (tpl_args, frame)
  +
if #tpl_args.skill_data.costs == 0 and tpl_args.skill_data[tables.static.fields.has_reservation_mana_cost.name] then
  +
return i18n.progression.mana_reserved
  +
end
  +
return i18n.progression.mana_cost
  +
end,
  +
fmt = function (tpl_args, frame)
  +
if #tpl_args.skill_data.costs == 0 and tpl_args.skill_data[tables.static.fields.has_percentage_mana_cost.name] then
  +
return '%s%%'
  +
end
  +
return '%s'
  +
end,
  +
},
  +
{
  +
field = 'mana_percent_cost',
  +
header = i18n.progression.mana_cost,
  +
fmt = '%s%%',
  +
},
  +
{
  +
field = 'life_cost',
  +
header = i18n.progression.life_cost,
  +
},
  +
{
  +
field = 'life_percent_cost',
  +
header = i18n.progression.life_cost,
  +
fmt = '%s%%',
  +
},
  +
{
  +
field = 'energy_shield_cost',
  +
header = i18n.progression.energy_shield_cost,
  +
},
  +
{
  +
field = 'rage_cost',
  +
header = i18n.progression.rage_cost,
  +
},
  +
{
  +
field = 'mana_reserved',
  +
header = i18n.progression.mana_reserved,
  +
},
  +
{
  +
field = 'mana_percent_reserved',
  +
header = i18n.progression.mana_reserved,
  +
fmt = '%s%%',
  +
},
  +
{
  +
field = 'life_reserved',
  +
header = i18n.progression.life_reserved,
  +
},
  +
{
  +
field = 'life_percent_reserved',
  +
header = i18n.progression.life_reserved,
  +
fmt = '%s%%',
  +
},
  +
{
  +
field = 'damage_effectiveness',
  +
header = i18n.progression.damage_effectiveness,
  +
fmt = '%s%%',
  +
},
  +
{
  +
field = 'stored_uses',
  +
header = i18n.progression.stored_uses,
  +
},
  +
{
  +
field = 'cooldown',
  +
header = i18n.progression.cooldown,
  +
fmt = '%.2f ' .. m_game.units.seconds.short_lower,
  +
},
  +
{
  +
field = 'vaal_souls_requirement',
  +
header = i18n.progression.vaal_souls_requirement,
  +
},
  +
{
  +
field = 'vaal_stored_uses',
  +
header = i18n.progression.vaal_stored_uses,
  +
},
  +
{
  +
field = 'vaal_soul_gain_prevention_time',
  +
header = i18n.progression.vaal_soul_gain_prevention_time,
  +
fmt = '%i ' .. m_game.units.seconds.short_lower,
  +
},
  +
{
  +
field = 'damage_multiplier',
  +
header = i18n.progression.damage_multiplier,
  +
fmt = '%s%%',
  +
},
  +
{
  +
field = 'duration',
  +
header = i18n.progression.duration,
  +
fmt = '%.2f ' .. m_game.units.seconds.short_lower,
  +
},
  +
{
  +
field = 'attack_speed_multiplier',
  +
header = i18n.progression.attack_speed_multiplier,
  +
fmt = '%s%%',
 
},
 
},
 
}
 
}
Line 635: Line 805:
 
{
 
{
 
header = i18n.infobox.cast_time,
 
header = i18n.infobox.cast_time,
func = h.display.factory.value{key='cast_time'},
+
func = function (tpl_args, frame)
  +
local value = tpl_args.cast_time
  +
if value then
  +
if value == 0 then
  +
return i18n.infobox.instant_cast_time
  +
end
  +
return string.format('%.2f %s', value, m_game.units.seconds.short_lower)
  +
end
  +
return value
  +
end,
 
},
 
},
 
{
 
{
 
header = i18n.infobox.item_class_restrictions,
 
header = i18n.infobox.item_class_restrictions,
 
func = function (tpl_args, frame)
 
func = function (tpl_args, frame)
if tpl_args.item_class_restrictions == nil then
+
if tpl_args.item_class_restriction == nil then
 
return
 
return
 
end
 
end
local ul = mw.html.create('ul')
+
local out = {}
for _, class in ipairs(i18n.infobox.item_class_restrictions) do
+
for _, class in ipairs(tpl_args.item_class_restriction) do
ul:tag('li')
+
out[#out+1] = string.format('[[%s]]', class)
:wikitext(string.format('[[%s]]', class))
 
 
end
 
end
return tostring(ul)
+
return table.concat(out, '<br>')
 
end,
 
end,
 
},
 
},
Line 681: Line 859:
 
},
 
},
 
{
 
{
header = i18n.infobox.mana_cost,
+
header = i18n.infobox.cost,
func = h.display.factory.range_value{key='mana_cost'},
+
func = function (tpl_args, frame)
  +
if not tpl_args.skill_costs.has_flat_cost then
  +
-- Try falling back to deprecated parameters
  +
if not tpl_args.has_reservation_mana_cost then
  +
local range = h.display.factory.range_value{key='mana_cost'}(tpl_args, frame)
  +
if range then
  +
return string.format('%s %s', range, m_game.constants.skill.cost_types.mana.long_upper)
  +
end
  +
return
  +
end
  +
return
  +
end
  +
local sets = {}
  +
for i=1, #tpl_args.skill_costs do
  +
if not tpl_args.skill_costs[i].is_reservation then -- Only get flat costs
  +
local cost_type = tpl_args.skill_costs[i].type
  +
local range = h.display.factory.range_value{key='amount', set_name='costs', set_id=i, map=tables.skill_level_costs}(tpl_args, frame)
  +
if range then
  +
local fmt
  +
if string.find(cost_type, 'percent', 1, true) then
  +
fmt = '%s%% %s'
  +
else
  +
fmt = '%s %s'
  +
end
  +
sets[#sets+1] = string.format(fmt, range, m_game.constants.skill.cost_types[cost_type].long_upper)
  +
end
  +
end
  +
end
  +
return table.concat(sets, ', ')
  +
end,
  +
},
  +
{
  +
header = i18n.infobox.reservation,
  +
func = function (tpl_args, frame)
  +
if not tpl_args.skill_costs.has_reservation_cost then
  +
-- Try falling back to deprecated parameters
  +
if tpl_args.has_reservation_mana_cost then
  +
local range = h.display.factory.range_value{key='mana_cost'}(tpl_args, frame)
  +
if range then
  +
if tpl_args.has_percentage_mana_cost then
  +
return string.format('%s%% %s', range, m_game.constants.skill.cost_types.mana.long_upper)
  +
end
  +
return string.format('%s %s', range, m_game.constants.skill.cost_types.mana.long_upper)
  +
end
  +
return
  +
end
  +
return
  +
end
  +
local sets = {}
  +
for i=1, #tpl_args.skill_costs do
  +
if tpl_args.skill_costs[i].is_reservation then -- Only get reservation costs
  +
local cost_type = tpl_args.skill_costs[i].type
  +
local range = h.display.factory.range_value{key='amount', set_name='costs', set_id=i, map=tables.skill_level_costs}(tpl_args, frame)
  +
if range then
  +
local fmt
  +
if string.find(cost_type, 'percent', 1, true) then
  +
fmt = '%s%% %s'
  +
else
  +
fmt = '%s %s'
  +
end
  +
sets[#sets+1] = string.format(fmt, range, m_game.constants.skill.cost_types[cost_type].long_upper)
  +
end
  +
end
  +
end
  +
return table.concat(sets, ', ')
  +
end,
  +
},
  +
{
  +
header = i18n.infobox.attack_speed_multiplier,
  +
func = h.display.factory.range_value{key='attack_speed_multiplier'},
  +
fmt = '%s ' .. i18n.infobox.of_base_stat,
  +
},
  +
{
  +
header = i18n.infobox.damage_multiplier,
  +
func = h.display.factory.range_value{key='damage_multiplier'},
  +
fmt = '%s ' .. i18n.infobox.of_base_stat,
 
},
 
},
 
{
 
{
Line 709: Line 962:
 
},
 
},
 
{
 
{
header = i18n.infobox.damage_multiplier,
+
header = i18n.infobox.duration,
func = h.display.factory.range_value{key='damage_multiplier'},
+
func = h.display.factory.range_value{key='duration'},
 
},
 
},
 
{
 
{
Line 725: Line 978:
   
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
  +
-- Invokable functions
-- Templates
 
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
 
local p = {}
 
local p = {}
   
p.table_skills = m_util.cargo.declare_factory{data=map.static}
+
p.table_skills = m_cargo.declare_factory{data=tables.static}
p.table_skill_levels = m_util.cargo.declare_factory{data=map.progression}
+
p.table_skill_levels = m_cargo.declare_factory{data=tables.progression}
p.table_skill_stats_per_level = m_util.cargo.declare_factory{data=map.skill_stats_per_level}
+
p.table_skill_costs = m_cargo.declare_factory{data=tables.skill_costs}
  +
p.table_skill_level_costs = m_cargo.declare_factory{data=tables.skill_level_costs}
  +
p.table_skill_stats_per_level = m_cargo.declare_factory{data=tables.skill_stats_per_level}
  +
p.table_skill_quality = m_cargo.declare_factory{data=tables.skill_quality}
  +
p.table_skill_quality_stats = m_cargo.declare_factory{data=tables.skill_quality_stats}
   
 
--
 
--
  +
-- Processes skill data from tpl_args.
-- Template:Skill
 
  +
-- Stores skill data in cargo tables.
  +
-- Attaches page to cargo tables.
 
--
 
--
function p.skill(frame, tpl_args)
+
function p._skill(tpl_args, frame)
if tpl_args == nil then
 
tpl_args = getArgs(frame, {
 
parentFirst = true
 
})
 
end
 
 
frame = m_util.misc.get_frame(frame)
 
frame = m_util.misc.get_frame(frame)
  +
tpl_args = tpl_args or {}
 
  +
tpl_args._flags = tpl_args._flags or {}
--
 
-- Args
 
--
 
local properties
 
 
 
tpl_args.skill_levels = {
 
tpl_args.skill_levels = {
 
[0] = {},
 
[0] = {},
 
}
 
}
 
 
-- Handle level progression
+
-- Quality
  +
tpl_args.skill_quality = {}
 
local i = 0
 
local i = 0
repeat
+
repeat
 
i = i + 1
 
i = i + 1
local prefix = i18n.args.prefix_level .. i
+
local prefix = string.format('quality_type%s', i)
local level = m_util.cast.boolean(tpl_args[prefix])
+
local q = {
  +
_table = tables.skill_quality.table,
if level == true then
 
-- Don't need this anymore
+
set_id = i,
tpl_args[prefix] = nil
+
weight = tonumber(tpl_args[string.format('%s_weight', prefix)]),
  +
stat_text = tpl_args[string.format('%s_stat_text', prefix)],
tpl_args.skill_levels[i] = {}
 
prefix = prefix .. '_'
+
}
+
if q.stat_text then
if tpl_args[prefix .. i18n.args.experience] ~= nil then
+
tpl_args.skill_quality[#tpl_args.skill_quality+1] = q
tpl_args.max_level = i
+
m_cargo.store(frame, q)
end
 
 
 
properties = {
+
q.stats = {}
_table = map.progression.table,
+
q._table = nil
[map.progression.fields.level.field] = i
+
local j = 0
}
+
repeat
  +
j = j + 1
h.map_to_arg(tpl_args, frame, properties, prefix, map.progression, i)
 
  +
local stat_prefix = string.format('%s_stat%s', prefix, j)
m_util.cargo.store(frame, properties)
 
+
local s = {
  +
_table = tables.skill_quality_stats.table,
h.stats(tpl_args, frame, prefix, i)
 
  +
set_id = i,
  +
id = tpl_args[string.format('%s_id', stat_prefix)],
  +
value = tonumber(tpl_args[string.format('%s_value', stat_prefix)]),
  +
}
  +
if s.id and s.value then
  +
q.stats[#q.stats+1] = s
  +
m_cargo.store(frame, s)
  +
end
  +
  +
s._table = nil
  +
until s.id == nil or s.value == nil
 
end
 
end
until level ~= true
+
until q.stat_text == nil
  +
if #tpl_args.skill_quality > 1 then
-- If no experience is given, assume this is a non skill gem skill.
 
  +
-- Gem has alternative qualtiy
tpl_args.max_level = tpl_args.max_level or (i - 1)
 
  +
tpl_args._flags.is_alt_quality_gem = true
  +
end
  +
  +
-- Costs
  +
for i=1, math.huge do -- repeat until no more cost sets are found
  +
local prefix = string.format('%s%d_', i18n.parameters.skill.skill_cost, i)
  +
if tpl_args[prefix .. tables.skill_costs.fields.type.field] == nil then
  +
break
  +
end
  +
local properties = {
  +
_table = tables.skill_costs.table,
  +
[tables.skill_costs.fields.set_id.field] = i,
  +
}
  +
h.map_to_arg(tpl_args, frame, properties, prefix, tables.skill_costs, nil, 'skill_costs', i)
  +
if properties.is_reservation then
  +
tpl_args.skill_costs.has_reservation_cost = true
  +
else
  +
tpl_args.skill_costs.has_flat_cost = true
  +
end
  +
tpl_args.skill_costs[i] = {
  +
set_id = properties.set_id,
  +
type = properties.type,
  +
is_reservation = properties.is_reservation,
  +
}
  +
if not tpl_args.test then
  +
m_cargo.store(frame, properties)
  +
end
  +
end
  +
  +
-- Handle level progression
  +
local level_count = 0
  +
for i=1, math.huge do -- repeat until no more levels are found
  +
local prefix = i18n.parameters.skill.level .. i
  +
local level = m_util.cast.boolean(tpl_args[prefix])
  +
if not level then
  +
break
  +
end
  +
tpl_args.skill_levels[i] = {}
  +
prefix = prefix .. '_'
  +
level_count = i
  +
if tpl_args[prefix .. i18n.parameters.skill.experience] ~= nil then
  +
-- For skill gems, max level is the highest level with experience.
  +
tpl_args.max_level = i
  +
end
  +
local properties = {
  +
_table = tables.progression.table,
  +
[tables.progression.fields.level.field] = i
  +
}
  +
h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, i)
  +
if not tpl_args.test then
  +
m_cargo.store(frame, properties)
  +
end
  +
h.costs(tpl_args, frame, prefix, i)
  +
h.stats(tpl_args, frame, prefix, i)
  +
end
  +
tpl_args.max_level = tpl_args.max_level or level_count
  +
 
-- handle static progression
 
-- handle static progression
  +
local prefix = i18n.parameters.skill.static .. '_'
properties = {
 
  +
do
_table = map.progression.table,
 
[map.progression.fields.level.field] = 0
+
local properties = {
  +
_table = tables.progression.table,
}
 
  +
[tables.progression.fields.level.field] = 0
h.map_to_arg(tpl_args, frame, properties, i18n.args.prefix_static, map.progression, 0)
 
  +
}
m_util.cargo.store(frame, properties)
 
  +
h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, 0)
  +
if not tpl_args.test then
  +
m_cargo.store(frame, properties)
  +
end
  +
end
 
 
 
-- Handle static arguments
 
-- Handle static arguments
properties = {
+
local properties = {
_table = map.static.table,
+
_table = tables.static.table,
  +
[tables.static.fields.max_level.field] = tpl_args.max_level
 
[map.static.fields.max_level.field] = tpl_args.max_level
 
 
}
 
}
  +
h.map_to_arg(tpl_args, frame, properties, '', tables.static)
 
h.map_to_arg(tpl_args, frame, properties, '', map.static)
+
h.costs(tpl_args, frame, prefix, 0)
h.stats(tpl_args, frame, i18n.args.prefix_static, 0)
+
h.stats(tpl_args, frame, prefix, 0)
  +
 
  +
-- Build infobox
 
 
--
 
-- Infobox progressing
 
--
 
 
local infobox = mw.html.create('span')
 
local infobox = mw.html.create('span')
 
infobox:attr('class', 'skill-box')
 
infobox:attr('class', 'skill-box')
 
-- tablular sections
 
 
local tbl = infobox:tag('table')
 
local tbl = infobox:tag('table')
 
tbl:attr('class', 'wikitable skill-box-table')
 
tbl:attr('class', 'wikitable skill-box-table')
 
for _, infobox_data in ipairs(data.infobox_table) do
 
for _, infobox_data in ipairs(data.infobox_table) do
 
local display = infobox_data.func(tpl_args, frame)
 
local display = infobox_data.func(tpl_args, frame)
  +
if display ~= nil and infobox_data.fmt ~= nil then
  +
if type(infobox_data.fmt) == 'string' then
  +
display = string.format(infobox_data.fmt, display)
  +
elseif type(infobox_data.fmt) == 'function' then
  +
display = string.format(infobox_data.fmt(tpl_args, frame) or '%s', display)
  +
end
  +
end
 
if display then
 
if display then
 
local tr = tbl:tag('tr')
 
local tr = tbl:tag('tr')
 
if infobox_data.header then
 
if infobox_data.header then
  +
local header_text
  +
if type(infobox_data.header) == 'function' then
  +
header_text = infobox_data.header(tpl_args, frame)
  +
else
  +
header_text = infobox_data.header
  +
end
 
tr
 
tr
 
:tag('th')
 
:tag('th')
:wikitext(infobox_data.header)
+
:wikitext(header_text)
 
:done()
 
:done()
 
end
 
end
Line 828: Line 1,157:
 
end
 
end
 
end
 
end
 
 
infobox = tostring(infobox)
 
infobox = tostring(infobox)
   
 
--
 
 
-- Store data
 
-- Store data
  +
properties[tables.static.fields.html.field] = infobox
--
 
  +
if not tpl_args.test then
properties[map.static.fields.html.field] = infobox
 
m_util.cargo.store(frame, properties)
+
m_cargo.store(frame, properties)
  +
end
  +
  +
-- Attach tables
  +
if not tpl_args.test then
  +
local attach_tables = {
  +
tables.static.table,
  +
tables.progression.table,
  +
}
  +
if #tpl_args.skill_quality > 0 then
  +
attach_tables[#attach_tables+1] = tables.skill_quality.table
  +
attach_tables[#attach_tables+1] = tables.skill_quality_stats.table
  +
end
  +
if #tpl_args.skill_costs > 0 then
  +
attach_tables[#attach_tables+1] = tables.skill_costs.table
  +
attach_tables[#attach_tables+1] = tables.skill_level_costs.table
  +
end
  +
if tpl_args.skill_levels.has_stats then
  +
attach_tables[#attach_tables+1] = tables.skill_stats_per_level.table
  +
end
  +
for _, table_name in ipairs(attach_tables) do
  +
frame:expandTemplate{
  +
title = string.format(i18n.templates.cargo_attach, table_name),
  +
args = {}
  +
}
  +
end
  +
end
  +
  +
-- Log when testing
  +
if tpl_args.test then
  +
mw.logObject(tpl_args)
  +
end
  +
  +
return infobox
  +
end
  +
  +
--
  +
-- Template:Skill
  +
--
  +
function p.skill(frame)
  +
--[[
  +
Display skill infobox
 
 
--
+
Examples
--
+
--------
  +
=p.skill{gem_description='Icy bolts rain down over the targeted area.', active_skill_name='Icestorm', skill_id='IcestormUniqueStaff12', cast_time=0.75, required_level=1, static_mana_cost=22, static_critical_strike_chance=6, static_damage_effectiveness=30, static_damage_multiplier=100, static_stat1_id='spell_minimum_base_cold_damage_+_per_10_intelligence', static_stat1_value=1, static_stat2_id='spell_maximum_base_cold_damage_+_per_10_intelligence', static_stat2_value=3, static_stat3_id='base_skill_effect_duration', static_stat3_value=1500, static_stat4_id='fire_storm_fireball_delay_ms', static_stat4_value=100, static_stat5_id='skill_effect_duration_per_100_int', static_stat5_value=150, static_stat6_id='skill_override_pvp_scaling_time_ms', static_stat6_value=450, static_stat7_id='firestorm_drop_ground_ice_duration_ms', static_stat7_value=500, static_stat8_id='skill_art_variation', static_stat8_value=4, static_stat9_id='base_skill_show_average_damage_instead_of_dps', static_stat9_value=1, static_stat10_id='is_area_damage', static_stat10_value=1, stat_text='Deals 1 to 3 base Cold Damage per 10 Intelligence<br>Base duration is 1.5 seconds<br>One impact every 0.1 seconds<br>0.15 seconds additional Base Duration per 100 Intelligence', quality_stat_text = nil, level1=true, level1_level_requirement=1}
--
 
  +
  +
]]
  +
  +
local tpl_args = getArgs(frame, {
  +
parentFirst = true
  +
})
  +
frame = m_util.misc.get_frame(frame)
  +
  +
-- Handle skill data and get infobox
  +
local infobox = p._skill(tpl_args, frame)
  +
  +
-- Container
 
local container = mw.html.create('span')
 
local container = mw.html.create('span')
container:attr('class', 'skill-box-page-container')
+
container
  +
:attr('class', 'skill-box-page-container')
container:wikitext(infobox)
 
  +
:wikitext(infobox)
 
if tpl_args.skill_screenshot then
 
if tpl_args.skill_screenshot then
container:wikitext(string.format('[[%s]]', tpl_args.skill_screenshot))
+
container
  +
:wikitext(string.format('[[%s]]', tpl_args.skill_screenshot))
  +
end
  +
  +
-- Generic messages on the page:
  +
out = {}
  +
if mw.ustring.find(tpl_args.skill_id, '_') then
  +
out[#out+1] = frame:expandTemplate{
  +
title = i18n.templates.incorrect_title,
  +
args = {title=tpl_args.skill_id}
  +
} .. '\n\n\n'
  +
end
  +
if tpl_args.active_skill_name then
  +
out[#out+1] = string.format(
  +
i18n.messages.intro_named_id,
  +
tpl_args.skill_id,
  +
tpl_args.active_skill_name
  +
)
  +
else
  +
out[#out+1] = string.format(
  +
i18n.messages.intro_unnamed_id,
  +
tpl_args.skill_id
  +
)
  +
end
  +
  +
-- Categories
  +
local cats = {i18n.categories.skill_data}
  +
if tpl_args.has_deprecated_parameters then
  +
cats[#cats+1] = i18n.categories.deprecated_parameters
 
end
 
end
 
 
return tostring(container) .. m_util.misc.add_category({'Skill data'})
+
return tostring(container) .. m_util.misc.add_category(cats) .. '\n' .. table.concat(out)
 
end
 
end
   
 
function p.progression(frame)
 
function p.progression(frame)
  +
--[[
  +
Displays the level progression for the skill gem.
  +
  +
Examples
  +
--------
  +
= p.progression{page='Reave'}
  +
]]
  +
 
local tpl_args = getArgs(frame, {
 
local tpl_args = getArgs(frame, {
 
parentFirst = true
 
parentFirst = true
Line 857: Line 1,273:
 
frame = m_util.misc.get_frame(frame)
 
frame = m_util.misc.get_frame(frame)
 
 
  +
-- Parse column arguments:
--
 
 
tpl_args.stat_format = {}
 
tpl_args.stat_format = {}
local prefix
+
local param_keys = {
  +
i18n.parameters.progression.header,
for i=1, 9 do
 
prefix = 'c' .. i .. '_'
+
i18n.parameters.progression.abbr,
  +
i18n.parameters.progression.pattern_extract,
local statfmt = {
 
  +
i18n.parameters.progression.pattern_value,
header = tpl_args[prefix .. 'header'],
 
  +
}
abbr = tpl_args[prefix .. 'abbr'],
 
  +
for i=1, math.huge do -- repeat until no more columns are found
pattern_extract = tpl_args[prefix .. 'pattern_extract'],
 
  +
local prefix = string.format('%s%d_', i18n.parameters.progression.column, i)
pattern_value = tpl_args[prefix .. 'pattern_value'],
 
counter = 0,
+
if tpl_args[prefix .. param_keys[1]] == nil then
}
 
if m_util.table.has_all_value(statfmt, {'header', 'abbr', 'pattern_extract', 'pattern_value'}) then
 
 
break
 
break
 
end
 
end
+
local statfmt = {counter = 0}
  +
for _, key in ipairs(param_keys) do
if m_util.table.has_one_value(statfmt, {'header', 'abbr', 'pattern_extract', 'pattern_value'}) then
 
  +
local arg = prefix .. key
error(string.format('All formatting keys must be specified for index "%s"', i))
 
  +
if tpl_args[arg] == nil then
  +
error(string.format(i18n.errors.progression.argument_unspecified, arg))
  +
end
  +
statfmt[key] = tpl_args[arg]
 
end
 
end
 
 
statfmt.header = m_util.html.abbr(statfmt.abbr, statfmt.header)
 
statfmt.header = m_util.html.abbr(statfmt.abbr, statfmt.header)
 
statfmt.abbr = nil
 
statfmt.abbr = nil
Line 882: Line 1,299:
 
end
 
end
 
 
  +
-- Query skill data
 
local result
+
local results = {}
local query = {
 
groupBy = 'skill._pageID',
 
}
 
 
local skill_data
 
local skill_data
 
 
local fields = {
 
local fields = {
'skill._pageName',
+
'_pageName',
'skill.has_reservation_mana_cost',
+
tables.static.fields.has_reservation_mana_cost.name,
  +
tables.static.fields.has_percentage_mana_cost.name,
 
}
 
}
+
local query = {
  +
groupBy = '_pageID',
if tpl_args.skill_id then
 
  +
}
query.where = string.format('skill.skill_id="%s"', tpl_args.skill_id)
 
  +
if tpl_args.skill_id then -- Query by skill id
result = m_util.cargo.query({'skill'}, fields, query)
 
  +
query.where = string.format('skill_id="%s"', tpl_args.skill_id)
if #result == 0 then
 
  +
results = m_cargo.query({tables.static.table}, fields, query)
error('Couldn\'t find a page for the specified skill id')
 
  +
if #results == 0 then
  +
error(string.format(i18n.errors.progression.no_results_for_skill_id, tpl_args.skill_id))
 
end
 
end
skill_data = result[1]
+
else -- Query by page name
  +
page = tpl_args.page or mw.title.getCurrentTitle().prefixedText
else
 
  +
query.where = string.format('_pageName="%s"', page)
if tpl_args.page then
 
  +
results = m_cargo.query({tables.static.table}, fields, query)
page = tpl_args.page
 
else
+
if #results == 0 then
  +
error(string.format(i18n.errors.progression.no_results_for_skill_page, page))
page = mw.title.getCurrentTitle().prefixedText
 
 
end
 
end
query.where = string.format('skill._pageName="%s"', page)
 
 
result = m_util.cargo.query({'skill'}, fields, query)
 
if #result == 0 then
 
error('Couldn\'t find the queried data on the skill page')
 
end
 
 
skill_data = result[1]
 
 
end
 
end
  +
skill_data = results[1]
 
skill_data["skill.has_reservation_mana_cost"] = h.cast.wrap(m_util.cast.boolean)(skill_data["skill.has_reservation_mana_cost"])
+
skill_data[tables.static.fields.has_reservation_mana_cost.name] = m_util.cast.boolean(skill_data[tables.static.fields.has_reservation_mana_cost.name])
  +
skill_data[tables.static.fields.has_percentage_mana_cost.name] = m_util.cast.boolean(skill_data[tables.static.fields.has_percentage_mana_cost.name])
 
  +
tpl_args.skill_data = skill_data
query.where=string.format('skill_levels._pageName="%s"', skill_data['skill._pageName'])
 
  +
  +
-- Query progression data
 
fields = {}
 
fields = {}
for _, pdata in pairs(map.progression.fields) do
+
for _, fmap in pairs(tables.progression.fields) do
fields[#fields+1] = string.format('skill_levels.%s', pdata.field)
+
fields[#fields+1] = fmap.field
 
end
 
end
+
query = {
result = m_util.cargo.query(
+
where = string.format(
{'skill_levels'},
+
'_pageName="%s" AND %s > 0',
fields,
+
skill_data['_pageName'],
  +
tables.progression.fields.level.field
{
 
  +
),
where=string.format('skill_levels._pageName="%s" AND skill_levels.level > 0', skill_data['skill._pageName']),
 
  +
groupBy = string.format(
groupBy='skill_levels._pageID, skill_levels.level',
 
orderBy='skill_levels.level ASC',
+
'_pageID, %s',
  +
tables.progression.fields.level.field
  +
),
  +
orderBy = string.format(
  +
'%s ASC',
  +
tables.progression.fields.level.field
  +
),
  +
}
  +
results = m_cargo.query({tables.progression.table}, fields, query)
  +
if #results == 0 then
  +
error(i18n.errors.progression.missing_level_data)
  +
end
  +
skill_data.levels = results
  +
  +
-- Query cost data
  +
fields = {}
  +
for _, fmap in pairs(tables.skill_costs.fields) do
  +
fields[#fields+1] = fmap.field
  +
end
  +
query = {
  +
where = string.format(
  +
'_pageName="%s"',
  +
skill_data['_pageName']
  +
),
  +
groupBy = string.format(
  +
'_pageID, %s',
  +
tables.skill_costs.fields.set_id.field
  +
),
  +
orderBy = string.format(
  +
'%s ASC',
  +
tables.skill_costs.fields.set_id.field
  +
),
  +
}
  +
results = m_cargo.query({tables.skill_costs.table}, fields, query)
  +
skill_data.costs = results
  +
if #results > 0 then -- If skill has costs, query cost data by levels
  +
fields = {}
  +
for _, fmap in pairs(tables.skill_level_costs.fields) do
  +
fields[#fields+1] = fmap.field
  +
end
  +
query = {
  +
where = string.format(
  +
'_pageName="%s" AND %s > 0',
  +
skill_data['_pageName'],
  +
tables.skill_level_costs.fields.level.field
  +
),
  +
groupBy = string.format(
  +
'_pageID, %s, %s',
  +
tables.skill_level_costs.fields.set_id.field,
  +
tables.skill_level_costs.fields.level.field
  +
),
  +
orderBy = string.format(
  +
'%s ASC, %s ASC',
  +
tables.skill_level_costs.fields.set_id.field,
  +
tables.skill_level_costs.fields.level.field
  +
),
 
}
 
}
  +
results = m_cargo.query({tables.skill_level_costs.table}, fields, query)
)
 
  +
skill_data.costs_by_level = results
 
  +
if #result == 0 then
 
error('No gem level progression data found')
+
-- Interpolate cost data into level data
  +
local column
  +
for _,cdata in ipairs(skill_data.costs) do
  +
if m_util.cast.boolean(cdata[tables.skill_costs.fields.is_reservation.field]) then
  +
column = string.format('%s_reserved', cdata[tables.skill_costs.fields.type.field])
  +
else
  +
column = string.format('%s_cost', cdata[tables.skill_costs.fields.type.field])
  +
end
  +
for _,ldata in ipairs(skill_data.levels) do
  +
for _,rdata in ipairs(skill_data.costs_by_level) do
  +
if rdata[tables.skill_level_costs.fields.set_id.field] == cdata[tables.skill_costs.fields.set_id.field] and rdata[tables.skill_level_costs.fields.level.field] == ldata[tables.progression.fields.level.field] then
  +
ldata[column] = rdata[tables.skill_level_costs.fields.amount.field]
  +
break
  +
end
  +
end
  +
end
  +
end
 
end
 
end
 
 
  +
-- Set up html table headers
 
headers = {}
 
headers = {}
for i, row in ipairs(result) do
+
for _, row in ipairs(skill_data.levels) do
 
for k, v in pairs(row) do
 
for k, v in pairs(row) do
 
headers[k] = true
 
headers[k] = true
 
end
 
end
 
end
 
end
 
 
local tbl = mw.html.create('table')
 
local tbl = mw.html.create('table')
  +
tbl
tbl:attr('class', 'wikitable skill-progression-table')
 
  +
:attr('class', 'wikitable responsive-table skill-progression-table')
 
 
local head = tbl:tag('tr')
 
local head = tbl:tag('tr')
  +
for _, tmap in pairs(data.skill_progression_table) do
head
 
:tag('th')
+
if headers[tmap.field] then
  +
local text = type(tmap.header) == 'function' and tmap.header(tpl_args, frame) or tmap.header
:wikitext('Level')
 
:done()
 
 
for _, key in ipairs(data.progression_display_order) do
 
local pdata = map.progression.fields[key]
 
-- TODO should be nil?
 
if pdata.hide == nil and headers['skill_levels.' .. pdata.field] then
 
local text
 
if type(pdata.header) == 'function' then
 
text = pdata.header(skill_data)
 
else
 
text = pdata.header
 
end
 
 
head
 
head
 
:tag('th')
 
:tag('th')
Line 971: Line 1,440:
 
end
 
end
 
end
 
end
 
 
for _, statfmt in ipairs(tpl_args.stat_format) do
 
for _, statfmt in ipairs(tpl_args.stat_format) do
 
head
 
head
Line 978: Line 1,446:
 
:done()
 
:done()
 
end
 
end
  +
if headers[tables.progression.fields.experience.field] then
 
if headers['skill_levels.experience'] then
 
 
head
 
head
 
:tag('th')
 
:tag('th')
:wikitext(m_util.html.abbr('Exp.', 'Experience Needed to Level Up'))
+
:wikitext(i18n.progression.experience)
 
:done()
 
:done()
 
:tag('th')
 
:tag('th')
:wikitext(m_util.html.abbr('Total Exp.', 'Total experience needed'))
+
:wikitext(i18n.progression.total_experience)
 
:done()
 
:done()
 
end
 
end
  +
 
  +
-- Table rows
 
local tblrow
 
local tblrow
 
local lastexp = 0
 
local lastexp = 0
 
local experience
 
local experience
  +
for _, row in ipairs(skill_data.levels) do
 
for i, row in ipairs(result) do
 
 
tblrow = tbl:tag('tr')
 
tblrow = tbl:tag('tr')
  +
for _, tmap in pairs(data.skill_progression_table) do
tblrow
 
:tag('th')
+
if headers[tmap.field] then
:wikitext(row['skill_levels.level'])
+
h.int_value_or_na(tpl_args, frame, tblrow, row[tmap.field], tmap)
:done()
 
 
for _, key in ipairs(data.progression_display_order) do
 
local pdata = map.progression.fields[key]
 
if pdata.hide == nil and headers['skill_levels.' .. pdata.field] then
 
h.int_value_or_na(tpl_args, frame, tblrow, row['skill_levels.' .. pdata.field], pdata)
 
 
end
 
end
 
end
 
end
 
 
 
-- stats
 
-- stats
if row['skill_levels.stat_text'] then
+
local stats = {}
stats = m_util.string.split(row['skill_levels.stat_text'], '<br>')
+
if row[tables.progression.fields.stat_text.field] then
  +
stats = m_util.string.split(
else
 
stats = {}
+
row[tables.progression.fields.stat_text.field],
  +
'<br>'
  +
)
 
end
 
end
 
for _, statfmt in ipairs(tpl_args.stat_format) do
 
for _, statfmt in ipairs(tpl_args.stat_format) do
Line 1,018: Line 1,481:
 
match = {string.match(stat, statfmt.pattern_extract)}
 
match = {string.match(stat, statfmt.pattern_extract)}
 
if #match > 0 then
 
if #match > 0 then
-- TODO maybe remove stat here to avoid testing against in future loops
+
-- TODO maybe remove stat here to avoid testing
  +
-- against in future loops
 
break
 
break
 
end
 
end
 
end
 
end
 
if #match == 0 then
 
if #match == 0 then
h.na(tblrow)
+
tblrow:node(m_util.html.td.na())
 
else
 
else
-- used to find broken progression (i.e. due to game updates)
+
-- used to find broken progression due to game updates
  +
-- for example:
 
statfmt.counter = statfmt.counter + 1
 
statfmt.counter = statfmt.counter + 1
 
tblrow
 
tblrow
 
:tag('td')
 
:tag('td')
:wikitext(string.format(statfmt.pattern_value, match[1], match[2], match[3], match[4], match[5]))
+
:wikitext(string.format(
  +
statfmt.pattern_value,
  +
match[1],
  +
match[2],
  +
match[3],
  +
match[4],
  +
match[5]
  +
)
  +
)
 
:done()
 
:done()
 
end
 
end
Line 1,036: Line 1,509:
 
-- TODO: Quality stats, afaik no gems use this atm
 
-- TODO: Quality stats, afaik no gems use this atm
 
 
if headers['skill_levels.experience'] then
+
if headers[tables.progression.fields.experience.field] then
experience = tonumber(row['skill_levels.experience'])
+
experience = tonumber(row[tables.progression.fields.experience.field])
 
if experience ~= nil then
 
if experience ~= nil then
 
h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
 
h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
 
 
lastexp = experience
 
lastexp = experience
 
else
 
else
h.na(tblrow)
+
tblrow:node(m_util.html.td.na())
 
end
 
end
 
h.int_value_or_na(tpl_args, frame, tblrow, experience, {})
 
h.int_value_or_na(tpl_args, frame, tblrow, experience, {})
Line 1,049: Line 1,521:
 
end
 
end
 
 
local empty = ''
+
local cats = {}
 
 
for _, statfmt in ipairs(tpl_args.stat_format) do
 
for _, statfmt in ipairs(tpl_args.stat_format) do
 
if statfmt.counter == 0 then
 
if statfmt.counter == 0 then
  +
cats = i18n.categories.broken_progression_table
empty = '[[Category:Pages with broken skill progression tables]]'
 
 
break
 
break
 
end
 
end
 
end
 
end
 
 
return empty .. tostring(tbl)
+
return tostring(tbl) .. m_util.misc.add_category(cats)
end
 
 
function p.map(arg)
 
for key, data in pairs(map[arg].fields) do
 
mw.logObject(key)
 
end
 
end
 
 
-- ----------------------------------------------------------------------------
 
-- Debug
 
-- ----------------------------------------------------------------------------
 
 
p._debug = {}
 
function p._debug.order(frame)
 
for _, mapping in ipairs({'static', 'progression'}) do
 
for _, key in ipairs(map[mapping].order) do
 
if map[mapping].fields[key] == nil then
 
mw.logObject(string.format('Missing key in %s.fields: %s', mapping, key))
 
end
 
end
 
for key, _ in pairs(map[mapping].fields) do
 
local missing = true
 
for _, order_key in ipairs(map[mapping].order) do
 
if order_key == key then
 
missing = false
 
break
 
end
 
end
 
if missing then
 
mw.logObject(string.format('Missing key in %s.order: %s', mapping, key))
 
end
 
end
 
end
 
 
end
 
end
   

Revision as of 21:52, 26 May 2021

Template info icon Module documentation[view] [edit] [history] [purge]

Overview

Module for handling skills with semantic media wiki support

Skill templates

Module:Item2

All templates defined in Module:Skill:

Module:Skill link

All templates defined in Module:Skill link:

-------------------------------------------------------------------------------
-- 
--                              Module:Skill
-- 
-- This module implements Template:Skill and Template:Skill progression
-------------------------------------------------------------------------------

local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')

local m_game = mw.loadData('Module:Game')

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Skill/config')

local mwlanguage = mw.language.getContentLanguage()

local i18n = cfg.i18n
local tables = {}
local data = {}

-- ----------------------------------------------------------------------------
-- Helper functions 
-- ----------------------------------------------------------------------------
local h = {}

function h.map_to_arg(tpl_args, frame, properties, prefix_in, map, level, set_name, set_id)
    if map.fields then
        for key, row in pairs(map.fields) do
            if row.name then
                local val = tpl_args[prefix_in .. row.name]
                if row.func ~= nil then
                    val = row.func(tpl_args, frame, val)
                end
                if val == nil and row.default ~= nil then
                    val = row.default
                end
                if val ~= nil then
                    if level ~= nil then
                        if set_name then
                            tpl_args.skill_levels[level][set_name] = tpl_args.skill_levels[level][set_name] or {}
                            tpl_args.skill_levels[level][set_name][set_id] = tpl_args.skill_levels[level][set_name][set_id] or {}
                            tpl_args.skill_levels[level][set_name][set_id][key] = val
                        else
                            tpl_args.skill_levels[level][key] = val
                        end

                        -- Nuke variables since they're remapped to skill_levels
                        tpl_args[prefix_in .. row.name] = nil
                    else
                        if set_name then
                            tpl_args[set_name] = tpl_args[set_name] or {}
                            tpl_args[set_name][set_id] = tpl_args[set_name][set_id] or {}
                            tpl_args[set_name][set_id][key] = val

                            -- Nuke variables since they're remapped to [set_name]
                            tpl_args[prefix_in .. row.name] = nil
                        else
                            tpl_args[key] = val
                        end
                    end
                    properties[row.field] = val

                    -- Deprecated parameters
                    if val and row.deprecated then
                        tpl_args.has_deprecated_parameters = true
                        if tpl_args.test then -- Log when testing
                            tpl_args.deprecated_parameters = tpl_args.deprecated_parameters or {}
                            tpl_args.deprecated_parameters[#tpl_args.deprecated_parameters+1] = {row.name, val}
                        end
                    end
                end
            end
        end
    end
end

function h.costs(tpl_args, frame, prefix_in, level)
    tpl_args.skill_costs = tpl_args.skill_costs or {}
    for i=1, #tpl_args.skill_costs do
        local cost_prefix = string.format('%s%s%d_', prefix_in, i18n.parameters.skill.cost, i) -- level<level>_cost<i>_
        local cost = {
            amount = tpl_args[cost_prefix .. tables.skill_level_costs.fields.amount.name], --level<level>_cost<i>_amount
        }
        if cost.amount ~= nil then
            local properties = {
                _table = tables.skill_level_costs.table,
                [tables.skill_level_costs.fields.set_id.field] = i,
                [tables.skill_level_costs.fields.level.field] = level,
            }
            h.map_to_arg(tpl_args, frame, properties, cost_prefix, tables.skill_level_costs, level, 'costs', i)
            if not tpl_args.test then
                m_cargo.store(frame, properties)
            end
        end
    end
end

function h.stats(tpl_args, frame, prefix_in, level)
    for i=1, cfg.max_stats_per_level do
        local stat_prefix = string.format('%s%s%d_', prefix_in, i18n.parameters.skill.stat, i) -- level<level>_stat<i>_
        local stat = {
            id = tpl_args[stat_prefix .. tables.skill_stats_per_level.fields.id.name], --level<level>_stat<i>_id
            value = tpl_args[stat_prefix .. tables.skill_stats_per_level.fields.value.name], --level<level>_stat<i>_value
        }
        if stat.id ~= nil and stat.value ~= nil then
            local properties = {
                _table = tables.skill_stats_per_level.table,
                [tables.skill_stats_per_level.fields.level.field] = level,
            }
            h.map_to_arg(tpl_args, frame, properties, stat_prefix, tables.skill_stats_per_level, level, 'stats', i)
            tpl_args.skill_levels.has_stats = true
            if not tpl_args.test then
                m_cargo.store(frame, properties)
            end
        end
    end
end

function h.int_value_or_na(tpl_args, frame, tblrow, value, tmap)
    value = tonumber(value)
    if value == nil then
        tblrow:node(m_util.html.td.na())
    else
        -- value = mwlanguage:formatNum(value) -- Removed for now. lang:formatNum() returns a string, which causes issues for formatting
        if tmap.fmt ~= nil then
            if type(tmap.fmt) == 'string' then
                value = string.format(tmap.fmt, value)
            elseif type(tmap.fmt) == 'function' then
                value = string.format(tmap.fmt(tpl_args, frame) or '%s', value)
            end
        end
        tblrow
            :tag('td')
                :wikitext(value)
                :done()
    end
end

h.cast = {}
function h.cast.wrap (f)
    return function(tpl_args, frame, value)
        if value == nil then
            return nil
        else
            return f(value)
        end
    end
end

h.display = {}
h.display.factory = {}
function h.display.factory.value(args)
    return function (tpl_args, frame)
        args.fmt = args.fmt or tables.static.fields[args.key].fmt
        local value = tpl_args[args.key]
        if args.fmt and value then
            return string.format(args.fmt, value)
        else
            return value
        end
    end
end

function h.display.factory.range_value(args)
    return function (tpl_args, frame)
        local value = {}
        if args.set_name and args.set_id then
            -- Guard against index errors
            tpl_args.skill_levels[0][args.set_name] = tpl_args.skill_levels[0][args.set_name] or {}
            tpl_args.skill_levels[0][args.set_name][args.set_id] = tpl_args.skill_levels[0][args.set_name][args.set_id] or {}
            tpl_args.skill_levels[1][args.set_name] = tpl_args.skill_levels[1][args.set_name] or {}
            tpl_args.skill_levels[1][args.set_name][args.set_id] = tpl_args.skill_levels[1][args.set_name][args.set_id] or {}
            tpl_args.skill_levels[tpl_args.max_level][args.set_name] = tpl_args.skill_levels[tpl_args.max_level][args.set_name] or {}
            tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id] = tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id] or {}

            value.min = tpl_args.skill_levels[0][args.set_name][args.set_id][args.key] or tpl_args.skill_levels[1][args.set_name][args.set_id][args.key]
            value.max = tpl_args.skill_levels[0][args.set_name][args.set_id][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id][args.key]
        else
            value.min = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[1][args.key]
            value.max = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.key]
        end

        -- property not set for this skill
        if value.min == nil or value.max == nil then
            return
        end

        local map = args.map or tables.progression
        return m_util.html.format_value(tpl_args, frame, value, {
            fmt=args.fmt or map.fields[args.key].fmt,
            color=false,
        })
    end
end

function h.display.factory.radius(args)
    return function (tpl_args, frame)
        local radius = tpl_args['radius' .. args.key]
        if radius == nil then
            return
        end
        local description = tpl_args[string.format('radius%s_description', args.key)]
        if description then
            return m_util.html.abbr(radius, description)
        else
            return radius
        end
    end
end

-- ----------------------------------------------------------------------------
-- Cargo tables
-- ----------------------------------------------------------------------------

tables.static = {
    table = 'skill',
    fields = {
        -- GrantedEffects.dat
        skill_id = {
            name = i18n.parameters.skill.skill_id,
            field = 'skill_id',
            type = 'String',
            func = nil,
        },
        -- Active Skills.dat
        cast_time = {
            name = i18n.parameters.skill.cast_time,
            field = 'cast_time',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%.2f ' .. m_game.units.seconds.short_lower,
        },
        gem_description = {
            name = i18n.parameters.skill.gem_description,
            field = 'description',
            type = 'Text',
            func = nil,
        },
        active_skill_name = {
            name = i18n.parameters.skill.active_skill_name,
            field = 'active_skill_name',
            type = 'String',
            func = nil,
        },
        skill_icon = {
            name = i18n.parameters.skill.skill_icon,
            field = 'skill_icon',
            type = 'Page',
            func = function(tpl_args, frame)
                if tpl_args.active_skill_name then
                    return string.format(i18n.files.skill_icon, tpl_args.active_skill_name)
                end
            end,
        },
        item_class_id_restriction = {
            name = i18n.parameters.skill.item_class_id_restriction,
            field = 'item_class_id_restriction',
            type = 'List (,) of String',
            func = function(tpl_args, frame, value)
                if value == nil then 
                    return nil
                end
                value = m_util.string.split(value, ', ')
                for _, v in ipairs(value) do
                    if m_game.constants.item.classes[v] == nil then
                        error(string.format(i18n.errors.skill.invalid_item_class_id, v))
                    end
                end
                return value
            end,
        },
        item_class_restriction = {
            name = i18n.parameters.skill.item_class_restriction,
            field = 'item_class_restriction',
            type = 'List (,) of String',
            func = function(tpl_args, frame, value)
                if tpl_args.item_class_id_restriction == nil then
                    return
                end
                -- This function makes a localized list based on ids
                local item_classes = {}
                for _, v in ipairs(tpl_args.item_class_id_restriction) do
                    item_classes[#item_classes+1] = m_game.constants.item.classes[v].full
                end
                
                return item_classes
            end,
        },
        -- Projectiles.dat - manually mapped to the skills
        projectile_speed = {
            name = i18n.parameters.skill.projectile_speed,
            field = 'projectile_speed',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        -- Misc data derieved from stats
        stat_text = {
            name = i18n.parameters.skill.stat_text,
            field = 'stat_text',
            type = 'Text',
            func = nil,
        },
        quality_stat_text = {
            name = i18n.parameters.skill.quality_stat_text,
            field = 'quality_stat_text',
            type = 'Text',
            func = nil,
        },
        -- Misc data currently not from game data
        radius = {
            name = i18n.parameters.skill.radius,
            field = 'radius',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        radius_description = {
            name = i18n.parameters.skill.radius_description,
            field = 'radius_description',
            type = 'Text',
            func = h.cast.wrap(m_util.cast.text),
        },
        radius_secondary = {
            name = i18n.parameters.skill.radius_secondary,
            field = 'radius_secondary',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        radius_secondary_description = {
            name = i18n.parameters.skill.radius_secondary_description,
            field = 'radius_secondary_description',
            type = 'Text',
            func = h.cast.wrap(m_util.cast.text),
        },
        radius_tertiary = { -- not sure if any skill actually has 3 radius componets
            name = i18n.parameters.skill.radius_tertiary,
            field = 'radius_tertiary',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        radius_tertiary_description = {
            name = i18n.parameters.skill.radius_tertiary_description,
            field = 'radius_tertiary_description',
            type = 'Text',
            func = h.cast.wrap(m_util.cast.text),
        },
        skill_screenshot = {
            name = i18n.parameters.skill.skill_screenshot,
            field = 'skill_screenshot',
            type = 'Page',
            func = function(tpl_args, frame)
                local ss
                if tpl_args.skill_screenshot_file ~= nil then
                    ss = string.format('File:%s', tpl_args.skill_screenshot_file)
                elseif tpl_args.skill_screenshot ~= nil then
                    ss = string.format(i18n.files.skill_screenshot, tpl_args.skill_screenshot)
                elseif tpl_args.active_skill_name then
                    -- When this parameter is set manually, we assume/expect it to be exist, but otherwise it probably doesn't and we don't need dead links in that case
                    ss = string.format(i18n.files.skill_screenshot, tpl_args.active_skill_name)
                    page = mw.title.new(ss)
                    if page == nil or not page.exists then
                        ss = nil
                    end
                end
                return ss
            end,
        },
        -- Set programmatically
        max_level = {
            name = nil,
            field = 'max_level',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        html = {
            name = nil,
            field = 'html',
            type = 'Text',
            func = nil,
        },
        -- Deprecated
        has_percentage_mana_cost = {
            name = i18n.parameters.skill.has_percentage_mana_cost,
            field = 'has_percentage_mana_cost',
            type = 'Boolean',
            func = h.cast.wrap(m_util.cast.boolean),
            default = false,
            deprecated = true,
        },
        has_reservation_mana_cost = {
            name = i18n.parameters.skill.has_reservation_mana_cost,
            field = 'has_reservation_mana_cost',
            type = 'Boolean',
            func = h.cast.wrap(m_util.cast.boolean),
            default = false,
            deprecated = true,
        },
    },
}

tables.progression = {
    table = 'skill_levels',
    fields = {
        level = {
            name = nil,
            field = 'level',
            type = 'Integer',
            func = nil,
        },
        level_requirement = {
            name = i18n.parameters.skill.level_requirement,
            field = 'level_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        dexterity_requirement = {
            name = i18n.parameters.skill.dexterity_requirement,
            field = 'dexterity_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        strength_requirement = {
            name = i18n.parameters.skill.strength_requirement,
            field = 'strength_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        intelligence_requirement = {
            name = i18n.parameters.skill.intelligence_requirement,
            field = 'intelligence_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        mana_multiplier = {
            name = i18n.parameters.skill.mana_multiplier,
            field = 'mana_multiplier',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        critical_strike_chance = {
            name = i18n.parameters.skill.critical_strike_chance,
            field = 'critical_strike_chance',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        damage_effectiveness = {
            name = i18n.parameters.skill.damage_effectiveness,
            field = 'damage_effectiveness',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        stored_uses = {
            name = i18n.parameters.skill.stored_uses,
            field = 'stored_uses',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        cooldown = {
            name = i18n.parameters.skill.cooldown,
            field = 'cooldown',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%.2f ' .. m_game.units.seconds.short_lower,
        },
        vaal_souls_requirement = {
            name = i18n.parameters.skill.vaal_souls_requirement,
            field = 'vaal_souls_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        vaal_stored_uses = {
            name = i18n.parameters.skill.vaal_stored_uses,
            field = 'vaal_stored_uses',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_stored_uses,
        },
        vaal_soul_gain_prevention_time = {
            name = i18n.parameters.skill.vaal_soul_gain_prevention_time,
            field = 'vaal_soul_gain_prevention_time',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%i ' .. m_game.units.seconds.short_lower,
        },
        damage_multiplier = {
            name = i18n.parameters.skill.damage_multiplier,
            field = 'damage_multiplier',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        attack_speed_multiplier = {
            name = i18n.parameters.skill.attack_speed_multiplier,
            field = 'attack_speed_multiplier',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        duration = {
            name = i18n.parameters.skill.duration,
            field = 'duration',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%.2f ' .. m_game.units.seconds.short_lower,
        },
        -- from gem experience, optional
        experience = {
            name = i18n.parameters.skill.experience,
            field = 'experience',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        stat_text = {
            name = i18n.parameters.skill.stat_text,
            field = 'stat_text',
            type = 'Text',
            func = h.cast.wrap(m_util.cast.text),
        },
        -- Deprecated
        mana_cost = {
            name = i18n.parameters.skill.mana_cost,
            field = 'mana_cost',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            deprecated = true,
        },
    }
}

tables.skill_costs = {
    table = 'skill_costs',
    fields = {
        set_id = {
            name = nil,
            field = 'set_id',
            type = 'Integer',
            func = nil,
        },
        type = {
            name = i18n.parameters.skill.cost_type,
            field = 'type',
            type = 'String',
            func = function(tpl_args, frame, value)
                if value == nil then 
                    return nil
                end
                if m_game.constants.skill.cost_types[value] == nil then
                    error(string.format(i18n.errors.skill.invalid_cost_type, value))
                end
                return value
            end,
        },
        is_reservation = {
            name = i18n.parameters.skill.cost_is_reservation,
            field = 'is_reservation',
            type = 'Boolean',
            func = h.cast.wrap(m_util.cast.boolean),
            default = false,
        },
    }
}

tables.skill_level_costs = {
    table = 'skill_level_costs',
    fields = {
        set_id = {
            name = nil,
            field = 'set_id',
            type = 'Integer',
            func = nil,
        },
        level = {
            name = nil,
            field = 'level',
            type = 'Integer',
            func = nil,
        },
        amount = {
            name = i18n.parameters.skill.cost_amount,
            field = 'amount',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
    },
}

tables.skill_stats_per_level = {
    table = 'skill_stats_per_level',
    fields = {
        level = {
            name = nil,
            field = 'level',
            type = 'Integer',
            func = nil,
        },
        id = {
            name = i18n.parameters.skill.stat_id,
            field = 'id',
            type = 'String',
            func = h.cast.wrap(m_util.cast.text),
        },
        value = {
            name = i18n.parameters.skill.stat_value,
            field = 'value',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
    },
}

tables.skill_quality = {
    table = 'skill_quality',
    fields = {
        set_id = {
            field = 'set_id',
            type = 'Integer',
        },
        weight = {
            field = 'weight',
            type = 'Integer',
        },
        stat_text = {
            field = 'stat_text',
            type = 'String',
        },
    },
}

tables.skill_quality_stats = {
    table = 'skill_quality_stats',
    fields = {
        set_id = {
            field = 'set_id',
            type = 'Integer',
        },
        id = {
            field = 'id',
            type = 'String',
        },
        value = {
            field = 'value',
            type = 'Integer',
        },
    },
}

-- ----------------------------------------------------------------------------
-- Data
-- ----------------------------------------------------------------------------

data.skill_progression_table = {
    {
        field = 'level',
        header = i18n.progression.level,
    },
    {
        field = 'level_requirement',
        header = i18n.progression.level_requirement,
    },
    {
        field = 'dexterity_requirement',
        header = i18n.progression.dexterity_requirement,
    },
    {
        field = 'strength_requirement',
        header = i18n.progression.strength_requirement,
    },
    {
        field = 'intelligence_requirement',
        header = i18n.progression.intelligence_requirement,
    },
    {
        field = 'mana_multiplier',
        header = i18n.progression.mana_multiplier,
        fmt = '%s%%',
    },
    {
        field = 'critical_strike_chance',
        header = i18n.progression.critical_strike_chance,
        fmt = '%s%%',
    },
    { -- Also supports deprecated method of specifying mana cost and reservation
        field = 'mana_cost',
        header = function (tpl_args, frame)
            if #tpl_args.skill_data.costs == 0 and tpl_args.skill_data[tables.static.fields.has_reservation_mana_cost.name] then
                return i18n.progression.mana_reserved
            end
            return i18n.progression.mana_cost
        end,
        fmt = function (tpl_args, frame)
            if #tpl_args.skill_data.costs == 0 and tpl_args.skill_data[tables.static.fields.has_percentage_mana_cost.name] then
                return '%s%%'
            end
            return '%s'
        end,
    },
    {
        field = 'mana_percent_cost',
        header = i18n.progression.mana_cost,
        fmt = '%s%%',
    },
    {
        field = 'life_cost',
        header = i18n.progression.life_cost,
    },
    {
        field = 'life_percent_cost',
        header = i18n.progression.life_cost,
        fmt = '%s%%',
    },
    {
        field = 'energy_shield_cost',
        header = i18n.progression.energy_shield_cost,
    },
    {
        field = 'rage_cost',
        header = i18n.progression.rage_cost,
    },
    {
        field = 'mana_reserved',
        header = i18n.progression.mana_reserved,
    },
    {
        field = 'mana_percent_reserved',
        header = i18n.progression.mana_reserved,
        fmt = '%s%%',
    },
    {
        field = 'life_reserved',
        header = i18n.progression.life_reserved,
    },
    {
        field = 'life_percent_reserved',
        header = i18n.progression.life_reserved,
        fmt = '%s%%',
    },
    {
        field = 'damage_effectiveness',
        header = i18n.progression.damage_effectiveness,
        fmt = '%s%%',
    },
    {
        field = 'stored_uses',
        header = i18n.progression.stored_uses,
    },
    {
        field = 'cooldown',
        header = i18n.progression.cooldown,
        fmt = '%.2f ' .. m_game.units.seconds.short_lower,
    },
    {
        field = 'vaal_souls_requirement',
        header = i18n.progression.vaal_souls_requirement,
    },
    {
        field = 'vaal_stored_uses',
        header = i18n.progression.vaal_stored_uses,
    },
    {
        field = 'vaal_soul_gain_prevention_time',
        header = i18n.progression.vaal_soul_gain_prevention_time,
        fmt = '%i ' .. m_game.units.seconds.short_lower,
    },
    {
        field = 'damage_multiplier',
        header = i18n.progression.damage_multiplier,
        fmt = '%s%%',
    },
    {
        field = 'duration',
        header = i18n.progression.duration,
        fmt = '%.2f ' .. m_game.units.seconds.short_lower,
    },
    {
        field = 'attack_speed_multiplier',
        header = i18n.progression.attack_speed_multiplier,
        fmt = '%s%%',
    },
}

data.infobox_table = {
    {
        header = i18n.infobox.active_skill_name,
        func = h.display.factory.value{key='active_skill_name'},
    },
    {
        header = i18n.infobox.skill_id,
        func = function (tpl_args, frame)
            return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.skill_id)
        end 
    },
    {
        header = i18n.infobox.skill_icon,
        func = function (tpl_args, frame)
            if tpl_args.skill_icon then 
                return string.format('[[%s]]', tpl_args.skill_icon)
            end
        end,
    },
    {
        header = i18n.infobox.cast_time,
        func = function (tpl_args, frame)
            local value = tpl_args.cast_time
            if value then
                if value == 0 then
                    return i18n.infobox.instant_cast_time
                end
                return string.format('%.2f %s', value, m_game.units.seconds.short_lower)
            end
            return value
        end,
    },
    {
        header = i18n.infobox.item_class_restrictions,
        func = function (tpl_args, frame)
            if tpl_args.item_class_restriction == nil then
                return
            end
            local out = {}
            for _, class in ipairs(tpl_args.item_class_restriction) do
                out[#out+1] = string.format('[[%s]]', class)
            end
            return table.concat(out, '<br>')
        end,
    },
    {
        header = i18n.infobox.projectile_speed,
        func = h.display.factory.value{key='projectile_speed'},
    },
    {
        header = i18n.infobox.radius,
        func = h.display.factory.radius{key=''},
    },
    {
        header = i18n.infobox.radius_secondary,
        func = h.display.factory.radius{key='_secondary'},
    },
    {
        header = i18n.infobox.radius_tertiary,
        func = h.display.factory.radius{key='_tertiary'},
    },
    {
        header = i18n.infobox.level_requirement,
        func = h.display.factory.range_value{key='level_requirement'},
    },
    -- ingore attrbiutes?
    {
        header = i18n.infobox.mana_multiplier,
        func = h.display.factory.range_value{key='mana_multiplier'},
    },
    {
        header = i18n.infobox.critical_strike_chance,
        func = h.display.factory.range_value{key='critical_strike_chance'},
    },
    {
        header = i18n.infobox.cost,
        func = function (tpl_args, frame)
            if not tpl_args.skill_costs.has_flat_cost then
                -- Try falling back to deprecated parameters
                if not tpl_args.has_reservation_mana_cost then
                    local range = h.display.factory.range_value{key='mana_cost'}(tpl_args, frame)
                    if range then
                        return string.format('%s %s', range, m_game.constants.skill.cost_types.mana.long_upper)
                    end
                    return
                end
                return
            end
            local sets = {}
            for i=1, #tpl_args.skill_costs do
                if not tpl_args.skill_costs[i].is_reservation then -- Only get flat costs
                    local cost_type = tpl_args.skill_costs[i].type
                    local range = h.display.factory.range_value{key='amount', set_name='costs', set_id=i, map=tables.skill_level_costs}(tpl_args, frame)
                    if range then
                        local fmt
                        if string.find(cost_type, 'percent', 1, true) then
                            fmt = '%s%% %s'
                        else
                            fmt = '%s %s'
                        end
                        sets[#sets+1] = string.format(fmt, range, m_game.constants.skill.cost_types[cost_type].long_upper)
                    end
                end
            end
            return table.concat(sets, ', ')
        end,
    },
    {
        header = i18n.infobox.reservation,
        func = function (tpl_args, frame)
            if not tpl_args.skill_costs.has_reservation_cost then
                -- Try falling back to deprecated parameters
                if tpl_args.has_reservation_mana_cost then
                    local range = h.display.factory.range_value{key='mana_cost'}(tpl_args, frame)
                    if range then
                        if tpl_args.has_percentage_mana_cost then
                            return string.format('%s%% %s', range, m_game.constants.skill.cost_types.mana.long_upper)
                        end
                        return string.format('%s %s', range, m_game.constants.skill.cost_types.mana.long_upper)
                    end
                    return
                end
                return
            end
            local sets = {}
            for i=1, #tpl_args.skill_costs do
                if tpl_args.skill_costs[i].is_reservation then -- Only get reservation costs
                    local cost_type = tpl_args.skill_costs[i].type
                    local range = h.display.factory.range_value{key='amount', set_name='costs', set_id=i, map=tables.skill_level_costs}(tpl_args, frame)
                    if range then
                        local fmt
                        if string.find(cost_type, 'percent', 1, true) then
                            fmt = '%s%% %s'
                        else
                            fmt = '%s %s'
                        end
                        sets[#sets+1] = string.format(fmt, range, m_game.constants.skill.cost_types[cost_type].long_upper)
                    end
                end
            end
            return table.concat(sets, ', ')
        end,
    },
    {
        header = i18n.infobox.attack_speed_multiplier,
        func = h.display.factory.range_value{key='attack_speed_multiplier'},
        fmt = '%s ' .. i18n.infobox.of_base_stat,
    },
    {
        header = i18n.infobox.damage_multiplier,
        func = h.display.factory.range_value{key='damage_multiplier'},
        fmt = '%s ' .. i18n.infobox.of_base_stat,
    },
    {
        header = i18n.infobox.damage_effectiveness,
        func = h.display.factory.range_value{key='damage_effectiveness'},
    },
    {
        header = i18n.infobox.stored_uses,
        func = h.display.factory.range_value{key='stored_uses'},
    },
    {
        header = i18n.infobox.cooldown,
        func = h.display.factory.range_value{key='cooldown'},
    },
    {
        header = i18n.infobox.vaal_souls_requirement,
        func = h.display.factory.range_value{key='vaal_souls_requirement'},
    },
    {
        header = i18n.infobox.vaal_stored_uses,
        func = h.display.factory.range_value{key='vaal_stored_uses'},
    },
    {
        header = i18n.infobox.vaal_soul_gain_prevention_time,
        func = h.display.factory.range_value{key='vaal_soul_gain_prevention_time'},
    }, 
    {
        header = i18n.infobox.duration,
        func = h.display.factory.range_value{key='duration'},
    },
    {
        header = nil,
        func = h.display.factory.value{key='gem_description'},
        class = 'tc -gemdesc',
    },
    {
        header = nil,
        func = h.display.factory.value{key='stat_text'},
        class = 'tc -mod',
    },
}

-- ----------------------------------------------------------------------------
-- Invokable functions
-- ----------------------------------------------------------------------------
local p = {}

p.table_skills = m_cargo.declare_factory{data=tables.static}
p.table_skill_levels = m_cargo.declare_factory{data=tables.progression}
p.table_skill_costs = m_cargo.declare_factory{data=tables.skill_costs}
p.table_skill_level_costs = m_cargo.declare_factory{data=tables.skill_level_costs}
p.table_skill_stats_per_level = m_cargo.declare_factory{data=tables.skill_stats_per_level}
p.table_skill_quality = m_cargo.declare_factory{data=tables.skill_quality}
p.table_skill_quality_stats = m_cargo.declare_factory{data=tables.skill_quality_stats}

--
-- Processes skill data from tpl_args.
-- Stores skill data in cargo tables.
-- Attaches page to cargo tables.
--
function p._skill(tpl_args, frame)
    frame = m_util.misc.get_frame(frame)
    tpl_args = tpl_args or {}
    tpl_args._flags = tpl_args._flags or {}
    tpl_args.skill_levels = {
        [0] = {},
    }
    
    -- Quality
    tpl_args.skill_quality = {}
    local i = 0
    repeat
        i = i + 1
        local prefix = string.format('quality_type%s', i)
        local q = {
            _table = tables.skill_quality.table,
            set_id = i,
            weight = tonumber(tpl_args[string.format('%s_weight', prefix)]),
            stat_text = tpl_args[string.format('%s_stat_text', prefix)],
        }
        if q.stat_text then
            tpl_args.skill_quality[#tpl_args.skill_quality+1] = q
            m_cargo.store(frame, q)
            
            q.stats = {}
            q._table = nil
            local j = 0
            repeat 
                j = j + 1
                local stat_prefix = string.format('%s_stat%s', prefix, j)
                local s = {
                    _table = tables.skill_quality_stats.table,
                    set_id = i,
                    id = tpl_args[string.format('%s_id', stat_prefix)],
                    value = tonumber(tpl_args[string.format('%s_value', stat_prefix)]),
                }
                if s.id and s.value then
                    q.stats[#q.stats+1] = s
                    m_cargo.store(frame, s)
                end
                
                s._table = nil
            until s.id == nil or s.value == nil
        end
    until q.stat_text == nil
    if #tpl_args.skill_quality > 1 then
        -- Gem has alternative qualtiy
        tpl_args._flags.is_alt_quality_gem = true
    end

    -- Costs
    for i=1, math.huge do -- repeat until no more cost sets are found
        local prefix = string.format('%s%d_', i18n.parameters.skill.skill_cost, i)
        if tpl_args[prefix .. tables.skill_costs.fields.type.field] == nil then
            break
        end
        local properties = {
            _table = tables.skill_costs.table,
            [tables.skill_costs.fields.set_id.field] = i,
        }
        h.map_to_arg(tpl_args, frame, properties, prefix, tables.skill_costs, nil, 'skill_costs', i)
        if properties.is_reservation then
            tpl_args.skill_costs.has_reservation_cost = true
        else
            tpl_args.skill_costs.has_flat_cost = true
        end
        tpl_args.skill_costs[i] = {
            set_id = properties.set_id,
            type = properties.type,
            is_reservation = properties.is_reservation,
        }
        if not tpl_args.test then
            m_cargo.store(frame, properties)
        end
    end
    
    -- Handle level progression
    local level_count = 0
    for i=1, math.huge do -- repeat until no more levels are found
        local prefix = i18n.parameters.skill.level .. i
        local level = m_util.cast.boolean(tpl_args[prefix])
        if not level then
            break
        end
        tpl_args.skill_levels[i] = {}
        prefix = prefix .. '_'
        level_count = i
        if tpl_args[prefix .. i18n.parameters.skill.experience] ~= nil then
            -- For skill gems, max level is the highest level with experience.
            tpl_args.max_level = i
        end
        local properties = {
            _table = tables.progression.table,
            [tables.progression.fields.level.field] = i
        }
        h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, i)
        if not tpl_args.test then
            m_cargo.store(frame, properties)
        end
        h.costs(tpl_args, frame, prefix, i)
        h.stats(tpl_args, frame, prefix, i)
    end
    tpl_args.max_level = tpl_args.max_level or level_count

    -- handle static progression
    local prefix = i18n.parameters.skill.static .. '_'
    do
        local properties = {
            _table = tables.progression.table,
            [tables.progression.fields.level.field] = 0
        }
        h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, 0)
        if not tpl_args.test then
            m_cargo.store(frame, properties)
        end
    end
    
    -- Handle static arguments
    local properties = {
        _table = tables.static.table,
        [tables.static.fields.max_level.field] = tpl_args.max_level
    }
    h.map_to_arg(tpl_args, frame, properties, '', tables.static)
    h.costs(tpl_args, frame, prefix, 0)
    h.stats(tpl_args, frame, prefix, 0)

    -- Build infobox
    local infobox = mw.html.create('span')
    infobox:attr('class', 'skill-box')
    local tbl = infobox:tag('table')
    tbl:attr('class', 'wikitable skill-box-table')
    for _, infobox_data in ipairs(data.infobox_table) do
        local display = infobox_data.func(tpl_args, frame)
        if display ~= nil and infobox_data.fmt ~= nil then
            if type(infobox_data.fmt) == 'string' then
                display = string.format(infobox_data.fmt, display)
            elseif type(infobox_data.fmt) == 'function' then
                display = string.format(infobox_data.fmt(tpl_args, frame) or '%s', display)
            end
        end
        if display then
            local tr = tbl:tag('tr')
            if infobox_data.header then
                local header_text
                if type(infobox_data.header) == 'function' then
                    header_text = infobox_data.header(tpl_args, frame)
                else
                    header_text = infobox_data.header
                end
                tr
                    :tag('th')
                        :wikitext(header_text)
                        :done()
            end
            local td = tr:tag('td')
            td:wikitext(display)
            td:attr('class', infobox_data.class or 'tc -value')
            if infobox_data.header == nil then
                td:attr('colspan', 2)
            end
        end
    end
    infobox = tostring(infobox)

    -- Store data
    properties[tables.static.fields.html.field] = infobox
    if not tpl_args.test then
        m_cargo.store(frame, properties)
    end

    -- Attach tables
    if not tpl_args.test then
        local attach_tables = {
            tables.static.table,
            tables.progression.table,
        }
        if #tpl_args.skill_quality > 0 then
            attach_tables[#attach_tables+1] = tables.skill_quality.table
            attach_tables[#attach_tables+1] = tables.skill_quality_stats.table
        end
        if #tpl_args.skill_costs > 0 then
            attach_tables[#attach_tables+1] = tables.skill_costs.table
            attach_tables[#attach_tables+1] = tables.skill_level_costs.table
        end
        if tpl_args.skill_levels.has_stats then
            attach_tables[#attach_tables+1] = tables.skill_stats_per_level.table
        end
        for _, table_name in ipairs(attach_tables) do
            frame:expandTemplate{
                title = string.format(i18n.templates.cargo_attach, table_name),
                args = {}
            }
        end
    end

     -- Log when testing
    if tpl_args.test then
        mw.logObject(tpl_args)
    end

    return infobox
end

--
-- Template:Skill
--
function p.skill(frame)
    --[[
    Display skill infobox
    
    Examples
    --------
    =p.skill{gem_description='Icy bolts rain down over the targeted area.', active_skill_name='Icestorm', skill_id='IcestormUniqueStaff12', cast_time=0.75, required_level=1, static_mana_cost=22, static_critical_strike_chance=6, static_damage_effectiveness=30, static_damage_multiplier=100, static_stat1_id='spell_minimum_base_cold_damage_+_per_10_intelligence', static_stat1_value=1, static_stat2_id='spell_maximum_base_cold_damage_+_per_10_intelligence', static_stat2_value=3, static_stat3_id='base_skill_effect_duration', static_stat3_value=1500, static_stat4_id='fire_storm_fireball_delay_ms', static_stat4_value=100, static_stat5_id='skill_effect_duration_per_100_int', static_stat5_value=150, static_stat6_id='skill_override_pvp_scaling_time_ms', static_stat6_value=450, static_stat7_id='firestorm_drop_ground_ice_duration_ms', static_stat7_value=500, static_stat8_id='skill_art_variation', static_stat8_value=4, static_stat9_id='base_skill_show_average_damage_instead_of_dps', static_stat9_value=1, static_stat10_id='is_area_damage', static_stat10_value=1, stat_text='Deals 1 to 3 base Cold Damage per 10 Intelligence<br>Base duration is 1.5 seconds<br>One impact every 0.1 seconds<br>0.15 seconds additional Base Duration per 100 Intelligence', quality_stat_text = nil, level1=true, level1_level_requirement=1}

    ]]

    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)

    -- Handle skill data and get infobox
    local infobox = p._skill(tpl_args, frame)

    -- Container
    local container = mw.html.create('span')
    container
        :attr('class', 'skill-box-page-container')
        :wikitext(infobox)
    if tpl_args.skill_screenshot then
        container
            :wikitext(string.format('[[%s]]', tpl_args.skill_screenshot))
    end

    -- Generic messages on the page:
    out = {}
    if mw.ustring.find(tpl_args.skill_id, '_') then
        out[#out+1] = frame:expandTemplate{
            title = i18n.templates.incorrect_title,
            args = {title=tpl_args.skill_id}
        } .. '\n\n\n'
    end
    if tpl_args.active_skill_name then
        out[#out+1] = string.format(
            i18n.messages.intro_named_id, 
            tpl_args.skill_id, 
            tpl_args.active_skill_name
        )
    else
        out[#out+1] = string.format(
            i18n.messages.intro_unnamed_id, 
            tpl_args.skill_id
        )
    end

    -- Categories
    local cats = {i18n.categories.skill_data}
    if tpl_args.has_deprecated_parameters then
        cats[#cats+1] = i18n.categories.deprecated_parameters
    end
    
    return tostring(container) .. m_util.misc.add_category(cats) .. '\n' .. table.concat(out)
end

function p.progression(frame)
    --[[
        Displays the level progression for the skill gem. 
        
        Examples
        --------
        = p.progression{page='Reave'}
    ]]
    
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    -- Parse column arguments:
    tpl_args.stat_format = {}
    local param_keys = {
        i18n.parameters.progression.header,
        i18n.parameters.progression.abbr,
        i18n.parameters.progression.pattern_extract,
        i18n.parameters.progression.pattern_value,
    }
    for i=1, math.huge do -- repeat until no more columns are found
        local prefix = string.format('%s%d_', i18n.parameters.progression.column, i)
        if tpl_args[prefix .. param_keys[1]] == nil then
            break
        end
        local statfmt = {counter = 0}
        for _, key in ipairs(param_keys) do
            local arg = prefix .. key
            if tpl_args[arg] == nil then
                error(string.format(i18n.errors.progression.argument_unspecified, arg))
            end
            statfmt[key] = tpl_args[arg]
        end
        statfmt.header = m_util.html.abbr(statfmt.abbr, statfmt.header)
        statfmt.abbr = nil
        tpl_args.stat_format[#tpl_args.stat_format+1] = statfmt
    end
    
    -- Query skill data
    local results = {}
    local skill_data
    local fields = {
        '_pageName',
        tables.static.fields.has_reservation_mana_cost.name,
        tables.static.fields.has_percentage_mana_cost.name,
    }
    local query = {
        groupBy = '_pageID',
    }
    if tpl_args.skill_id then -- Query by skill id
        query.where = string.format('skill_id="%s"', tpl_args.skill_id)
        results = m_cargo.query({tables.static.table}, fields, query)
        if #results == 0 then
            error(string.format(i18n.errors.progression.no_results_for_skill_id, tpl_args.skill_id))
        end
    else -- Query by page name
        page = tpl_args.page or mw.title.getCurrentTitle().prefixedText
        query.where = string.format('_pageName="%s"', page)
        results = m_cargo.query({tables.static.table}, fields, query)
        if #results == 0 then
            error(string.format(i18n.errors.progression.no_results_for_skill_page, page))
        end
    end
    skill_data = results[1]
    skill_data[tables.static.fields.has_reservation_mana_cost.name] = m_util.cast.boolean(skill_data[tables.static.fields.has_reservation_mana_cost.name])
    skill_data[tables.static.fields.has_percentage_mana_cost.name] = m_util.cast.boolean(skill_data[tables.static.fields.has_percentage_mana_cost.name])
    tpl_args.skill_data = skill_data

    -- Query progression data
    fields = {}
    for _, fmap in pairs(tables.progression.fields) do
        fields[#fields+1] = fmap.field
    end
    query = {
        where = string.format(
            '_pageName="%s" AND %s > 0',
            skill_data['_pageName'],
            tables.progression.fields.level.field
        ),
        groupBy = string.format(
            '_pageID, %s',
            tables.progression.fields.level.field
        ),
        orderBy = string.format(
            '%s ASC',
            tables.progression.fields.level.field
        ),
    }
    results = m_cargo.query({tables.progression.table}, fields, query)
    if #results == 0 then
        error(i18n.errors.progression.missing_level_data)
    end
    skill_data.levels = results

    -- Query cost data
    fields = {}
    for _, fmap in pairs(tables.skill_costs.fields) do
        fields[#fields+1] = fmap.field
    end
    query = {
        where = string.format(
            '_pageName="%s"',
            skill_data['_pageName']
        ),
        groupBy = string.format(
            '_pageID, %s',
            tables.skill_costs.fields.set_id.field
        ),
        orderBy = string.format(
            '%s ASC',
            tables.skill_costs.fields.set_id.field
        ),
    }
    results = m_cargo.query({tables.skill_costs.table}, fields, query)
    skill_data.costs = results
    if #results > 0 then -- If skill has costs, query cost data by levels
        fields = {}
        for _, fmap in pairs(tables.skill_level_costs.fields) do
            fields[#fields+1] = fmap.field
        end
        query = {
            where = string.format(
                '_pageName="%s" AND %s > 0',
                skill_data['_pageName'],
                tables.skill_level_costs.fields.level.field
            ),
            groupBy = string.format(
                '_pageID, %s, %s',
                tables.skill_level_costs.fields.set_id.field,
                tables.skill_level_costs.fields.level.field
            ),
            orderBy = string.format(
                '%s ASC, %s ASC',
                tables.skill_level_costs.fields.set_id.field,
                tables.skill_level_costs.fields.level.field
            ),
        }
        results = m_cargo.query({tables.skill_level_costs.table}, fields, query)
        skill_data.costs_by_level = results

        -- Interpolate cost data into level data
        local column
        for _,cdata in ipairs(skill_data.costs) do
            if m_util.cast.boolean(cdata[tables.skill_costs.fields.is_reservation.field]) then
                column = string.format('%s_reserved', cdata[tables.skill_costs.fields.type.field])
            else
                column = string.format('%s_cost', cdata[tables.skill_costs.fields.type.field])
            end
            for _,ldata in ipairs(skill_data.levels) do
                for _,rdata in ipairs(skill_data.costs_by_level) do
                    if rdata[tables.skill_level_costs.fields.set_id.field] == cdata[tables.skill_costs.fields.set_id.field] and rdata[tables.skill_level_costs.fields.level.field] == ldata[tables.progression.fields.level.field] then
                        ldata[column] = rdata[tables.skill_level_costs.fields.amount.field]
                        break
                    end
                end
            end
        end
    end
    
    -- Set up html table headers
    headers = {}
    for _, row in ipairs(skill_data.levels) do
        for k, v in pairs(row) do
            headers[k] = true
        end
    end
    local tbl = mw.html.create('table')
    tbl
        :attr('class', 'wikitable responsive-table skill-progression-table')
    local head = tbl:tag('tr')
    for _, tmap in pairs(data.skill_progression_table) do
        if headers[tmap.field] then
            local text = type(tmap.header) == 'function' and tmap.header(tpl_args, frame) or tmap.header
            head
                :tag('th')
                    :wikitext(text)
                    :done()
        end
    end
    for _, statfmt in ipairs(tpl_args.stat_format) do
        head
            :tag('th')
                :wikitext(statfmt.header)
                :done()
    end
    if headers[tables.progression.fields.experience.field] then
        head
            :tag('th')
                :wikitext(i18n.progression.experience)
                :done()
            :tag('th')
                :wikitext(i18n.progression.total_experience)
                :done()
    end

    -- Table rows
    local tblrow
    local lastexp = 0
    local experience
    for _, row in ipairs(skill_data.levels) do
        tblrow = tbl:tag('tr')
        for _, tmap in pairs(data.skill_progression_table) do
            if headers[tmap.field] then
                h.int_value_or_na(tpl_args, frame, tblrow, row[tmap.field], tmap)
            end
        end
        
        -- stats
        local stats = {}
        if row[tables.progression.fields.stat_text.field] then
            stats = m_util.string.split(
                row[tables.progression.fields.stat_text.field],
                '<br>'
            )
        end
        for _, statfmt in ipairs(tpl_args.stat_format) do
            local match = {}
            for j, stat in ipairs(stats) do
                match = {string.match(stat, statfmt.pattern_extract)}
                if #match > 0 then
                    -- TODO maybe remove stat here to avoid testing 
                    -- against in future loops
                    break
                end
            end
            if #match == 0 then
                tblrow:node(m_util.html.td.na())
            else
                -- used to find broken progression due to game updates
                -- for example:
                statfmt.counter = statfmt.counter + 1
                tblrow
                    :tag('td')
                        :wikitext(string.format(
                            statfmt.pattern_value, 
                            match[1], 
                            match[2], 
                            match[3], 
                            match[4], 
                            match[5]
                            )
                        )
                        :done()
            end
        end
        
        -- TODO: Quality stats, afaik no gems use this atm
        
        if headers[tables.progression.fields.experience.field] then
            experience = tonumber(row[tables.progression.fields.experience.field])
            if experience ~= nil then
                h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
                lastexp = experience
            else
                tblrow:node(m_util.html.td.na())
            end
            h.int_value_or_na(tpl_args, frame, tblrow, experience, {})
        end
    end
    
    local cats = {}
    for _, statfmt in ipairs(tpl_args.stat_format) do
        if statfmt.counter == 0 then
            cats = i18n.categories.broken_progression_table
            break
        end
    end
    
    return tostring(tbl) .. m_util.misc.add_category(cats) 
end

return p