Module:Skill: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
(Revised cost parameters and database structure for parity with PyPoE. Supports old parameters for now until skills are updated with PyPoE.)
(The skill_icon parameter can be used to specify an override for the default icon based on the name of the skill; The skill_screenshot_file parameter is now deprecated.)
 
(17 intermediate revisions by 2 users not shown)
Line 6: Line 6:
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------


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


-- Should we use the sandbox version of our submodules?
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Skill')
local use_sandbox = m_util.misc.maybe_sandbox('Skill')
local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or mw.loadData('Module:Game')


-- The cfg table contains all localisable strings and configuration, to make it
-- The cfg table contains all localisable strings and configuration, to make it
Line 30: Line 30:
local h = {}
local h = {}


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


function h.costs(tpl_args, frame, prefix_in, level)
function h.expand_costs_data(tpl_args, skill_levels)
     tpl_args.skill_costs = tpl_args.skill_costs or {}
     --[[
     for i=1, #tpl_args.skill_costs do
    Expand costs data so that each cost type has its own column with amounts
         local cost_prefix = string.format('%s%s%d_', prefix_in, i18n.parameters.skill.cost, i) -- level<level>_cost<i>_
    Assumptions:
         local cost = {
      Cost types are always static
             amount = tpl_args[cost_prefix .. tables.skill_level_costs.fields.amount.name], --level<level>_cost<i>_amount
      Cost amounts can either be static or leveled, but not both
        }
    --]]
        if cost.amount ~= nil then
     if skill_levels[0] then
            local properties = {
         local cost_types = m_util.cast.table(skill_levels[0].cost_types)
                _table = tables.skill_level_costs.table,
         if #cost_types > 0 then
                [tables.skill_level_costs.fields.set_id.field] = i,
             for _, level_data in pairs(skill_levels) do
                [tables.skill_level_costs.fields.level.field] = level,
                if type(level_data) == 'table' and level_data.cost_amounts then
            }
                    local cost_amounts = m_util.cast.table(level_data.cost_amounts, {callback = m_util.cast.number})
            h.map_to_arg(tpl_args, frame, properties, cost_prefix, tables.skill_level_costs, level, 'costs', i)
                    for i=1, #cost_types do
            if not tpl_args.test then
                        local type = cost_types[i]
                 m_cargo.store(frame, properties)
                        local amount = cost_amounts[i]
                        if amount then
                            level_data['cost_' .. type] = amount
                        end
                    end
                 end
             end
             end
         end
         end
Line 102: Line 107:
end
end


function h.stats(tpl_args, frame, prefix_in, level)
function h.stats(tpl_args, prefix_in, level)
     for i=1, cfg.max_stats_per_level do
     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_prefix = string.format('%s%s%d_', prefix_in, i18n.parameters.skill.stat, i) -- level<level>_stat<i>_
Line 114: Line 119:
                 [tables.skill_stats_per_level.fields.level.field] = level,
                 [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)
             h.map_to_arg(tpl_args, properties, stat_prefix, tables.skill_stats_per_level, level, 'stats', i)
             tpl_args.skill_levels.has_stats = true
             tpl_args.skill_levels.has_stats = true
             if not tpl_args.test then
             if not tpl_args.test then
                 m_cargo.store(frame, properties)
                 m_cargo.store(properties)
             end
             end
         end
         end
Line 123: Line 128:
end
end


function h.int_value_or_na(tpl_args, frame, tblrow, value, tmap)
function h.int_value_or_na(tpl_args, tblrow, value, tmap)
     value = tonumber(value)
     value = tonumber(value)
     if value == nil then
     if value == nil then
Line 133: Line 138:
                 value = string.format(tmap.fmt, value)
                 value = string.format(tmap.fmt, value)
             elseif type(tmap.fmt) == 'function' then
             elseif type(tmap.fmt) == 'function' then
                 value = string.format(tmap.fmt(tpl_args, frame) or '%s', value)
                 value = string.format(tmap.fmt(tpl_args) or '%s', value)
             end
             end
         end
         end
Line 145: Line 150:
h.cast = {}
h.cast = {}
function h.cast.wrap(func)
function h.cast.wrap(func)
     return function(tpl_args, frame, value)
     return function(tpl_args, value)
         if value == nil then
         if value == nil then
             return nil
             return nil
Line 156: Line 161:
h.display.factory = {}
h.display.factory = {}
function h.display.factory.value(args)
function h.display.factory.value(args)
     return function (tpl_args, frame)
     return function (tpl_args)
         args.fmt = args.fmt or tables.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]
Line 168: Line 173:


function h.display.factory.range_value(args)
function h.display.factory.range_value(args)
     return function (tpl_args, frame)
     return function (tpl_args)
         local value = {}
         local value = {}
         if args.set_name and args.set_id then
         if args.set_name and args.set_id then
Line 201: Line 206:
         local map = args.map or tables.progression
         local map = args.map or tables.progression
         local options = {
         local options = {
             fmt=args.fmt or map.fields[args.key].fmt,
             fmt=args.fmt or map.fields[args.key] and map.fields[args.key].fmt,
             color=false,
             color=false,
         }
         }
Line 207: Line 212:
             local formatted_values = {}
             local formatted_values = {}
             for i=1, #value.min do
             for i=1, #value.min do
                 formatted_values[i] = m_util.html.format_value(tpl_args, frame, {min = value.min[i], max = value.max[i]}, options)
                 formatted_values[i] = m_util.html.format_value(tpl_args, {min = value.min[i], max = value.max[i]}, options)
             end
             end
             return formatted_values
             return formatted_values
         end
         end
         return m_util.html.format_value(tpl_args, frame, value, options)
         return m_util.html.format_value(tpl_args, value, options)
     end
     end
end
end


function h.display.factory.radius(args)
function h.display.factory.radius(args)
     return function (tpl_args, frame)
     return function (tpl_args)
         local radius = tpl_args['radius' .. args.key]
         local radius = tpl_args['radius' .. args.key]
         if radius == nil then
         if radius == nil then
Line 228: Line 233:
         end
         end
     end
     end
end
function h.query_skill(tpl_args)
    local fields = {
        'skill._pageID=_pageID',
    }
    local query = {
        groupBy = 'skill._pageID',
    }
    local results = {}
    local search_param
    if tpl_args.skill_id then -- Query by skill id
        query.where = string.format('skill_id="%s"', tpl_args.skill_id)
        search_param = 'skill_id'
    else -- Query by page name
        local page = tpl_args.page or mw.title.getCurrentTitle().prefixedText
        query.where = string.format('_pageName="%s"', page)
        search_param = 'page'
    end
    results = m_cargo.query({tables.static.table}, fields, query)
    if #results == 0 then
        -- No results found
        error(string.format(i18n.errors.validate_skill.no_results_found, search_param, tpl_args[search_param]))
    elseif #results > 1 then
        -- More than one result found
        error(string.format(i18n.errors.validate_skill.many_results_found, search_param, tpl_args[search_param]))
    end
    return results[1]
end
end


Line 268: Line 301:
             field = 'skill_icon',
             field = 'skill_icon',
             type = 'Page',
             type = 'Page',
             func = function(tpl_args, frame)
             func = function(tpl_args, value)
                 if tpl_args.active_skill_name then
                 if value then
                     return string.format(i18n.files.skill_icon, tpl_args.active_skill_name)
                    value = string.format(i18n.files.skill_icon, value)
                elseif tpl_args.active_skill_name then
                     value = string.format(i18n.files.skill_icon, tpl_args.active_skill_name)
                else
                    value = nil
                 end
                 end
                return value
             end,
             end,
         },
         },
Line 278: Line 316:
             field = 'item_class_id_restriction',
             field = 'item_class_id_restriction',
             type = 'List (,) of String',
             type = 'List (,) of String',
             func = function(tpl_args, frame, value)
             func = function(tpl_args, value)
                 if value == nil then  
                 if value == nil then  
                     return nil
                     return nil
Line 295: Line 333:
             field = 'item_class_restriction',
             field = 'item_class_restriction',
             type = 'List (,) of String',
             type = 'List (,) of String',
             func = function(tpl_args, frame, value)
             func = function(tpl_args, value)
                 if tpl_args.item_class_id_restriction == nil then
                 if tpl_args.item_class_id_restriction == nil then
                     return
                     return
Line 319: Line 357:
             name = i18n.parameters.skill.stat_text,
             name = i18n.parameters.skill.stat_text,
             field = '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',
             type = 'Text',
             func = nil,
             func = nil,
Line 369: Line 401:
             field = 'skill_screenshot',
             field = 'skill_screenshot',
             type = 'Page',
             type = 'Page',
             func = function(tpl_args, frame)
             func = function(tpl_args, value)
                local ss
                 if tpl_args.skill_screenshot_file then
                 if tpl_args.skill_screenshot_file ~= nil then
                    tpl_args._flags.has_deprecated_skill_parameters = true
                     ss = string.format('File:%s', tpl_args.skill_screenshot_file)
                     value = string.format('File:%s', tpl_args.skill_screenshot_file)
                 elseif tpl_args.skill_screenshot ~= nil then
                 elseif value then
                     ss = string.format(i18n.files.skill_screenshot, tpl_args.skill_screenshot)
                     value = string.format(i18n.files.skill_screenshot, value)
                 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 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)
                     value = string.format(i18n.files.skill_screenshot, tpl_args.active_skill_name)
                     local page = mw.title.new(ss)
                     local page = mw.title.new(value)
                     if page == nil or not page.exists then
                     if page == nil or not page.exists then
                         ss = nil
                         value = nil
                     end
                     end
                else
                    value = nil
                 end
                 end
                 return ss
                 return value
             end,
             end,
         },
         },
Line 399: Line 433:
             func = nil,
             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,
        -- },
     },
     },
}
}
Line 452: Line 469:
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
         },
         },
         mana_multiplier = {
         cost_multiplier = {
             name = i18n.parameters.skill.mana_multiplier,
             name = i18n.parameters.skill.cost_multiplier,
             field = 'mana_multiplier',
             field = 'cost_multiplier',
             type = 'Float',
             type = 'Float',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
             fmt = '%s%%',
             fmt = '%s%%',
        },
        attack_time = {
            name = i18n.parameters.skill.attack_time,
            field = 'attack_time',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%.2f ' .. m_game.units.seconds.short_lower,
         },
         },
         critical_strike_chance = {
         critical_strike_chance = {
Line 531: Line 555:
             field = 'cost_types',
             field = 'cost_types',
             type = 'List (,) of String',
             type = 'List (,) of String',
             func = function(tpl_args, frame, value)
             func = function(tpl_args, value)
                 if value == nil then  
                 if value == nil then  
                     return nil
                     return nil
Line 548: Line 572:
             field = 'cost_amounts',
             field = 'cost_amounts',
             type = 'List (,) of Integer',
             type = 'List (,) of Integer',
             func = function(tpl_args, frame, value)
             func = function(tpl_args, value)
                 if value == nil then  
                 if value == nil then  
                     return nil
                     return nil
Line 593: Line 617:
             func = h.cast.wrap(m_util.cast.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),
        },
    },
}
}


Line 747: Line 706:
     },
     },
     {
     {
         field = 'mana_multiplier',
         field = 'cost_multiplier',
         header = i18n.progression.mana_multiplier,
         header = i18n.progression.cost_multiplier,
         fmt = '%s%%',
         fmt = '%s%%',
    },
    {
        field = 'attack_time',
        header = i18n.progression.attack_time,
        fmt = '%.2f ' .. m_game.units.seconds.short_lower,
     },
     },
     {
     {
         field = 'critical_strike_chance',
         field = 'critical_strike_chance',
         header = i18n.progression.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%%',
         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 = 'cost_Mana',
         field = 'cost_Mana',
Line 819: Line 746:
         fmt = '%s%%',
         fmt = '%s%%',
     },
     },
    --[[{
        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 = 'mana_reservation_flat',
         field = 'mana_reservation_flat',
Line 906: Line 815:
     {
     {
         header = i18n.infobox.skill_id,
         header = i18n.infobox.skill_id,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.skill_id)
             return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.skill_id)
         end  
         end  
Line 912: Line 821:
     {
     {
         header = i18n.infobox.skill_icon,
         header = i18n.infobox.skill_icon,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             if tpl_args.skill_icon then  
             if tpl_args.skill_icon then  
                 return string.format('[[%s]]', tpl_args.skill_icon)
                 return string.format('[[%s]]', tpl_args.skill_icon)
Line 920: Line 829:
     {
     {
         header = i18n.infobox.cast_time,
         header = i18n.infobox.cast_time,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local value = tpl_args.cast_time
             local value = tpl_args.cast_time
             if value then
             if value then
Line 933: Line 842:
     {
     {
         header = i18n.infobox.item_class_restrictions,
         header = i18n.infobox.item_class_restrictions,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             if tpl_args.item_class_restriction == nil then
             if tpl_args.item_class_restriction == nil then
                 return
                 return
Line 966: Line 875:
     -- ignore attrbiutes?
     -- ignore attrbiutes?
     {
     {
         header = i18n.infobox.mana_multiplier,
         header = i18n.infobox.cost_multiplier,
         func = h.display.factory.range_value{key='mana_multiplier'},
         func = h.display.factory.range_value{key='cost_multiplier'},
    },
    {
        header = i18n.infobox.attack_time,
        func = h.display.factory.range_value{key='attack_time'},
     },
     },
     {
     {
Line 975: Line 888:
     {
     {
         header = i18n.infobox.cost,
         header = i18n.infobox.cost,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local sets = {}
             local parts = {}
             local cost_types = h.display.factory.range_value{key='cost_types'}(tpl_args, frame)
             for k, v in pairs(m_game.constants.skill.cost_types) do
            if type(cost_types) == 'table' and #cost_types > 0 then
                 local key = 'cost_' .. k
                 local cost_amounts = h.display.factory.range_value{key='cost_amounts'}(tpl_args, frame)
                 local fmt
                 for i=1, #cost_types do
                if string.find(k, 'Percent', 1, true) then
                    local cost_type = cost_types[i]
                    fmt = '%s%% %s'
                    local cost_amount = cost_amounts[i]
                else
                    local fmt
                    fmt = '%s %s'
                    if string.find(cost_type, 'Percent', 1, true) then
                        fmt = '%s%% %s'
                    else
                        fmt = '%s %s'
                    end
                    sets[#sets+1] = string.format(fmt, cost_amount, m_game.constants.skill.cost_types[cost_type].long_lower)
                 end
                 end
            else
                 local range = h.display.factory.range_value{key=key}(tpl_args)
                 -- Try falling back to deprecated parameters
                if range then
                for i=1, #tpl_args.skill_costs do
                    parts[#parts+1] = string.format(fmt, range, v.long_lower)
                    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_lower)
                        end
                    end
                 end
                 end
             end
             end
             return table.concat(sets, ', ')
             return table.concat(parts, ', ')
 
            -- 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
           
         end,
         end,
     },
     },
     {
     {
         header = i18n.infobox.reservation,
         header = i18n.infobox.reservation,
         func = function (tpl_args, frame)
         func = function (tpl_args)
             local sets = {}
             local parts = {}
             local keys = {
             local keys = {
                 {
                 {
Line 1,048: Line 929:
             }
             }
             for _, v in ipairs(keys) do
             for _, v in ipairs(keys) do
                 local range = h.display.factory.range_value{key=v.key}(tpl_args, frame)
                 local range = h.display.factory.range_value{key=v.key}(tpl_args)
                 if range then
                 if range then
                     sets[#sets+1] = string.format(v.fmt, range)
                     parts[#parts+1] = string.format(v.fmt, range)
                end
            end
            if #sets == 0 then
                -- Try falling back to deprecated parameters
                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_lower)
                        end
                    end
                 end
                 end
             end
             end
             return table.concat(sets, ', ')
             return table.concat(parts, ', ')
 
            -- 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
 
         end,
         end,
     },
     },
Line 1,141: Line 988:


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Exported functions
-- Main functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
local p = {}


p.table_skills = m_cargo.declare_factory{data=tables.static}
local function _process_skill_data(tpl_args)
p.table_skill_levels = m_cargo.declare_factory{data=tables.progression}
    --[[
p.table_skill_costs = m_cargo.declare_factory{data=tables.skill_costs}
    Processes skill data from tpl_args.
p.table_skill_level_costs = m_cargo.declare_factory{data=tables.skill_level_costs}
    Stores skill data in cargo tables.
p.table_skill_stats_per_level = m_cargo.declare_factory{data=tables.skill_stats_per_level}
    Attaches page to cargo tables.
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 = tpl_args or {}
     tpl_args._flags = tpl_args._flags or {}
     tpl_args._flags = tpl_args._flags or {}
Line 1,180: Line 1,018:
         if q.stat_text then
         if q.stat_text then
             tpl_args.skill_quality[#tpl_args.skill_quality+1] = q
             tpl_args.skill_quality[#tpl_args.skill_quality+1] = q
             m_cargo.store(frame, q)
             m_cargo.store(q)
              
              
             q.stats = {}
             q.stats = {}
Line 1,196: Line 1,034:
                 if s.id and s.value then
                 if s.id and s.value then
                     q.stats[#q.stats+1] = s
                     q.stats[#q.stats+1] = s
                     m_cargo.store(frame, s)
                     m_cargo.store(s)
                 end
                 end
                  
                  
Line 1,206: Line 1,044:
         -- Gem has alternative qualtiy
         -- Gem has alternative qualtiy
         tpl_args._flags.is_alt_quality_gem = true
         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.name] == 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
     end
      
      
Line 1,253: Line 1,065:
             [tables.progression.fields.level.field] = i
             [tables.progression.fields.level.field] = i
         }
         }
         h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, i)
         h.map_to_arg(tpl_args, properties, prefix, tables.progression, i)
         if not tpl_args.test then
         if not tpl_args.test then
             m_cargo.store(frame, properties)
             m_cargo.store(properties)
         end
         end
        h.costs(tpl_args, frame, prefix, i)
         h.stats(tpl_args, prefix, i)
         h.stats(tpl_args, frame, prefix, i)
     end
     end
     tpl_args.max_level = tpl_args.max_level or level_count
     tpl_args.max_level = tpl_args.max_level or level_count
Line 1,269: Line 1,080:
             [tables.progression.fields.level.field] = 0
             [tables.progression.fields.level.field] = 0
         }
         }
         h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, 0)
         h.map_to_arg(tpl_args, properties, prefix, tables.progression, 0)
         if not tpl_args.test then
         if not tpl_args.test then
             m_cargo.store(frame, properties)
             m_cargo.store(properties)
         end
         end
     end
     end
    -- Expand costs data
    h.expand_costs_data(tpl_args, tpl_args.skill_levels)
      
      
     -- Handle static arguments
     -- Handle static arguments
Line 1,280: Line 1,094:
         [tables.static.fields.max_level.field] = tpl_args.max_level
         [tables.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, properties, '', tables.static)
    h.costs(tpl_args, frame, prefix, 0)
     h.stats(tpl_args, prefix, 0)
     h.stats(tpl_args, frame, prefix, 0)


     -- Build infobox
     -- Build infobox
     local infobox = mw.html.create('span')
     local infobox = mw.html.create('span')
     infobox:attr('class', 'skill-box')
     infobox:addClass('skill-box')
     local tbl = infobox:tag('table')
     local tbl = infobox:tag('table')
     tbl:attr('class', 'wikitable skill-box-table')
     tbl:addClass('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)
         if type(display) == 'string' and string.len(display) > 0 then
         if type(display) == 'string' and string.len(display) > 0 then
             if infobox_data.fmt ~= nil then
             if infobox_data.fmt ~= nil then
Line 1,296: Line 1,109:
                     display = string.format(infobox_data.fmt, display)
                     display = string.format(infobox_data.fmt, display)
                 elseif type(infobox_data.fmt) == 'function' then
                 elseif type(infobox_data.fmt) == 'function' then
                     display = string.format(infobox_data.fmt(tpl_args, frame) or '%s', display)
                     display = string.format(infobox_data.fmt(tpl_args) or '%s', display)
                 end
                 end
             end
             end
Line 1,303: Line 1,116:
                 local header_text
                 local header_text
                 if type(infobox_data.header) == 'function' then
                 if type(infobox_data.header) == 'function' then
                     header_text = infobox_data.header(tpl_args, frame)
                     header_text = infobox_data.header(tpl_args)
                 else
                 else
                     header_text = infobox_data.header
                     header_text = infobox_data.header
Line 1,314: Line 1,127:
             local td = tr:tag('td')
             local td = tr:tag('td')
             td:wikitext(display)
             td:wikitext(display)
             td:attr('class', infobox_data.class or 'tc -value')
             td:addClass(infobox_data.class or 'tc -value')
             if infobox_data.header == nil then
             if infobox_data.header == nil then
                 td:attr('colspan', 2)
                 td:attr('colspan', 2)
Line 1,325: Line 1,138:
     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
         m_cargo.store(frame, properties)
         m_cargo.store(properties)
     end
     end


Line 1,337: Line 1,150:
             attach_tables[#attach_tables+1] = tables.skill_quality.table
             attach_tables[#attach_tables+1] = tables.skill_quality.table
             attach_tables[#attach_tables+1] = tables.skill_quality_stats.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
         end
         if tpl_args.skill_levels.has_stats then
         if tpl_args.skill_levels.has_stats then
Line 1,346: Line 1,155:
         end
         end
         for _, table_name in ipairs(attach_tables) do
         for _, table_name in ipairs(attach_tables) do
             frame:expandTemplate{
             mw.getCurrentFrame():expandTemplate{
                 title = string.format(i18n.templates.cargo_attach, table_name),
                 title = string.format(i18n.templates.cargo_attach, table_name),
                 args = {}
                 args = {}
Line 1,361: Line 1,170:
end
end


--
local function _skill(tpl_args)
-- Template:Skill
--
function p.skill(frame)
     --[[
     --[[
     Display skill infobox
     Display skill infobox
Line 1,373: Line 1,179:


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


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


     -- Container
     -- Container
     local container = mw.html.create('span')
     local container = mw.html.create('span')
     container
     container
         :attr('class', 'skill-box-page-container')
         :addClass('skill-box-page-container')
         :wikitext(infobox)
         :wikitext(infobox)
     if tpl_args.skill_screenshot then
     if tpl_args.skill_screenshot then
Line 1,393: Line 1,194:


     -- Generic messages on the page:
     -- Generic messages on the page:
     out = {}
     local 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] = mw.getCurrentFrame():expandTemplate{
             title = i18n.templates.incorrect_title,
             title = i18n.templates.incorrect_title,
             args = {title=tpl_args.skill_id}
             args = {title=tpl_args.skill_id}
Line 1,422: Line 1,223:
end
end


function p.progression(frame)
local function _progression(tpl_args)
     --[[
     --[[
         Displays the level progression for the skill gem.  
         Displays the level progression for the skill gem.  
Line 1,430: Line 1,231:
         = p.progression{page='Reave'}
         = p.progression{page='Reave'}
     ]]
     ]]
   
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
      
      
     -- Parse column arguments:
     -- Parse column arguments:
Line 1,463: Line 1,259:
      
      
     -- Query skill data
     -- Query skill data
    local results = {}
     local skill_data = h.query_skill(tpl_args)
     local skill_data
    local fields = {
        '_pageID',
        -- tables.static.fields.has_reservation_mana_cost.field,
        -- tables.static.fields.has_percentage_mana_cost.field,
    }
    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.field] = m_util.cast.boolean(skill_data[tables.static.fields.has_reservation_mana_cost.field])
    -- skill_data[tables.static.fields.has_percentage_mana_cost.field] = m_util.cast.boolean(skill_data[tables.static.fields.has_percentage_mana_cost.field])
    tpl_args.skill_data = skill_data


     -- Query progression data
     -- Query progression data
     fields = {}
     local fields = {}
     for _, fmap in pairs(tables.progression.fields) do
     for _, fmap in pairs(tables.progression.fields) do
         fields[#fields+1] = fmap.field
         fields[#fields+1] = fmap.field
     end
     end
     query = {
     local query = {
         where = string.format(
         where = string.format(
             '_pageID="%s"',
             '_pageID="%s"',
             skill_data['_pageID']
             skill_data._pageID
         ),
         ),
         groupBy = string.format(
         groupBy = string.format(
Line 1,507: Line 1,276:
         ),
         ),
     }
     }
     results = m_cargo.query({tables.progression.table}, fields, query)
     local results = m_cargo.query({tables.progression.table}, fields, query)
      
      
     -- Re-index by level
     -- Re-index by level
Line 1,517: Line 1,286:
         error(i18n.errors.progression.missing_level_data)
         error(i18n.errors.progression.missing_level_data)
     end
     end
    -- 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--]]


     -- Expand costs data
     -- Expand costs data
     if skill_data.levels[0] then
     h.expand_costs_data(tpl_args, skill_data.levels)
        -- Assume that cost_types are static
        local cost_types = m_util.cast.table(skill_data.levels[0].cost_types)
        if #cost_types > 0 then
            for _, level_data in ipairs(skill_data.levels) do
                local cost_amounts = m_util.cast.table(level_data.cost_amounts, {callback = m_util.cast.number})
                for i=1, #cost_types do
                    local type = cost_types[i]
                    local amount = cost_amounts[i]
                    if amount then
                        -- If amount is leveled, add column
                        level_data['cost_' .. type] = amount
                    end
                end
            end
        end
    end
      
      
     -- Set up html table headers
     -- Set up html table headers
     headers = {}
     local headers = {}
     for _, row in ipairs(skill_data.levels) do
     for _, row in ipairs(skill_data.levels) do
         for k, v in pairs(row) do
         for k, v in pairs(row) do
Line 1,611: Line 1,299:
     local tbl = mw.html.create('table')
     local tbl = mw.html.create('table')
     tbl
     tbl
         :attr('class', 'wikitable responsive-table skill-progression-table')
         :addClass('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
     for _, tmap in pairs(data.skill_progression_table) do
         if headers[tmap.field] then
         if headers[tmap.field] then
             local text = type(tmap.header) == 'function' and tmap.header(tpl_args, frame) or tmap.header
             local text = type(tmap.header) == 'function' and tmap.header(tpl_args) or tmap.header
             head
             head
                 :tag('th')
                 :tag('th')
Line 1,646: Line 1,334:
         for _, tmap in pairs(data.skill_progression_table) do
         for _, tmap in pairs(data.skill_progression_table) do
             if headers[tmap.field] then
             if headers[tmap.field] then
                 h.int_value_or_na(tpl_args, frame, tblrow, row[tmap.field], tmap)
                 h.int_value_or_na(tpl_args, tblrow, row[tmap.field], tmap)
             end
             end
         end
         end
Line 1,688: Line 1,376:
             end
             end
         end
         end
       
        -- TODO: Quality stats, afaik no gems use this atm
          
          
         if headers[tables.progression.fields.experience.field] then
         if headers[tables.progression.fields.experience.field] then
             experience = tonumber(row[tables.progression.fields.experience.field])
             experience = tonumber(row[tables.progression.fields.experience.field])
             if experience ~= nil then
             if experience ~= nil then
                 h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
                 h.int_value_or_na(tpl_args, tblrow, experience - lastexp, {})
                 lastexp = experience
                 lastexp = experience
             else
             else
                 tblrow:node(m_util.html.td.na())
                 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, tblrow, experience, {})
         end
         end
     end
     end
Line 1,711: Line 1,397:
     end
     end
      
      
     return tostring(tbl) .. m_util.misc.add_category(cats)  
     return tostring(tbl) .. m_util.misc.add_category(cats)
end
end
local function _quality(tpl_args)
    --[[
    Displays a table comparing the stats of superior gem quality with
    alternative quality types.
    --]]
    -- Query skill data
    local skill_data = h.query_skill(tpl_args)
    -- Query progression data
    local fields = {}
    for _, fmap in pairs(tables.skill_quality.fields) do
        fields[#fields+1] = fmap.field
    end
    local query = {
        where = string.format(
            '_pageID="%s"',
            skill_data._pageID
        ),
        groupBy = string.format(
            '_pageID, %s',
            tables.skill_quality.fields.set_id.field
        ),
        orderBy = string.format(
            '%s ASC',
            tables.skill_quality.fields.set_id.field
        ),
    }
    skill_data.quality = m_cargo.query({tables.skill_quality.table}, fields, query)
    if #skill_data.quality == 0 then
        error(i18n.errors.quality.missing_quality_data)
    end
    -- Build table
    local tbl = mw.html.create('table')
    tbl
        :addClass('wikitable skill-quality-table')
        :tag('tr')
            :tag('th')
                :wikitext(i18n.quality.type)
                :done()
            :tag('th')
                :wikitext(i18n.quality.stats)
                :done()
            :tag('th')
                :wikitext(i18n.quality.weight)
                :done()
    for k, row in ipairs(skill_data.quality) do
        tbl
            :tag('tr')
                :tag('td')
                    :wikitext(m_game.constants.item.gem_quality_types[k].long_upper)
                    :done()
                :tag('td')
                    :addClass('tc -mod')
                    :wikitext(skill_data.quality[k].stat_text)
                    :done()
                :tag('td')
                    :wikitext(skill_data.quality[k].weight)
                    :done()
    end
   
    return tostring(tbl)
end
-- ----------------------------------------------------------------------------
-- Exported 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_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}
function p.process_skill_data(tpl_args)
    _process_skill_data(tpl_args)
end
p._skill = p.process_skill_data
--
-- Template:Skill
--
p.skill = m_util.misc.invoker_factory(_skill, {
    wrappers = cfg.wrappers.skill,
})
--
-- Template:Skill progression
--
p.progression = m_util.misc.invoker_factory(_progression, {
    wrappers = cfg.wrappers.progression,
})
--
-- Template:Skill quality
--
p.quality = m_util.misc.invoker_factory(_quality, {
    wrappers = cfg.wrappers.quality,
})


return p
return p

Latest revision as of 16:44, 16 December 2023

Module documentation[view] [edit] [history] [purge]


Lua logo

This module depends on the following other modules:

Overview

Module for handling skills with semantic media wiki support

Skill templates

Module:Item2

All templates defined in Module:Skill:

Module:Skill link

All templates defined in Module:Skill link:

ru:Модуль:Skill

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

require('Module:No globals')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')

-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Skill')

local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or 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 = use_sandbox and mw.loadData('Module:Skill/config/sandbox') or 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, 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, 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.expand_costs_data(tpl_args, skill_levels)
    --[[
    Expand costs data so that each cost type has its own column with amounts
    Assumptions:
      Cost types are always static
      Cost amounts can either be static or leveled, but not both
    --]]
    if skill_levels[0] then
        local cost_types = m_util.cast.table(skill_levels[0].cost_types)
        if #cost_types > 0 then
            for _, level_data in pairs(skill_levels) do
                if type(level_data) == 'table' and level_data.cost_amounts then
                    local cost_amounts = m_util.cast.table(level_data.cost_amounts, {callback = m_util.cast.number})
                    for i=1, #cost_types do
                        local type = cost_types[i]
                        local amount = cost_amounts[i]
                        if amount then
                            level_data['cost_' .. type] = amount
                        end
                    end
                end
            end
        end
    end
end

function h.stats(tpl_args, 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, 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(properties)
            end
        end
    end
end

function h.int_value_or_na(tpl_args, 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) or '%s', value)
            end
        end
        tblrow
            :tag('td')
                :wikitext(value)
                :done()
    end
end

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

h.display = {}
h.display.factory = {}
function h.display.factory.value(args)
    return function (tpl_args)
        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)
        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]
            if value.min == nil or type(value.min) == 'table' and #value.min == 0 then
                value.min = tpl_args.skill_levels[1][args.key]
            end
            value.max = tpl_args.skill_levels[0][args.key]
            if value.max == nil or type(value.max) == 'table' and #value.max == 0 then
                value.max = tpl_args.skill_levels[tpl_args.max_level][args.key]
            end
            if type(value.min) == 'table' and type(value.max) == 'table' and args.key_index then
                value.min = value.min[args.key_index]
                value.max = value.max[args.key_index]
            end
        end
        if value.min == nil or value.max == nil then
            -- property not set for this skill
            return nil
        end
        local map = args.map or tables.progression
        local options = {
            fmt=args.fmt or map.fields[args.key] and map.fields[args.key].fmt,
            color=false,
        }
        if type(value.min) == 'table' and type(value.max) == 'table' then
            local formatted_values = {}
            for i=1, #value.min do
                formatted_values[i] = m_util.html.format_value(tpl_args, {min = value.min[i], max = value.max[i]}, options)
            end
            return formatted_values
        end
        return m_util.html.format_value(tpl_args, value, options)
    end
end

function h.display.factory.radius(args)
    return function (tpl_args)
        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

function h.query_skill(tpl_args)
    local fields = {
        'skill._pageID=_pageID',
    }
    local query = {
        groupBy = 'skill._pageID',
    }
    local results = {}
    local search_param
    if tpl_args.skill_id then -- Query by skill id
        query.where = string.format('skill_id="%s"', tpl_args.skill_id)
        search_param = 'skill_id'
    else -- Query by page name
        local page = tpl_args.page or mw.title.getCurrentTitle().prefixedText
        query.where = string.format('_pageName="%s"', page)
        search_param = 'page'
    end
    results = m_cargo.query({tables.static.table}, fields, query)
    if #results == 0 then
        -- No results found
        error(string.format(i18n.errors.validate_skill.no_results_found, search_param, tpl_args[search_param]))
    elseif #results > 1 then
        -- More than one result found
        error(string.format(i18n.errors.validate_skill.many_results_found, search_param, tpl_args[search_param]))
    end
    return results[1]
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, value)
                if value then
                    value = string.format(i18n.files.skill_icon, value)
                elseif tpl_args.active_skill_name then
                    value = string.format(i18n.files.skill_icon, tpl_args.active_skill_name)
                else
                    value = nil
                end
                return value
            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, 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, 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,
        },
        -- 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, value)
                if tpl_args.skill_screenshot_file then
                    tpl_args._flags.has_deprecated_skill_parameters = true
                    value = string.format('File:%s', tpl_args.skill_screenshot_file)
                elseif value then
                    value = string.format(i18n.files.skill_screenshot, value)
                elseif tpl_args.active_skill_name then
                    -- When this parameter is set manually, we assume/expect it to exist, but otherwise it probably doesn't and we don't need dead links in that case
                    value = string.format(i18n.files.skill_screenshot, tpl_args.active_skill_name)
                    local page = mw.title.new(value)
                    if page == nil or not page.exists then
                        value = nil
                    end
                else
                    value = nil
                end
                return value
            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,
        },
    },
}

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),
        },
        cost_multiplier = {
            name = i18n.parameters.skill.cost_multiplier,
            field = 'cost_multiplier',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        attack_time = {
            name = i18n.parameters.skill.attack_time,
            field = 'attack_time',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%.2f ' .. m_game.units.seconds.short_lower,
        },
        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,
        },
        cost_types = {
            name = i18n.parameters.skill.cost_types,
            field = 'cost_types',
            type = 'List (,) of String',
            func = function(tpl_args, value)
                if value == nil then 
                    return nil
                end
                value = m_util.cast.table(value)
                for _, v in ipairs(value) do
                    if m_game.constants.skill.cost_types[v] == nil then
                        error(string.format(i18n.errors.skill.invalid_cost_type, v))
                    end
                end
                return value
            end,
        },
        cost_amounts = {
            name = i18n.parameters.skill.cost_amounts,
            field = 'cost_amounts',
            type = 'List (,) of Integer',
            func = function(tpl_args, value)
                if value == nil then 
                    return nil
                end
                value = m_util.cast.table(value, {callback = m_util.cast.number})
                return value
            end,
        },
        mana_reservation_flat = {
            name = i18n.parameters.skill.mana_reservation_flat,
            field = 'mana_reservation_flat',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        mana_reservation_percent = {
            name = i18n.parameters.skill.mana_reservation_percent,
            field = 'mana_reservation_percent',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        life_reservation_flat = {
            name = i18n.parameters.skill.life_reservation_flat,
            field = 'life_reservation_flat',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        life_reservation_percent = {
            name = i18n.parameters.skill.life_reservation_percent,
            field = 'life_reservation_percent',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        -- 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),
        },
    }
}

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 = 'cost_multiplier',
        header = i18n.progression.cost_multiplier,
        fmt = '%s%%',
    },
    {
        field = 'attack_time',
        header = i18n.progression.attack_time,
        fmt = '%.2f ' .. m_game.units.seconds.short_lower,
    },
    {
        field = 'critical_strike_chance',
        header = i18n.progression.critical_strike_chance,
        fmt = '%s%%',
    },
    {
        field = 'cost_Mana',
        header = i18n.progression.mana_cost,
    },
    {
        field = 'cost_Life',
        header = i18n.progression.life_cost,
    },
    {
        field = 'cost_ES',
        header = i18n.progression.energy_shield_cost,
    },
    {
        field = 'cost_Rage',
        header = i18n.progression.rage_cost,
    },
    {
        field = 'cost_ManaPercent',
        header = i18n.progression.mana_cost,
        fmt = '%s%%',
    },
    {
        field = 'cost_LifePercent',
        header = i18n.progression.life_cost,
        fmt = '%s%%',
    },
    {
        field = 'mana_reservation_flat',
        header = i18n.progression.mana_reserved,
    },
    {
        field = 'mana_reservation_percent',
        header = i18n.progression.mana_reserved,
        fmt = '%s%%',
    },
    {
        field = 'life_reservation_flat',
        header = i18n.progression.life_reserved,
    },
    {
        field = 'life_reservation_percent',
        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)
            return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.skill_id)
        end 
    },
    {
        header = i18n.infobox.skill_icon,
        func = function (tpl_args)
            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)
            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)
            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'},
    },
    -- ignore attrbiutes?
    {
        header = i18n.infobox.cost_multiplier,
        func = h.display.factory.range_value{key='cost_multiplier'},
    },
    {
        header = i18n.infobox.attack_time,
        func = h.display.factory.range_value{key='attack_time'},
    },
    {
        header = i18n.infobox.critical_strike_chance,
        func = h.display.factory.range_value{key='critical_strike_chance'},
    },
    {
        header = i18n.infobox.cost,
        func = function (tpl_args)
            local parts = {}
            for k, v in pairs(m_game.constants.skill.cost_types) do
                local key = 'cost_' .. k
                local fmt
                if string.find(k, 'Percent', 1, true) then
                    fmt = '%s%% %s'
                else
                    fmt = '%s %s'
                end
                local range = h.display.factory.range_value{key=key}(tpl_args)
                if range then
                    parts[#parts+1] = string.format(fmt, range, v.long_lower)
                end
            end
            return table.concat(parts, ', ')
        end,
    },
    {
        header = i18n.infobox.reservation,
        func = function (tpl_args)
            local parts = {}
            local keys = {
                {
                    key = 'mana_reservation_flat',
                    fmt = '%s ' .. m_game.constants.skill.cost_types['Mana'].long_lower,
                },
                {
                    key = 'mana_reservation_percent',
                    fmt = '%s%% ' .. m_game.constants.skill.cost_types['Mana'].long_lower,
                },
                {
                    key = 'life_reservation_flat',
                    fmt = '%s ' .. m_game.constants.skill.cost_types['Life'].long_lower,
                },
                {
                    key = 'life_reservation_percent',
                    fmt = '%s%% ' .. m_game.constants.skill.cost_types['Life'].long_lower,
                },
            }
            for _, v in ipairs(keys) do
                local range = h.display.factory.range_value{key=v.key}(tpl_args)
                if range then
                    parts[#parts+1] = string.format(v.fmt, range)
                end
            end
            return table.concat(parts, ', ')
        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',
    },
}

-- ----------------------------------------------------------------------------
-- Main functions
-- ----------------------------------------------------------------------------

local function _process_skill_data(tpl_args)
    --[[
    Processes skill data from tpl_args.
    Stores skill data in cargo tables.
    Attaches page to cargo tables.
    --]]
    
    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(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(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
    
    -- 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, properties, prefix, tables.progression, i)
        if not tpl_args.test then
            m_cargo.store(properties)
        end
        h.stats(tpl_args, 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, properties, prefix, tables.progression, 0)
        if not tpl_args.test then
            m_cargo.store(properties)
        end
    end

    -- Expand costs data
    h.expand_costs_data(tpl_args, tpl_args.skill_levels)
    
    -- 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, properties, '', tables.static)
    h.stats(tpl_args, prefix, 0)

    -- Build infobox
    local infobox = mw.html.create('span')
    infobox:addClass('skill-box')
    local tbl = infobox:tag('table')
    tbl:addClass('wikitable skill-box-table')
    for _, infobox_data in ipairs(data.infobox_table) do
        local display = infobox_data.func(tpl_args)
        if type(display) == 'string' and string.len(display) > 0 then
            if 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) or '%s', display)
                end
            end
            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)
                else
                    header_text = infobox_data.header
                end
                tr
                    :tag('th')
                        :wikitext(header_text)
                        :done()
            end
            local td = tr:tag('td')
            td:wikitext(display)
            td:addClass(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(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_levels.has_stats then
            attach_tables[#attach_tables+1] = tables.skill_stats_per_level.table
        end
        for _, table_name in ipairs(attach_tables) do
            mw.getCurrentFrame():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

local function _skill(tpl_args)
    --[[
    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}

    ]]

    -- Handle skill data and get infobox
    local infobox = _process_skill_data(tpl_args)

    -- Container
    local container = mw.html.create('span')
    container
        :addClass('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:
    local out = {}
    if mw.ustring.find(tpl_args.skill_id, '_') then
        out[#out+1] = mw.getCurrentFrame():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

local function _progression(tpl_args)
    --[[
        Displays the level progression for the skill gem. 
        
        Examples
        --------
        = p.progression{page='Reave'}
    ]]
    
    -- 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 skill_data = h.query_skill(tpl_args)

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

    -- Expand costs data
    h.expand_costs_data(tpl_args, skill_data.levels)
    
    -- Set up html table headers
    local 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
        :addClass('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) 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, 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
        
        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, tblrow, experience - lastexp, {})
                lastexp = experience
            else
                tblrow:node(m_util.html.td.na())
            end
            h.int_value_or_na(tpl_args, 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

local function _quality(tpl_args)
    --[[
    Displays a table comparing the stats of superior gem quality with 
    alternative quality types.
    --]]

    -- Query skill data
    local skill_data = h.query_skill(tpl_args)

    -- Query progression data
    local fields = {}
    for _, fmap in pairs(tables.skill_quality.fields) do
        fields[#fields+1] = fmap.field
    end
    local query = {
        where = string.format(
            '_pageID="%s"',
            skill_data._pageID
        ),
        groupBy = string.format(
            '_pageID, %s',
            tables.skill_quality.fields.set_id.field
        ),
        orderBy = string.format(
            '%s ASC',
            tables.skill_quality.fields.set_id.field
        ),
    }
    skill_data.quality = m_cargo.query({tables.skill_quality.table}, fields, query)
    if #skill_data.quality == 0 then
        error(i18n.errors.quality.missing_quality_data)
    end

    -- Build table
    local tbl = mw.html.create('table')
    tbl
        :addClass('wikitable skill-quality-table')
        :tag('tr')
            :tag('th')
                :wikitext(i18n.quality.type)
                :done()
            :tag('th')
                :wikitext(i18n.quality.stats)
                :done()
            :tag('th')
                :wikitext(i18n.quality.weight)
                :done()
    for k, row in ipairs(skill_data.quality) do
        tbl
            :tag('tr')
                :tag('td')
                    :wikitext(m_game.constants.item.gem_quality_types[k].long_upper)
                    :done()
                :tag('td')
                    :addClass('tc -mod')
                    :wikitext(skill_data.quality[k].stat_text)
                    :done()
                :tag('td')
                    :wikitext(skill_data.quality[k].weight)
                    :done()
    end
    
    return tostring(tbl)
end

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

function p.process_skill_data(tpl_args)
    _process_skill_data(tpl_args)
end

p._skill = p.process_skill_data

-- 
-- Template:Skill
-- 
p.skill = m_util.misc.invoker_factory(_skill, {
    wrappers = cfg.wrappers.skill,
})

-- 
-- Template:Skill progression
-- 
p.progression = m_util.misc.invoker_factory(_progression, {
    wrappers = cfg.wrappers.progression,
})

-- 
-- Template:Skill quality
-- 
p.quality = m_util.misc.invoker_factory(_quality, {
    wrappers = cfg.wrappers.quality,
})

return p