Path of Exile Wiki

Wiki поддерживается сообществом, поэтому подумайте над тем, чтобы внести свой вклад.

ПОДРОБНЕЕ

Path of Exile Wiki
Нет описания правки
Magiczocker (обсуждение | вклад)
м (sync)
Метка: ручная отмена
 
(не показано 36 промежуточных версий 2 участников)
Строка 1: Строка 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_cargo = require('Module:Cargo')
 
local m_cargo = require('Module:Cargo')
  +
local m_util = require('Module:Util')
 
local m_game = require('Module:Game')
+
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',
 
 
-- Intro texts:
 
intro_named_id = "'''%s''' is the internal id of the [[skill]] '''%s'''.\n",
 
intro_unnamed_id = "'''%s''' is the internal id of an unnamed [[skill]].\n",
 
 
errors = {
 
all_format_keys_specified = 'All formatting keys must be specified for index "%s"',
 
no_results_for_skill_id = "Couldn't find a page for the specified skill id",
 
no_results_for_skill_page = "Couldn't find the queried data on the skill page",
 
missing_level_data = 'No gem level progression data found',
 
},
 
 
categories = {
 
broken_progression_table = 'Страницы со сломанными таблицами skill progression'
 
},
 
 
infobox = {
 
skill_id = 'Id умения',
 
active_skill_name = 'Название',
 
skill_icon = 'Изображение',
 
cast_time = 'Время применения',
 
item_class_restrictions = 'Ограничения<br>по классу<br>предмета',
 
projectile_speed = 'Скорость снаряда',
 
radius = 'Радиус',
 
radius_secondary = 'Радиус 2',
 
radius_tertiary = 'Радиус 3',
 
level_requirement = 'Треб. уровень',
 
mana_multiplier = 'Множитель маны',
 
critical_strike_chance = 'Шанс критического удара',
 
mana_cost = 'Расход маны',
 
mana_reserved = 'Удержано маны',
 
damage_effectiveness = 'Эффективность добавленного урона',
 
stored_uses = 'Заряды',
 
cooldown = 'Перезарядка',
 
vaal_souls_requirement = 'Vaal Souls',
 
vaal_stored_uses = 'Vaal Stored Uses',
 
vaal_soul_gain_prevention_time = 'Soul Gain Prevention',
 
damage_multiplier = 'Множитель урона',
 
attack_speed_multiplier = 'Множитель скорости атаки',
 
},
 
 
progression = {
 
level_requirement = m_util.html.abbr('[[Image:Level_up_icon_small.png|link=|Требуемый уровень]]', 'Требуемый уровень', 'nounderline'),
 
dexterity_requirement = m_util.html.abbr('[[Image:DexterityIcon_small.png|link=|Требуемая ловкость]]', 'Требуемая ловкость', 'nounderline'),
 
strength_requirement = m_util.html.abbr('[[Image:StrengthIcon_small.png|link=|Требуемая сила]]', 'Требуемая сила', 'nounderline'),
 
intelligence_requirement = m_util.html.abbr('[[Image:IntelligenceIcon_small.png|link=|Требуемый интеллект]]', 'Требуемый интеллект', 'nounderline'),
 
mana_multiplier = 'Множитель<br>маны',
 
critical_strike_chance = 'Шанс<br>критического<br>удара',
 
mana_cost = 'Расход<br>маны',
 
mana_reserved = 'Удержано<br>маны',
 
damage_effectiveness = 'Эффект.<br>добавл.<br>урона',
 
stored_uses = 'Stored<br>Uses',
 
cooldown = 'Перезарядка',
 
vaal_souls_requirement = 'Vaal<br>souls',
 
vaal_stored_uses = 'Stored<br>Uses',
 
vaal_soul_gain_prevention_time = 'Soul<br>Prevention<br>Time',
 
damage_multiplier = m_util.html.abbr('Множитель<br>урона', 'Наносит x% базового урона'),
 
attack_speed_multiplier = 'Множитель<br>скорости<br>атаки',
 
exp_short = 'Опыт',
 
exp_long = 'Опыт, необходимый для повышения уровня',
 
tot_exp_short = 'Всего<br>опыта',
 
tot_exp_long = 'Общий необходимый опыт',
 
},
 
}
 
   
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
Строка 95: Строка 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]
Строка 107: Строка 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._flags.has_deprecated_skill_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
Строка 124: Строка 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, '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, 'id')
 
  +
}
local stat_val_key = string.format('%s%s_%s', type_prefix, i, '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)
if not tpl_args.test then
 
m_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,
 
})
 
end
 
 
-- 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
Строка 156: Строка 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('<p title="Нет данных">Н/Д</p>')
 
  +
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)
Строка 199: Строка 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
Строка 211: Строка 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
Строка 243: Строка 213:
   
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
  +
-- Cargo tables
-- Data
 
 
-- ----------------------------------------------------------------------------
 
-- ----------------------------------------------------------------------------
   
  +
tables.static = {
 
map.stats = {
 
{
 
prefix_in = '',
 
out = 'stats',
 
quality = false,
 
},
 
{
 
prefix_in = 'quality_',
 
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_id_restriction', '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 = 'skill_id',
+
name = i18n.parameters.skill.skill_id,
 
field = 'skill_id',
 
field = 'skill_id',
 
type = 'String',
 
type = 'String',
Строка 273: Строка 228:
 
-- Active Skills.dat
 
-- Active Skills.dat
 
cast_time = {
 
cast_time = {
name = '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 = '%s сек.',
+
fmt = '%.2f ' .. m_game.units.seconds.short_lower,
 
},
 
},
 
gem_description = {
 
gem_description = {
name = 'gem_description',
+
name = i18n.parameters.skill.gem_description,
 
field = 'description',
 
field = 'description',
 
type = 'Text',
 
type = 'Text',
Строка 286: Строка 241:
 
},
 
},
 
active_skill_name = {
 
active_skill_name = {
name = 'active_skill_name',
+
name = i18n.parameters.skill.active_skill_name,
 
field = 'active_skill_name',
 
field = 'active_skill_name',
 
type = 'String',
 
type = 'String',
Строка 292: Строка 247:
 
},
 
},
 
skill_icon = {
 
skill_icon = {
name = '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_id_restriction = {
 
item_class_id_restriction = {
name = 'item_class_id_restriction',
+
name = i18n.parameters.skill.item_class_id_restriction,
 
field = 'item_class_id_restriction',
 
field = 'item_class_id_restriction',
 
type = 'List (,) of String',
 
type = 'List (,) of String',
Строка 312: Строка 267:
 
for _, v in ipairs(value) do
 
for _, v in ipairs(value) do
 
if m_game.constants.item.classes[v] == nil then
 
if m_game.constants.item.classes[v] == nil then
error(string.format('Invalid item class id: %s', v))
+
error(string.format(i18n.errors.skill.invalid_item_class_id, v))
 
end
 
end
 
end
 
end
Строка 319: Строка 274:
 
},
 
},
 
item_class_restriction = {
 
item_class_restriction = {
name = 'item_class_restriction',
+
name = i18n.parameters.skill.item_class_restriction,
 
field = 'item_class_restriction',
 
field = 'item_class_restriction',
 
type = 'List (,) of String',
 
type = 'List (,) of String',
Строка 337: Строка 292:
 
-- Projectiles.dat - manually mapped to the skills
 
-- Projectiles.dat - manually mapped to the skills
 
projectile_speed = {
 
projectile_speed = {
name = 'projectile_speed',
+
name = i18n.parameters.skill.projectile_speed,
 
field = 'projectile_speed',
 
field = 'projectile_speed',
 
type = 'Integer',
 
type = 'Integer',
Строка 344: Строка 299:
 
-- Misc data derieved from stats
 
-- Misc data derieved from stats
 
stat_text = {
 
stat_text = {
name = 'stat_text',
+
name = i18n.parameters.skill.stat_text,
 
field = 'stat_text',
 
field = 'stat_text',
 
type = 'Text',
 
type = 'Text',
Строка 350: Строка 305:
 
},
 
},
 
quality_stat_text = {
 
quality_stat_text = {
name = 'quality_stat_text',
+
name = i18n.parameters.skill.quality_stat_text,
 
field = 'quality_stat_text',
 
field = 'quality_stat_text',
 
type = 'Text',
 
type = 'Text',
Строка 356: Строка 311:
 
},
 
},
 
-- Misc data currently not from game data
 
-- Misc data currently not from game data
has_percentage_mana_cost = {
 
name = '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 = 'has_reservation_mana_cost',
 
field = 'has_reservation_mana_cost',
 
type = 'Boolean',
 
func = h.cast.wrap(m_util.cast.boolean),
 
default = false,
 
},
 
 
radius = {
 
radius = {
name = 'radius',
+
name = i18n.parameters.skill.radius,
 
field = 'radius',
 
field = 'radius',
 
type = 'Integer',
 
type = 'Integer',
Строка 377: Строка 318:
 
},
 
},
 
radius_description = {
 
radius_description = {
name = '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 = 'radius_secondary',
+
name = i18n.parameters.skill.radius_secondary,
 
field = 'radius_secondary',
 
field = 'radius_secondary',
 
type = 'Integer',
 
type = 'Integer',
Строка 389: Строка 330:
 
},
 
},
 
radius_secondary_description = {
 
radius_secondary_description = {
name = '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 = 'radius_tertiary',
 
 
field = 'radius_tertiary',
 
field = 'radius_tertiary',
 
type = 'Integer',
 
type = 'Integer',
Строка 402: Строка 342:
 
},
 
},
 
radius_tertiary_description = {
 
radius_tertiary_description = {
name = '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 = 'skill_screenshot',
+
name = i18n.parameters.skill.skill_screenshot,
 
field = 'skill_screenshot',
 
field = 'skill_screenshot',
 
type = 'Page',
 
type = 'Page',
Строка 425: Строка 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)
+
local page = mw.title.new(ss)
 
if page == nil or not page.exists then
 
if page == nil or not page.exists then
 
ss = nil
 
ss = nil
Строка 436: Строка 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 = {
Строка 449: Строка 409:
 
type = 'Integer',
 
type = 'Integer',
 
func = nil,
 
func = nil,
header = nil,
 
 
},
 
},
 
level_requirement = {
 
level_requirement = {
name = '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 = i18n.progression.level_requirement,
 
 
},
 
},
 
dexterity_requirement = {
 
dexterity_requirement = {
name = '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 = i18n.progression.dexterity_requirement,
 
 
},
 
},
 
strength_requirement = {
 
strength_requirement = {
name = '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 = i18n.progression.strength_requirement,
 
 
},
 
},
 
intelligence_requirement = {
 
intelligence_requirement = {
name = '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 = i18n.progression.intelligence_requirement,
 
 
},
 
},
 
mana_multiplier = {
 
mana_multiplier = {
name = '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 = i18n.progression.mana_multiplier,
 
 
fmt = '%s%%',
 
fmt = '%s%%',
 
},
 
},
 
critical_strike_chance = {
 
critical_strike_chance = {
name = '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 = i18n.progression.critical_strike_chance,
 
 
fmt = '%s%%',
 
fmt = '%s%%',
},
 
mana_cost = {
 
name = '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 i18n.progression.mana_cost
 
else
 
return i18n.progression.mana_reservation
 
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 = '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 = i18n.progression.damage_effectiveness,
 
 
fmt = '%s%%',
 
fmt = '%s%%',
 
},
 
},
 
stored_uses = {
 
stored_uses = {
name = '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 = i18n.progression.stored_uses,
 
 
},
 
},
 
cooldown = {
 
cooldown = {
name = '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 = i18n.progression.cooldown,
+
fmt = '%.2f ' .. m_game.units.seconds.short_lower,
fmt = '%s сек.',
 
 
},
 
},
 
vaal_souls_requirement = {
 
vaal_souls_requirement = {
name = '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 = i18n.progression.vaal_souls_requirement,
 
 
},
 
},
 
vaal_stored_uses = {
 
vaal_stored_uses = {
name = 'vaal_stored_uses',
+
name = i18n.parameters.skill.vaal_stored_uses,
 
field = 'vaal_stored_uses',
 
field = 'vaal_stored_uses',
 
type = 'Integer',
 
type = 'Integer',
Строка 555: Строка 482:
 
},
 
},
 
vaal_soul_gain_prevention_time = {
 
vaal_soul_gain_prevention_time = {
name = '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 = i18n.progression.vaal_soul_gain_prevention_time,
+
fmt = '%i ' .. m_game.units.seconds.short_lower,
fmt = '%s сек.',
 
 
},
 
},
 
damage_multiplier = {
 
damage_multiplier = {
name = '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 = i18n.progression.damage_multiplier,
 
 
fmt = '%s%%',
 
fmt = '%s%%',
 
},
 
},
 
attack_speed_multiplier = {
 
attack_speed_multiplier = {
name = 'attack_speed_multiplier',
+
name = i18n.parameters.skill.attack_speed_multiplier,
 
field = 'attack_speed_multiplier',
 
field = 'attack_speed_multiplier',
 
type = 'Integer',
 
type = 'Integer',
 
func = h.cast.wrap(m_util.cast.number),
 
func = h.cast.wrap(m_util.cast.number),
header = i18n.progression.attack_speed_multiplier,
 
 
fmt = '%s%%',
 
fmt = '%s%%',
hide = true,
+
},
  +
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 = '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 = '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 = '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', 'attack_speed_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%%',
 
},
 
},
 
}
 
}
Строка 649: Строка 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,
 
},
 
},
 
{
 
{
Строка 694: Строка 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_spending_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 spending 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,
 
},
 
},
 
{
 
{
Строка 722: Строка 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'},
 
},
 
},
 
{
 
{
Строка 738: Строка 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)
--[[
 
Creates an infobox for skills.
 
 
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}
 
 
]]
 
 
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 = '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 .. '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)
if not tpl_args.test then
 
m_cargo.store(frame, properties)
+
local s = {
  +
_table = tables.skill_quality_stats.table,
end
 
+
set_id = i,
h.stats(tpl_args, frame, prefix, 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_spending_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,
}
 
h.map_to_arg(tpl_args, frame, properties, 'static_', map.progression, 0)
+
[tables.progression.fields.level.field] = 0
if not tpl_args.test then
+
}
m_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
 
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, '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
Строка 854: Строка 1157:
 
end
 
end
 
end
 
end
 
 
infobox = tostring(infobox)
 
infobox = tostring(infobox)
   
 
--
 
 
-- Store data
 
-- Store data
  +
properties[tables.static.fields.html.field] = infobox
--
 
properties[map.static.fields.html.field] = infobox
 
 
if not tpl_args.test then
 
if not tpl_args.test then
 
m_cargo.store(frame, properties)
 
m_cargo.store(frame, properties)
 
end
 
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
 
container
Строка 881: Строка 1232:
 
out = {}
 
out = {}
 
if mw.ustring.find(tpl_args.skill_id, '_') then
 
if mw.ustring.find(tpl_args.skill_id, '_') then
out[#out+1] = frame:expandTemplate{
+
out[#out+1] = frame:expandTemplate{
title = 'Incorrect title',
+
title = i18n.templates.incorrect_title,
args = {title=tpl_args.skill_id}
+
args = {title=tpl_args.skill_id}
 
} .. '\n\n\n'
 
} .. '\n\n\n'
 
end
 
end
 
if tpl_args.active_skill_name then
 
if tpl_args.active_skill_name then
 
out[#out+1] = string.format(
 
out[#out+1] = string.format(
i18n.intro_named_id,
+
i18n.messages.intro_named_id,
 
tpl_args.skill_id,
 
tpl_args.skill_id,
 
tpl_args.active_skill_name
 
tpl_args.active_skill_name
Строка 894: Строка 1245:
 
else
 
else
 
out[#out+1] = string.format(
 
out[#out+1] = string.format(
i18n.intro_unnamed_id,
+
i18n.messages.intro_unnamed_id,
 
tpl_args.skill_id
 
tpl_args.skill_id
 
)
 
)
  +
end
  +
  +
-- Categories
  +
local cats = {i18n.categories.skill_data}
  +
if tpl_args._flags.has_deprecated_skill_parameters then
  +
cats[#cats+1] = i18n.categories.deprecated_parameters
 
end
 
end
 
 
return tostring(container) .. m_util.misc.add_category({'Skill data'}) .. '\n' .. table.concat(out)
+
return tostring(container) .. m_util.misc.add_category(cats) .. '\n' .. table.concat(out)
 
end
 
end
   
Строка 918: Строка 1275:
 
-- Parse column arguments:
 
-- 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 format_keys = {
 
  +
i18n.parameters.progression.pattern_value,
'header',
 
  +
}
'abbr',
 
  +
for i=1, math.huge do -- repeat until no more columns are found
'pattern_extract',
 
  +
local prefix = string.format('%s%d_', i18n.parameters.progression.column, i)
'pattern_value'
 
  +
if tpl_args[prefix .. param_keys[1]] == nil then
}
 
local statfmt = {counter = 0}
 
for _,v in ipairs(format_keys) do
 
statfmt[v] = tpl_args[prefix .. v]
 
end
 
 
if m_util.table.has_all_value(statfmt, format_keys) then
 
 
break
 
break
 
end
 
end
+
local statfmt = {counter = 0}
  +
for _, key in ipairs(param_keys) do
if m_util.table.has_one_value(statfmt, format_keys) then
 
error(string.format(i18n.errors.all_format_keys_specified, i))
+
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
 
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
Строка 945: Строка 1299:
 
end
 
end
 
 
  +
-- Query skill data
 
 
local results = {}
 
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 by skill id
 
if tpl_args.skill_id then
+
query.where = string.format('skill_id="%s"', tpl_args.skill_id)
  +
results = m_cargo.query({tables.static.table}, fields, query)
query.where = string.format(
 
'skill.skill_id="%s"',
 
tpl_args.skill_id
 
)
 
results = m_cargo.query({'skill'}, fields, query)
 
 
if #results == 0 then
 
if #results == 0 then
error(i18n.errors.no_results_for_skill_id)
+
error(string.format(i18n.errors.progression.no_results_for_skill_id, tpl_args.skill_id))
 
end
 
end
skill_data = results[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
 
page = mw.title.getCurrentTitle().prefixedText
 
end
 
query.where = string.format('skill._pageName="%s"', page)
 
 
results = m_cargo.query({'skill'}, fields, query)
 
 
if #results == 0 then
 
if #results == 0 then
error(i18n.errors.no_results_for_skill_page)
+
error(string.format(i18n.errors.progression.no_results_for_skill_page, page))
 
end
 
end
 
skill_data = results[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"',
 
  +
-- Query progression data
skill_data['skill._pageName']
 
)
 
 
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 = {
results = m_cargo.query(
+
where = string.format(
{'skill_levels'},
+
'_pageName="%s" AND %s > 0',
fields,
+
skill_data['_pageName'],
  +
tables.progression.fields.level.field
{
 
where=string.format(
+
),
  +
groupBy = string.format(
'skill_levels._pageName="%s" AND skill_levels.level > 0',
 
skill_data['skill._pageName']
+
'_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
 
),
 
),
groupBy='skill_levels._pageID, skill_levels.level',
 
orderBy='skill_levels.level ASC',
 
 
}
 
}
  +
results = m_cargo.query({tables.skill_level_costs.table}, fields, query)
)
 
  +
skill_data.costs_by_level = results
 
  +
if #results == 0 then
 
  +
-- Interpolate cost data into level data
error(i18n.errors.missing_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(results) 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 responsive-table 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('Ур.')
 
: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')
Строка 1047: Строка 1440:
 
end
 
end
 
end
 
end
 
 
for _, statfmt in ipairs(tpl_args.stat_format) do
 
for _, statfmt in ipairs(tpl_args.stat_format) do
 
head
 
head
Строка 1054: Строка 1446:
 
: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(
+
:wikitext(i18n.progression.experience)
i18n.progression.exp_short,
 
i18n.progression.exp_long
 
)
 
)
 
 
:done()
 
:done()
 
:tag('th')
 
:tag('th')
:wikitext(m_util.html.abbr(
+
:wikitext(i18n.progression.total_experience)
i18n.progression.tot_exp_short,
 
i18n.progression.tot_exp_long
 
)
 
)
 
 
: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(results) 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 = {}
  +
if row[tables.progression.fields.stat_text.field] then
 
stats = m_util.string.split(
 
stats = m_util.string.split(
row['skill_levels.stat_text'],
+
row[tables.progression.fields.stat_text.field],
 
'<br>'
 
'<br>'
 
)
 
)
else
 
stats = {}
 
 
end
 
end
 
for _, statfmt in ipairs(tpl_args.stat_format) do
 
for _, statfmt in ipairs(tpl_args.stat_format) do
Строка 1117: Строка 1487:
 
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 due to game updates
 
-- used to find broken progression due to game updates
Строка 1139: Строка 1509:
 
-- 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(
+
h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
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, {})
Строка 1167: Строка 1530:
 
 
 
return tostring(tbl) .. m_util.misc.add_category(cats)
 
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
   

Текущая версия от 14:44, 6 сентября 2023

Template info icon Документация модуля[просмотр] [править] [история] [очистить]

Описание

Модуль для обработки умений с поддержкой Semantic MediaWiki.

Шаблоны Skill

Модуль:Item2

Все шаблоны, определенные в Модуль:Skill:

Модуль:Skill link

Все шаблоны, определенные в Модуль: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._flags.has_deprecated_skill_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)
                    local 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_spending_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 spending 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_spending_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._flags.has_deprecated_skill_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