Path of Exile Wiki

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

ПОДРОБНЕЕ

Path of Exile Wiki
Нет описания правки
(Avoiding magic numbers is one of the most fundamental rules of programming.)
(не показана 1 промежуточная версия этого же участника)
Строка 131: Строка 131:
 
local type_prefix = string.format('%s%s', prefix_in, 'stat')
 
local type_prefix = string.format('%s%s', prefix_in, 'stat')
 
tpl_args.skill_levels[level]['stats'] = {}
 
tpl_args.skill_levels[level]['stats'] = {}
for i=1, 8 do
+
for i=1, data.max_stats_per_level do
 
local stat_id_key = string.format('%s%s_%s', type_prefix, i, 'id')
 
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_val_key = string.format('%s%s_%s', type_prefix, i, 'value')
Строка 610: Строка 610:
 
},
 
},
 
}
 
}
  +
  +
data.max_stats_per_level = 8
   
 
tables.skill_quality = {
 
tables.skill_quality = {
Строка 804: Строка 806:
 
}
 
}
 
 
  +
-- Quality
 
tpl_args.skill_quality = {}
 
tpl_args.skill_quality = {}
 
local i = 0
 
local i = 0
Строка 863: Строка 866:
 
h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, i)
 
h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, i)
 
if not tpl_args.test then
 
if not tpl_args.test then
frame:expandTemplate{title=string.format('Template:Skill/cargo/attach/%s', properties._table), args={}}
 
 
m_cargo.store(frame, properties)
 
m_cargo.store(frame, properties)
 
end
 
end
Строка 891: Строка 893:
 
h.map_to_arg(tpl_args, frame, properties, '', tables.static)
 
h.map_to_arg(tpl_args, frame, properties, '', tables.static)
 
h.stats(tpl_args, frame, 'static_', 0)
 
h.stats(tpl_args, frame, 'static_', 0)
 
 
 
 
 
Строка 930: Строка 931:
 
properties[tables.static.fields.html.field] = infobox
 
properties[tables.static.fields.html.field] = infobox
 
if not tpl_args.test then
 
if not tpl_args.test then
frame:expandTemplate{title=string.format('Template:Skill/cargo/attach/%s', properties._table), args={}}
 
 
m_cargo.store(frame, properties)
 
m_cargo.store(frame, properties)
 
end
 
end
Строка 965: Строка 965:
 
tpl_args.skill_id
 
tpl_args.skill_id
 
)
 
)
 
end
  +
  +
-- Attach tables
  +
if not tpl_args.test then
  +
local attach_tables = {
  +
tables.static.table,
  +
tables.progression.table,
  +
tables.skill_stats_per_level.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
  +
for _, table_name in ipairs(attach_tables) do
 
frame:expandTemplate{title=string.format('Template:Skill/cargo/attach/%s', table_name), args={}}
  +
end
 
end
 
end
 
 

Версия от 19:15, 6 мая 2021

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

Описание

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

Шаблоны Skill

Модуль:Item2

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

Модуль:Skill link

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

-- Skill module

-- ----------------------------------------------------------------------------
-- Includes
-- ----------------------------------------------------------------------------

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

local mwlanguage = mw.language.getContentLanguage()

--- define here to avoid errors
local data = {}
local tables = {}

-- 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''' — это внутренний идентификатор [[умения]] '''%s'''.\n",
    intro_unnamed_id = "'''%s''' — это внутренний идентификатор безымянного [[умения]].\n",
    
    errors = {
        all_format_keys_specified = 'All formatting keys must be specified for index "%s"',
        no_results_for_skill_id = "Не удалось найти страницу для указанного 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 = 'Удержано маны',
        attack_speed_multiplier = 'Скорость атаки',
        damage_effectiveness = 'Эффективность добавленного урона',
        stored_uses = 'Максимум зарядов',
        cooldown = 'Перезарядка',
        vaal_souls_requirement = 'Разовый расход душ',
        vaal_stored_uses = 'Максимум зарядов',
        vaal_soul_gain_prevention_time = 'Нельзя получать души',
        damage_multiplier = 'Множитель урона',
        duration = 'Базовая длительность',
    },
    
    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>маны',
        attack_speed_multiplier = 'Множитель<br>скорости<br>атаки',
        damage_effectiveness = 'Эффект.<br>добавл.<br>урона',
        stored_uses = 'Максимум<br>зарядов',
        cooldown = 'Перезарядка',
        vaal_souls_requirement = 'Разовый<br>расход<br>душ',
        vaal_stored_uses = 'Максимум<br>зарядов',
        vaal_soul_gain_prevention_time = 'Нельзя<br>получать<br>души',
        damage_multiplier = m_util.html.abbr('Множитель<br>урона', 'Наносит x% базового урона'),
        duration = m_util.html.abbr('Базовая<br>длительность', 'Базовая длительность: x секунд'),
        exp_short = 'Опыт',
        exp_long = 'Опыт, необходимый для повышения уровня',
        tot_exp_short = 'Всего<br>опыта',
        tot_exp_long = 'Общий необходимый опыт',
    },
}

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

function h.map_to_arg(tpl_args, frame, properties, prefix_in, map, level)
    if map.order then
        for _, key in ipairs(map.order) do
            row = map.fields[key]
            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
                        tpl_args.skill_levels[level][key] = val
                        -- Nuke variables since they're remapped to skill_levels
                        tpl_args[prefix_in .. row.name] = nil
                    else
                        tpl_args[row.name] = val
                    end
                    properties[row.field] = val
                end
            else
                error(string.format('Programming error, missing field %s from order keys', key))
            end
        end
    end
end

function h.stats(tpl_args, frame, prefix_in, level)
    local type_prefix = string.format('%s%s', prefix_in, 'stat')
    tpl_args.skill_levels[level]['stats'] = {}
    for i=1, data.max_stats_per_level do
        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 = {
            id = tpl_args[stat_id_key],
            value = tonumber(tpl_args[stat_val_key]),
        }
        if stat.id ~= nil and stat.value ~= nil then
            tpl_args.skill_levels[level]['stats'][#tpl_args.skill_levels[level]['stats']+1] = stat
            
            if not tpl_args.test then
                m_cargo.store(frame, {
                    _table = tables.skill_stats_per_level.table,
                    [tables.skill_stats_per_level.fields.level.field] = level,
                    [tables.skill_stats_per_level.fields.id.field] = stat.id,
                    [tables.skill_stats_per_level.fields.value.field] = stat.value,
                })
            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

function h.na(tr)
    tr
        :tag('td')
            :attr('class', 'table-na')
            :wikitext('<p title="Нет данных">Н/Д</p>')
            :done()
end

function h.int_value_or_na(tpl_args, frame, tr, value, pdata)
    value = tonumber(value)
    if value == nil then
        h.na(tr)
    else
        value = mwlanguage:formatNum(value)
        if pdata.fmt ~= nil then
            if type(pdata.fmt) == 'string' then
                value = string.format(pdata.fmt, value)
            elseif type(pdata.fmt) == 'function' then
                value = string.format(pdata.fmt(tpl_args, frame), value)
            end
        end
        tr
            :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 = {
            min = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[1][args.key],
            max = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.key],
        }
        -- property not set for this skill
        if value.min == nil or value.max == nil then
            return
        end
        
        return m_util.html.format_value(tpl_args, frame, value, {
            fmt=args.fmt or tables.progression.fields[args.key].fmt,
            no_color=true,
        })
    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

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

tables.static = {
    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 = {
        -- GrantedEffects.dat
        skill_id = {
            name = 'skill_id',
            field = 'skill_id',
            type = 'String',
            func = nil,
        },
        -- Active Skills.dat
        cast_time = {
            name = 'cast_time',
            field = 'cast_time',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s сек.',
        },
        gem_description = {
            name = 'gem_description',
            field = 'description',
            type = 'Text',
            func = nil,
        },
        active_skill_name = {
            name = 'active_skill_name',
            field = 'active_skill_name',
            type = 'String',
            func = nil,
        },
        skill_icon = {
            name = 'skill_icon',
            field = 'skill_icon',
            type = 'Page',
            func = function(tpl_args, frame)
                if tpl_args.active_skill_name then
                    return string.format(i18n.skill_icon, tpl_args.active_skill_name)
                end
            end,
        },
        item_class_id_restriction = {
            name = '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('Invalid item class id: %s', v))
                    end
                end
                return value
            end,
        },
        item_class_restriction = {
            name = '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 = 'projectile_speed',
            field = 'projectile_speed',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        -- Misc data derieved from stats
        stat_text = {
            name = 'stat_text',
            field = 'stat_text',
            type = 'Text',
            func = nil,
        },
        quality_stat_text = {
            name = 'quality_stat_text',
            field = 'quality_stat_text',
            type = 'Text',
            func = nil,
        },
        -- 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 = {
            name = 'radius',
            field = 'radius',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        radius_description = {
            name = 'radius_description',
            field = 'radius_description',
            type = 'Text',
            func = nil,
        },
        radius_secondary = {
            name = 'radius_secondary',
            field = 'radius_secondary',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        radius_secondary_description = {
            name = 'radius_secondary_description',
            field = 'radius_secondary_description',
            type = 'Text',
            func = nil,
        },
        -- not sure if any skill actually has 3 radius componets
        radius_tertiary = {
            name = 'radius_tertiary',
            field = 'radius_tertiary',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        radius_tertiary_description = {
            name = 'radius_tertiary_description',
            field = 'radius_tertiary_description',
            type = 'Text',
            func = nil,
        },
        -- Set manually
        max_level = {
            field = 'max_level',
            type = 'Integer',
        },
        html = {
            field = 'html',
            type = 'Text',
        },
        skill_screenshot = {
            name = '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.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.skill_screenshot, tpl_args.active_skill_name)
                    page = mw.title.new(ss)
                    if page == nil or not page.exists then
                        ss = nil
                    end
                end
                return ss
            end,
        },
    },
}

tables.progression = {
    table = 'skill_levels',
    order = {'level_requirement', 'dexterity_requirement', 'intelligence_requirement', 'strength_requirement', 'mana_multiplier', 'critical_strike_chance', 'mana_cost', 'attack_speed_multiplier', 'damage_effectiveness', 'stored_uses', 'cooldown', 'vaal_souls_requirement', 'vaal_stored_uses', 'vaal_soul_gain_prevention_time', 'damage_multiplier', 'duration', 'experience', 'stat_text'},
    fields = {
        level = {
            name = nil,
            field = 'level',
            type = 'Integer',
            func = nil,
            header = nil,
        },
        level_requirement = {
            name = 'level_requirement',
            field = 'level_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.level_requirement,
        },
        dexterity_requirement = {
            name = 'dexterity_requirement',
            field = 'dexterity_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.dexterity_requirement,
        },
        strength_requirement = {
            name = 'strength_requirement',
            field = 'strength_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.strength_requirement,
        },
        intelligence_requirement = {
            name = 'intelligence_requirement',
            field = 'intelligence_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.intelligence_requirement,
        },
        mana_multiplier = {
            name = 'mana_multiplier',
            field = 'mana_multiplier',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.mana_multiplier,
            fmt = '%s%%',
        },
        critical_strike_chance = {
            name = 'critical_strike_chance',
            field = 'critical_strike_chance',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.critical_strike_chance,
            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_reserved
                else
                    return i18n.progression.mana_cost
                end
            end,
            fmt = function (tpl_args, frame)
                if tpl_args.has_percentage_mana_cost or (tpl_args.skill_data and tpl_args.skill_data['skill.has_percentage_mana_cost']) then
                    str = '%s%%'
                else
                    str = '%s'
                end
            
                return str
            end,
        },
        damage_effectiveness = {
            name = 'damage_effectiveness',
            field = 'damage_effectiveness',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.damage_effectiveness,
            fmt = '%s%%',
        },
        stored_uses = {
            name = 'stored_uses',
            field = 'stored_uses',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.stored_uses,
        },
        cooldown = {
            name = 'cooldown',
            field = 'cooldown',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.cooldown,
            fmt = '%s сек.',
        },
        vaal_souls_requirement = {
            name = 'vaal_souls_requirement',
            field = 'vaal_souls_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_souls_requirement,
        },
        vaal_stored_uses = {
            name = '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 = 'vaal_soul_gain_prevention_time',
            field = 'vaal_soul_gain_prevention_time',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_soul_gain_prevention_time,
            fmt = '%s сек.',
        },
        damage_multiplier = {
            name = 'damage_multiplier',
            field = 'damage_multiplier',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.damage_multiplier,
            fmt = '%s%%',
        },
        attack_speed_multiplier = {
            name = 'attack_speed_multiplier',
            field = 'attack_speed_multiplier',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.attack_speed_multiplier,
            fmt = '%s%%',
        },
        duration = {
            name = 'duration',
            field = 'duration',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.duration,
            fmt = '%s сек.',
        },
        -- from gem experience, optional
        experience = {
            name = 'experience',
            field = 'experience',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            hide = true,
        },
        stat_text = {
            name = 'stat_text',
            field = 'stat_text',
            type = 'Text',
            func = nil,
            hide = true,
        },
    }
}

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', 'duration', 'attack_speed_multiplier'}

tables.skill_stats_per_level = {
    table = 'skill_stats_per_level',
    fields = {
        level = {
            field = 'level',
            type = 'Integer',
        },
        id = {
            field = 'id',
            type = 'String',
        },
        value = {
            field = 'value',
            type = 'Integer',
        },
    },
}

data.max_stats_per_level = 8

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.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 = h.display.factory.value{key='cast_time'},
    },
    {
        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.mana_cost,
        func = h.display.factory.range_value{key='mana_cost'},
    },
    {
        header = i18n.infobox.attack_speed_multiplier,
        func = h.display.factory.range_value{key='attack_speed_multiplier'},
    },
    {
        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.damage_multiplier,
        func = h.display.factory.range_value{key='damage_multiplier'},
    },
    {
        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',
    },
}

-- ----------------------------------------------------------------------------
-- Templates
-- ----------------------------------------------------------------------------
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_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}

--
-- Template:Skill
--
function p.skill(frame, tpl_args)
    --[[
    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)
    
    --
    -- Args
    --
    local properties
    
    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
    
    -- Handle level progression
    local i = 0
    repeat 
        i = i + 1
        local prefix = 'level' .. i 
        local level = m_util.cast.boolean(tpl_args[prefix])
        if level == true then
            -- Don't need this anymore
            tpl_args[prefix] = nil
            tpl_args.skill_levels[i] = {}
            prefix = prefix .. '_'
        
            if tpl_args[prefix .. 'experience'] ~= nil then
                tpl_args.max_level = i
            end
            
            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.stats(tpl_args, frame, prefix, i)
        end
    until level ~= true
    -- If no experience is given, assume this is a non skill gem skill.
    tpl_args.max_level = tpl_args.max_level or (i - 1)
    -- handle static progression
    properties = {
        _table = tables.progression.table,
        [tables.progression.fields.level.field] = 0
    }
    h.map_to_arg(tpl_args, frame, properties, 'static_', tables.progression, 0)
    if not tpl_args.test then
        m_cargo.store(frame, properties)
    end
    
    -- Handle static arguments
    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.stats(tpl_args, frame, 'static_', 0)
    
    
    --
    -- Infobox progressing
    --
    local infobox = mw.html.create('span')
    infobox:attr('class', 'skill-box')
    
    -- tablular sections
    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 then
            local tr = tbl:tag('tr')
            if infobox_data.header then
                tr
                    :tag('th')
                        :wikitext(infobox_data.header)
                        :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
    
    --
    --
    --
    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 = '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.intro_named_id, 
            tpl_args.skill_id, 
            tpl_args.active_skill_name
        )
    else
        out[#out+1] = string.format(
            i18n.intro_unnamed_id, 
            tpl_args.skill_id
        )
    end

    -- Attach tables
    if not tpl_args.test then
        local attach_tables = {
            tables.static.table,
            tables.progression.table,
            tables.skill_stats_per_level.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
        for _, table_name in ipairs(attach_tables) do
            frame:expandTemplate{title=string.format('Template:Skill/cargo/attach/%s', table_name), args={}}
        end
    end
    
    return tostring(container) .. m_util.misc.add_category({'Данные умений'}) .. '\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 prefix
    for i=1, 9 do
        prefix = 'c' .. i .. '_' 
        local format_keys = {
            'header', 
            'abbr', 
            'pattern_extract', 
            'pattern_value'
        }
        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
        end
        
        if m_util.table.has_one_value(statfmt, format_keys) then
            error(string.format(i18n.errors.all_format_keys_specified, i))
        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
    
    
    local results = {}
    local query = {
        groupBy = 'skill._pageID',
    }
    local skill_data
    
    local fields = {
        'skill._pageName',
        'skill.has_reservation_mana_cost',
        'skill.has_percentage_mana_cost',
    }
    
    if tpl_args.skill_id then
        query.where = string.format(
            'skill.skill_id="%s"', 
            tpl_args.skill_id
        ) 
        results = m_cargo.query({'skill'}, fields, query)
        if #results == 0 then
            error(i18n.errors.no_results_for_skill_id)
        end
        skill_data = results[1]
    else
        if tpl_args.page then
            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
            error(i18n.errors.no_results_for_skill_page)
        end
        
        skill_data = results[1]
    end
    
    tpl_args.skill_data = skill_data
    
    skill_data["skill.has_reservation_mana_cost"] = m_util.cast.boolean(skill_data["skill.has_reservation_mana_cost"])
    skill_data['skill.has_percentage_mana_cost'] = m_util.cast.boolean(skill_data["skill.has_percentage_mana_cost"])
    
    query.where = string.format(
        'skill_levels._pageName="%s"', 
        skill_data['skill._pageName']
    )
    fields = {}
    for _, pdata in pairs(tables.progression.fields) do
        fields[#fields+1] = string.format('skill_levels.%s', pdata.field)
    end
    
    results = m_cargo.query(
        {'skill_levels'}, 
        fields, 
        {
            where=string.format(
                'skill_levels._pageName="%s" AND skill_levels.level > 0', 
                skill_data['skill._pageName']
            ),
            groupBy='skill_levels._pageID, skill_levels.level',
            orderBy='skill_levels.level ASC',
        }
    )
    
    if #results == 0 then
        error(i18n.errors.missing_level_data)
    end
    
    headers = {}
    for i, row in ipairs(results) 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')
    head
        :tag('th')
            :wikitext('Ур.')
            :done()
    
    for _, key in ipairs(data.progression_display_order) do
        local pdata = tables.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
                :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['skill_levels.experience'] then
        head
            :tag('th')
                :wikitext(m_util.html.abbr(
                    i18n.progression.exp_short, 
                    i18n.progression.exp_long
                    )
                )
                :done()
            :tag('th')
                :wikitext(m_util.html.abbr(
                    i18n.progression.tot_exp_short, 
                    i18n.progression.tot_exp_long
                    )
                )
                :done()
    end
    
    local tblrow
    local lastexp = 0
    local experience
    
    for i, row in ipairs(results) do
        tblrow = tbl:tag('tr')
        tblrow
            :tag('th')
                :wikitext(row['skill_levels.level'])
                :done()
        
        for _, key in ipairs(data.progression_display_order) do
            local pdata = tables.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
        
        -- stats
        if row['skill_levels.stat_text'] then
            stats = m_util.string.split(
                row['skill_levels.stat_text'], 
                '<br>'
            )
        else
            stats = {}
        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
                h.na(tblrow)
            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['skill_levels.experience'] then
            experience = tonumber(row['skill_levels.experience'])
            if experience ~= nil then
                h.int_value_or_na(
                    tpl_args, 
                    frame, 
                    tblrow, 
                    experience - lastexp, 
                    {}
                )
                
                lastexp = experience
            else
                h.na(tblrow)
            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

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

return p