Module:Skill: Difference between revisions
		
		
		
		Jump to navigation
		Jump to search
		
 (Precision for values in seconds; Display "Instant" for zero second cast time.)  | 
				Mefisto1029 (talk | contribs)   (Skill progression cost)  | 
				||
| (35 intermediate revisions by 5 users not shown) | |||
| Line 6: | Line 6: | ||
-------------------------------------------------------------------------------  | -------------------------------------------------------------------------------  | ||
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   | 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  | ||
-- easier to port this module to another wiki.  | -- easier to port this module to another wiki.  | ||
local cfg = mw.loadData('Module:Skill/config')  | local cfg = use_sandbox and mw.loadData('Module:Skill/config/sandbox') or mw.loadData('Module:Skill/config')  | ||
local mwlanguage = mw.language.getContentLanguage()  | local mwlanguage = mw.language.getContentLanguage()  | ||
| Line 30: | Line 30: | ||
local h = {}  | local h = {}  | ||
function h.map_to_arg(tpl_args  | 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  |                      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 69: | Line 69: | ||
                     -- Deprecated parameters  |                      -- Deprecated parameters  | ||
                     if val and row.deprecated then  |                      if val and row.deprecated then  | ||
                         tpl_args.  |                          tpl_args._flags.has_deprecated_skill_parameters = true  | ||
                         if tpl_args.test then -- Log when testing  |                          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 or {}  | ||
| Line 81: | Line 81: | ||
end  | end  | ||
function h.  | function h.expand_costs_data(tpl_args, skill_levels)  | ||
     --[[  | |||
    Expand costs data so that each cost type has its own column with amounts  | |||
         local   |     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  | ||
| Line 102: | Line 107: | ||
end  | end  | ||
function h.stats(tpl_args  | function h.stats(tpl_args, prefix_in, level)  | ||
     for i=1,   |      for i=1, math.huge do -- repeat until no more stats are found  | ||
         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>_  | ||
         local stat = {  |          local stat = {  | ||
| Line 109: | Line 114: | ||
             value = tpl_args[stat_prefix .. tables.skill_stats_per_level.fields.value.name], --level<level>_stat<i>_value  |              value = tpl_args[stat_prefix .. tables.skill_stats_per_level.fields.value.name], --level<level>_stat<i>_value  | ||
         }  |          }  | ||
         if stat.id   |          if stat.id == nil or stat.value == nil then  | ||
             local properties = {  |              break  | ||
        end  | |||
        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  | ||
end  | end  | ||
function h.int_value_or_na(tpl_args  | 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  | ||
         tblrow:node(m_util.html.  |          tblrow:node(m_util.html.table_cell('na'))  | ||
     else  |      else  | ||
         value = mwlanguage:formatNum(value)  |          -- value = mwlanguage:formatNum(value) -- Removed for now. lang:formatNum() returns a string, which causes issues for formatting  | ||
         if tmap.fmt ~= nil then  |          if tmap.fmt ~= nil then  | ||
             if type(tmap.fmt) == 'string' then  |              if type(tmap.fmt) == 'string' then  | ||
                 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  |                  value = string.format(tmap.fmt(tpl_args) or '%s', value)  | ||
             end  |              end  | ||
         end  |          end  | ||
| Line 144: | Line 150: | ||
h.cast = {}  | h.cast = {}  | ||
function h.cast.wrap (  | function h.cast.wrap(func)  | ||
     return function(tpl_args  |      return function(tpl_args, value)  | ||
         if value == nil then  |          if value == nil then  | ||
             return nil  |              return nil  | ||
         end  |          end  | ||
        return func(value)  | |||
     end  |      end  | ||
end  | end  | ||
| Line 157: | Line 162: | ||
h.display.factory = {}  | h.display.factory = {}  | ||
function h.display.factory.value(args)  | function h.display.factory.value(args)  | ||
     return function (tpl_args  |      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 169: | Line 174: | ||
function h.display.factory.range_value(args)  | function h.display.factory.range_value(args)  | ||
     return function (tpl_args  |      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 183: | Line 188: | ||
             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]  |              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  |          else  | ||
             value.min = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[1][args.key]  |              value.min = tpl_args.skill_levels[0][args.key]  | ||
             value.max = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[tpl_args.max_level][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  |          end  | ||
         if value.min == nil or value.max == nil then  |          if value.min == nil or value.max == nil then  | ||
             return  |             -- property not set for this skill  | ||
             return nil  | |||
         end  |          end  | ||
         local map = args.map or tables.progression  |          local map = args.map or tables.progression  | ||
         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,  | ||
         })  |          }  | ||
        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  | ||
end  | end  | ||
function h.display.factory.radius(args)  | function h.display.factory.radius(args)  | ||
     return function (tpl_args  |      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 213: | Line 234: | ||
         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 253: | Line 302: | ||
             field = 'skill_icon',  |              field = 'skill_icon',  | ||
             type = 'Page',  |              type = 'Page',  | ||
             func = function(tpl_args,   |              func = function(tpl_args, value)  | ||
                 if tpl_args.active_skill_name then  |                  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  |                  end  | ||
                return value  | |||
             end,  |              end,  | ||
         },  |          },  | ||
| Line 263: | Line 317: | ||
             field = 'item_class_id_restriction',  |              field = 'item_class_id_restriction',  | ||
             type = 'List (,) of String',  |              type = 'List (,) of String',  | ||
             func = function(tpl_args  |              func = function(tpl_args, value)  | ||
                 if value == nil then    |                  if value == nil then    | ||
                     return nil  |                      return nil  | ||
| Line 280: | Line 334: | ||
             field = 'item_class_restriction',  |              field = 'item_class_restriction',  | ||
             type = 'List (,) of String',  |              type = 'List (,) of String',  | ||
             func = function(tpl_args  |              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 304: | Line 358: | ||
             name = i18n.parameters.skill.stat_text,  |              name = i18n.parameters.skill.stat_text,  | ||
             field = 'stat_text',  |              field = 'stat_text',  | ||
             type = 'Text',  |              type = 'Text',  | ||
             func = nil,  |              func = nil,  | ||
| Line 354: | Line 402: | ||
             field = 'skill_screenshot',  |              field = 'skill_screenshot',  | ||
             type = 'Page',  |              type = 'Page',  | ||
             func = function(tpl_args,   |              func = function(tpl_args, value)  | ||
                 if tpl_args.skill_screenshot_file then  | |||
                 if tpl_args.skill_screenshot_file   |                     tpl_args._flags.has_deprecated_skill_parameters = true  | ||
                     value = string.format('File:%s', tpl_args.skill_screenshot_file)  | |||
                 elseif   |                  elseif value then  | ||
                     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   |                      -- 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)  | |||
                     page = mw.title.new(  |                      local page = mw.title.new(value)  | ||
                     if page == nil or not page.exists then  |                      if page == nil or not page.exists then  | ||
                         value = nil  | |||
                     end  |                      end  | ||
                else  | |||
                    value = nil  | |||
                 end  |                  end  | ||
                 return   |                  return value  | ||
             end,  |              end,  | ||
         },  |          },  | ||
| Line 383: | Line 433: | ||
             type = 'Text',  |              type = 'Text',  | ||
             func = nil,  |              func = nil,  | ||
         },  |          },  | ||
     },  |      },  | ||
| Line 437: | Line 470: | ||
             func = h.cast.wrap(m_util.cast.number),  |              func = h.cast.wrap(m_util.cast.number),  | ||
         },  |          },  | ||
         cost_multiplier = {  | |||
             name = i18n.parameters.skill.  |              name = i18n.parameters.skill.cost_multiplier,  | ||
             field = '  |              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 512: | Line 552: | ||
             fmt = '%.2f ' .. m_game.units.seconds.short_lower,  |              fmt = '%.2f ' .. m_game.units.seconds.short_lower,  | ||
         },  |          },  | ||
         cost_types = {  | |||
            name = i18n.parameters.skill.cost_types,  | |||
             name = i18n.parameters.skill.  |             field = 'cost_types',  | ||
             field = '  |             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',  |              type = 'Integer',  | ||
             func = h.cast.wrap(m_util.cast.number),  |              func = h.cast.wrap(m_util.cast.number),  | ||
         },  |          },  | ||
         mana_reservation_percent = {  | |||
             name = i18n.parameters.skill.  |              name = i18n.parameters.skill.mana_reservation_percent,  | ||
             field = '  |              field = 'mana_reservation_percent',  | ||
             type = 'Integer',  |              type = 'Integer',  | ||
             func = h.cast.wrap(m_util.cast.number),  |              func = h.cast.wrap(m_util.cast.number),  | ||
         },  |          },  | ||
         life_reservation_flat = {  | |||
             name = i18n.parameters.skill.life_reservation_flat,  | |||
             field = 'life_reservation_flat',  | |||
             name =   | |||
             field = '  | |||
             type = 'Integer',  |              type = 'Integer',  | ||
             func =   |              func = h.cast.wrap(m_util.cast.number),  | ||
         },  |          },  | ||
         life_reservation_percent = {  | |||
             name = i18n.parameters.skill.  |              name = i18n.parameters.skill.life_reservation_percent,  | ||
             field = '  |              field = 'life_reservation_percent',  | ||
             type = 'Integer',  |              type = 'Integer',  | ||
             func =   |              func = h.cast.wrap(m_util.cast.number),  | ||
         },  |          },  | ||
         spirit_reservation_flat = {  | |||
             name =   |              name = i18n.parameters.skill.spirit_reservation_flat,  | ||
             field = '  |              field = 'spirit_reservation_flat',  | ||
             type = 'Integer',  |              type = 'Integer',  | ||
             func =   |              func = h.cast.wrap(m_util.cast.number),  | ||
         },  |          },  | ||
         -- from gem experience, optional  | |||
             name = i18n.parameters.skill.  |         experience = {  | ||
             field = '  |              name = i18n.parameters.skill.experience,  | ||
             field = 'experience',  | |||
             type = 'Integer',  |              type = 'Integer',  | ||
             func = h.cast.wrap(m_util.cast.number),  |              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),  | |||
        },  | |||
     }  | |||
}  | }  | ||
| Line 679: | Line 713: | ||
     },  |      },  | ||
     {  |      {  | ||
         field = '  |          field = 'cost_multiplier',  | ||
         header = i18n.progression.  |          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,  | |||
     },  |      },  | ||
     {  |      {  | ||
| Line 688: | Line 727: | ||
         fmt = '%s%%',  |          fmt = '%s%%',  | ||
     },  |      },  | ||
     {   |      {  | ||
         field = '  |          field = 'cost_Mana',  | ||
         header =   |          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 = '  |          field = 'cost_ManaPercent',  | ||
         header = i18n.progression.mana_cost,  |          header = i18n.progression.mana_cost,  | ||
         fmt = '%s%%',  |          fmt = '%s%%',  | ||
     },  |      },  | ||
     {  |      {  | ||
         field = 'life_cost',  |          field = 'cost_LifePercent',  | ||
        header = i18n.progression.life_cost,  | |||
        fmt = '%s%%',  | |||
    },  | |||
    {  | |||
        field = 'cost_UnreservedManaPercent',  | |||
        header = i18n.progression.mana_cost,  | |||
    },  | |||
    {  | |||
        field = 'cost_ManaPerMinute',  | |||
        header = i18n.progression.mana_cost,  | |||
    },  | |||
    {  | |||
        field = 'cost_LifePerMinute',  | |||
         header = i18n.progression.life_cost,  |          header = i18n.progression.life_cost,  | ||
     },  |      },  | ||
     {  |      {  | ||
         field = '  |          field = 'cost_ManaPercentPerMinute',  | ||
        header = i18n.progression.mana_cost,  | |||
        fmt = '%s%%',  | |||
    },  | |||
    {  | |||
        field = 'cost_LifePercentPerMinute',  | |||
         header = i18n.progression.life_cost,  |          header = i18n.progression.life_cost,  | ||
         fmt = '%s%%',  |          fmt = '%s%%',  | ||
     },  |      },  | ||
     {  |      {  | ||
         field = '  |          field = 'cost_ESPerMinute',  | ||
         header = i18n.progression.energy_shield_cost,  |          header = i18n.progression.energy_shield_cost,  | ||
     },  |      },  | ||
     {  |      {  | ||
         field = '  |          field = 'cost_ESPercentPerMinute',  | ||
         header = i18n.progression.  |          header = i18n.progression.energy_shield_cost,  | ||
        fmt = '%s%%',  | |||
    },  | |||
    {  | |||
        field = 'cost_ESPercent',  | |||
        header = i18n.progression.energy_shield_cost,  | |||
        fmt = '%s%%',  | |||
     },  |      },  | ||
     {  |      {  | ||
         field = '  |          field = 'mana_reservation_flat',  | ||
         header = i18n.progression.mana_reserved,  |          header = i18n.progression.mana_reserved,  | ||
     },  |      },  | ||
     {  |      {  | ||
         field = '  |          field = 'mana_reservation_percent',  | ||
         header = i18n.progression.mana_reserved,  |          header = i18n.progression.mana_reserved,  | ||
         fmt = '%s%%',  |          fmt = '%s%%',  | ||
     },  |      },  | ||
     {  |      {  | ||
         field = '  |          field = 'life_reservation_flat',  | ||
         header = i18n.progression.life_reserved,  |          header = i18n.progression.life_reserved,  | ||
     },  |      },  | ||
     {  |      {  | ||
         field = '  |          field = 'life_reservation_percent',  | ||
         header = i18n.progression.life_reserved,  |          header = i18n.progression.life_reserved,  | ||
         fmt = '%s%%',  |          fmt = '%s%%',  | ||
    },  | |||
    {  | |||
        field = 'spirit_reservation_flat',  | |||
        header = i18n.progression.spirit_reserved,  | |||
     },  |      },  | ||
     {  |      {  | ||
| Line 794: | Line 862: | ||
     {  |      {  | ||
         header = i18n.infobox.skill_id,  |          header = i18n.infobox.skill_id,  | ||
         func = function (tpl_args  |          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 800: | Line 868: | ||
     {  |      {  | ||
         header = i18n.infobox.skill_icon,  |          header = i18n.infobox.skill_icon,  | ||
         func = function (tpl_args  |          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 808: | Line 876: | ||
     {  |      {  | ||
         header = i18n.infobox.cast_time,  |          header = i18n.infobox.cast_time,  | ||
         func = function (tpl_args  |          func = function (tpl_args)  | ||
             local value = tpl_args.cast_time  | |||
                 return m_game.  |             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  |              end  | ||
             return   |              return value  | ||
         end,  |          end,  | ||
     },  |      },  | ||
     {  |      {  | ||
         header = i18n.infobox.item_class_restrictions,  |          header = i18n.infobox.item_class_restrictions,  | ||
         func = function (tpl_args  |          func = function (tpl_args)  | ||
             if tpl_args.item_class_restriction == nil then  |              if tpl_args.item_class_restriction == nil then  | ||
                 return  |                  return  | ||
| Line 848: | Line 920: | ||
         func = h.display.factory.range_value{key='level_requirement'},  |          func = h.display.factory.range_value{key='level_requirement'},  | ||
     },  |      },  | ||
     --   |      -- ignore attrbiutes?  | ||
     {  |      {  | ||
         header = i18n.infobox.  |          header = i18n.infobox.cost_multiplier,  | ||
         func = h.display.factory.range_value{key='  |          func = h.display.factory.range_value{key='cost_multiplier'},  | ||
    },  | |||
    {  | |||
        header = i18n.infobox.attack_time,  | |||
        func = h.display.factory.range_value{key='attack_time'},  | |||
     },  |      },  | ||
     {  |      {  | ||
| Line 859: | Line 935: | ||
     {  |      {  | ||
         header = i18n.infobox.cost,  |          header = i18n.infobox.cost,  | ||
         func = function (tpl_args  |          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  |                  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  | ||
             end  |              end  | ||
             return table.concat(  |              return table.concat(parts, ', ')  | ||
         end,  |          end,  | ||
     },  |      },  | ||
     {  |      {  | ||
         header = i18n.infobox.reservation,  |          header = i18n.infobox.reservation,  | ||
         func = function (tpl_args  |          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,  | |||
                },  | |||
                {  | |||
             for   |                     key = 'life_reservation_percent',  | ||
                    fmt = '%s%% ' .. m_game.constants.skill.cost_types['Life'].long_lower,  | |||
                },  | |||
                {  | |||
                     key = 'spirit_reservation_flat',  | |||
                     fmt = '%s ' .. 'spirit',  | |||
                 },  | |||
             }  | |||
             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  | ||
             end  |              end  | ||
             return table.concat(  |              return table.concat(parts, ', ')  | ||
         end,  |          end,  | ||
     },  |      },  | ||
| Line 929: | Line 991: | ||
         header = i18n.infobox.attack_speed_multiplier,  |          header = i18n.infobox.attack_speed_multiplier,  | ||
         func = h.display.factory.range_value{key='attack_speed_multiplier'},  |          func = h.display.factory.range_value{key='attack_speed_multiplier'},  | ||
        fmt = '%s ' .. i18n.infobox.of_base_stat,  | |||
    },  | |||
    {  | |||
        header = i18n.infobox.damage_multiplier,  | |||
        func = h.display.factory.range_value{key='damage_multiplier'},  | |||
        fmt = '%s ' .. i18n.infobox.of_base_stat,  | |||
     },  |      },  | ||
     {  |      {  | ||
| Line 954: | Line 1,022: | ||
         func = h.display.factory.range_value{key='vaal_soul_gain_prevention_time'},  |          func = h.display.factory.range_value{key='vaal_soul_gain_prevention_time'},  | ||
     },    |      },    | ||
     {  |      {  | ||
         header = i18n.infobox.duration,  |          header = i18n.infobox.duration,  | ||
| Line 975: | Line 1,039: | ||
-- ----------------------------------------------------------------------------  | -- ----------------------------------------------------------------------------  | ||
--   | -- 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 = tpl_args or {}  | ||
    tpl_args._flags = tpl_args._flags or {}  | |||
     tpl_args.skill_levels = {  |      tpl_args.skill_levels = {  | ||
         [0] = {},  |          [0] = {},  | ||
| Line 1,013: | Line 1,069: | ||
         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(  |              m_cargo.store(q)  | ||
             q.stats = {}  |              q.stats = {}  | ||
| Line 1,029: | Line 1,085: | ||
                 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(  |                      m_cargo.store(s)  | ||
                 end  |                  end  | ||
                 s._table = nil  |                  s._table = nil  | ||
             until s.id == nil or s.value == nil    |              until s.id == nil or s.value == nil  | ||
         end  |          end  | ||
     until q.stat_text == nil  |      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  |      end  | ||
| Line 1,082: | Line 1,116: | ||
             [tables.progression.fields.level.field] = i  |              [tables.progression.fields.level.field] = i  | ||
         }  |          }  | ||
         h.map_to_arg(tpl_args  |          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(  |              m_cargo.store(properties)  | ||
         end  |          end  | ||
         h.stats(tpl_args, prefix, i)  | |||
         h.stats(tpl_args  | |||
     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,098: | Line 1,131: | ||
             [tables.progression.fields.level.field] = 0  |              [tables.progression.fields.level.field] = 0  | ||
         }  |          }  | ||
         h.map_to_arg(tpl_args  |          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(  |              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,109: | Line 1,145: | ||
         [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  |      h.map_to_arg(tpl_args, properties, '', tables.static)  | ||
     h.stats(tpl_args, prefix, 0)  | |||
     h.stats(tpl_args  | |||
     -- Build infobox  |      -- Build infobox  | ||
     local infobox = mw.html.create('span')  |      local infobox = mw.html.create('span')  | ||
     infobox:  |      infobox:addClass('skill-box')  | ||
     local tbl = infobox:tag('table')  |      local tbl = infobox:tag('table')  | ||
     tbl:  |      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  |          local display = infobox_data.func(tpl_args)  | ||
         if display then  |          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')  |              local tr = tbl:tag('tr')  | ||
             if infobox_data.header then  |              if infobox_data.header then  | ||
                 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  |                      header_text = infobox_data.header(tpl_args)  | ||
                 else  |                  else  | ||
                     header_text = infobox_data.header  |                      header_text = infobox_data.header  | ||
| Line 1,136: | Line 1,178: | ||
             local td = tr:tag('td')  |              local td = tr:tag('td')  | ||
             td:wikitext(display)  |              td:wikitext(display)  | ||
             td:  |              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,147: | Line 1,189: | ||
     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(  |          m_cargo.store(properties)  | ||
     end  |      end  | ||
| Line 1,159: | Line 1,201: | ||
             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  |          end  | ||
         if tpl_args.skill_levels.has_stats then  |          if tpl_args.skill_levels.has_stats then  | ||
| Line 1,168: | Line 1,206: | ||
         end  |          end  | ||
         for _, table_name in ipairs(attach_tables) do  |          for _, table_name in ipairs(attach_tables) do  | ||
             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,183: | Line 1,221: | ||
end  | end  | ||
local function _skill(tpl_args)  | |||
function   | |||
     --[[  |      --[[  | ||
     Display skill infobox  |      Display skill infobox  | ||
| Line 1,195: | Line 1,230: | ||
     ]]  |      ]]  | ||
     -- Handle skill data and get infobox  |      -- Handle skill data and get infobox  | ||
     local infobox =   |      local infobox = _process_skill_data(tpl_args)  | ||
     -- Container  |      -- Container  | ||
     local container = mw.html.create('span')  |      local container = mw.html.create('span')  | ||
     container  |      container  | ||
         :  |          :addClass('skill-box-page-container')  | ||
         :wikitext(infobox)  |          :wikitext(infobox)  | ||
     if tpl_args.skill_screenshot then  |      if tpl_args.skill_screenshot then  | ||
| Line 1,215: | Line 1,245: | ||
     -- 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] =   |          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,237: | Line 1,267: | ||
     -- Categories  |      -- Categories  | ||
     local cats = {i18n.categories.skill_data}  |      local cats = {i18n.categories.skill_data}  | ||
     if tpl_args.  |      if tpl_args._flags.has_deprecated_skill_parameters then  | ||
         cats[#cats+1] = i18n.categories.deprecated_parameters  |          cats[#cats+1] = i18n.categories.deprecated_parameters  | ||
     end  |      end  | ||
| Line 1,244: | Line 1,274: | ||
end  | end  | ||
function   | local function _progression(tpl_args)  | ||
     --[[  |      --[[  | ||
         Displays the level progression for the skill gem.    |          Displays the level progression for the skill gem.    | ||
| Line 1,252: | Line 1,282: | ||
         = p.progression{page='Reave'}  |          = p.progression{page='Reave'}  | ||
     ]]  |      ]]  | ||
     -- Parse column arguments:  |      -- Parse column arguments:  | ||
| Line 1,285: | Line 1,310: | ||
     -- Query skill data  |      -- Query skill data  | ||
     local skill_data = h.query_skill(tpl_args)  | |||
     local 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"',  | ||
             skill_data  |              skill_data._pageID  | ||
         ),  |          ),  | ||
         groupBy = string.format(  |          groupBy = string.format(  | ||
             '_pageID, %s',  |              '_pageID, %s',  | ||
             tables.progression.fields.level.field  |              tables.progression.fields.level.field  | ||
         ),  |          ),  | ||
     }  |      }  | ||
     results = m_cargo.query({tables.progression.table}, fields, query)  |      local results = m_cargo.query({tables.progression.table}, fields, query)  | ||
     if #  | |||
    -- 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)  |          error(i18n.errors.progression.missing_level_data)  | ||
     end  |      end  | ||
    -- Expand costs data  | |||
    h.expand_costs_data(tpl_args, skill_data.levels)  | |||
     -- 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,414: | Line 1,350: | ||
     local tbl = mw.html.create('table')  |      local tbl = mw.html.create('table')  | ||
     tbl  |      tbl  | ||
         :  |          :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  |              local text = type(tmap.header) == 'function' and tmap.header(tpl_args) or tmap.header  | ||
             head  |              head  | ||
                 :tag('th')  |                  :tag('th')  | ||
| Line 1,449: | Line 1,385: | ||
         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  |                  h.int_value_or_na(tpl_args, tblrow, row[tmap.field], tmap)  | ||
             end  |              end  | ||
         end  |          end  | ||
| Line 1,472: | Line 1,408: | ||
             end  |              end  | ||
             if #match == 0 then  |              if #match == 0 then  | ||
                 tblrow:node(m_util.html.  |                  tblrow:node(m_util.html.table_cell('na'))  | ||
             else  |              else  | ||
                 -- used to find broken progression due to game updates  |                  -- used to find broken progression due to game updates  | ||
| Line 1,491: | Line 1,427: | ||
             end  |              end  | ||
         end  |          end  | ||
         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  |                  h.int_value_or_na(tpl_args, tblrow, experience - lastexp, {})  | ||
                 lastexp = experience  |                  lastexp = experience  | ||
             else  |              else  | ||
                 tblrow:node(m_util.html.  |                  tblrow:node(m_util.html.table_cell('na'))  | ||
             end  |              end  | ||
             h.int_value_or_na(tpl_args  |              h.int_value_or_na(tpl_args, tblrow, experience, {})  | ||
         end  |          end  | ||
     end  |      end  | ||
| Line 1,514: | Line 1,448: | ||
     end  |      end  | ||
     return tostring(tbl) .. m_util.misc.add_category(cats)    |      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  | 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 15:38, 20 August 2025
Module for handling skills with Cargo support.
Implemented templates
- {{Skill}}
 - {{Skill progression}}
 
The above documentation is transcluded from Module:Skill/doc. 
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
-------------------------------------------------------------------------------
-- 
--                              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, math.huge do -- repeat until no more stats are found
        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 or stat.value == nil then
            break
        end
        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
function h.int_value_or_na(tpl_args, tblrow, value, tmap)
    value = tonumber(value)
    if value == nil then
        tblrow:node(m_util.html.table_cell('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),
        },
        spirit_reservation_flat = {
            name = i18n.parameters.skill.spirit_reservation_flat,
            field = 'spirit_reservation_flat',
            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 = 'cost_UnreservedManaPercent',
        header = i18n.progression.mana_cost,
    },
    {
        field = 'cost_ManaPerMinute',
        header = i18n.progression.mana_cost,
    },
    {
        field = 'cost_LifePerMinute',
        header = i18n.progression.life_cost,
    },
    {
        field = 'cost_ManaPercentPerMinute',
        header = i18n.progression.mana_cost,
        fmt = '%s%%',
    },
    {
        field = 'cost_LifePercentPerMinute',
        header = i18n.progression.life_cost,
        fmt = '%s%%',
    },
    {
        field = 'cost_ESPerMinute',
        header = i18n.progression.energy_shield_cost,
    },
    {
        field = 'cost_ESPercentPerMinute',
        header = i18n.progression.energy_shield_cost,
        fmt = '%s%%',
    },
    {
        field = 'cost_ESPercent',
        header = i18n.progression.energy_shield_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 = 'spirit_reservation_flat',
        header = i18n.progression.spirit_reserved,
    },
    {
        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,
                },
                {
                    key = 'spirit_reservation_flat',
                    fmt = '%s ' .. 'spirit',
                },
            }
            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.table_cell('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.table_cell('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