Module:Skill: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
(Undo revision 698307 by Vinifera7 (talk) Issues detected on Awakened Minion Damage Support)
Line 13: Line 13:


--- define here to avoid errors
--- define here to avoid errors
local tables = {}
local data = {}
local data = {}
local tables = {}
-- TODO:
-- skill_id field link to data page
-- aspects: % mana cost


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 25: Line 21:


local i18n = {
local i18n = {
     skill_icon = 'File:%s skill icon.png',
     arguments = {
    skill_screenshot = 'File:%s skill screenshot.jpg',
        skill = {
   
            -- static
    -- Intro texts:
            static = 'static',
    intro_named_id = "'''%s''' is the internal id of the [[skill]] '''%s'''.\n",
            skill_id = 'skill_id',
    intro_unnamed_id = "'''%s''' is the internal id of an unnamed [[skill]].\n",
            cast_time = 'cast_time',
      
            gem_description = 'gem_description',
            active_skill_name = 'active_skill_name',
            skill_icon = 'skill_icon',
            item_class_id_restriction = 'item_class_id_restriction',
            item_class_restriction = 'item_class_restriction',
            projectile_speed = 'projectile_speed',
            stat_text = 'stat_text',
            quality_stat_text = 'quality_stat_text',
            radius = 'radius',
            radius_description = 'radius_description',
            radius_secondary = 'radius_secondary',
            radius_secondary_description = 'radius_secondary_description',
            radius_tertiary = 'radius_tertiary',
            radius_tertiary_description = 'radius_tertiary_description',
            skill_screenshot = 'skill_screenshot',
            has_percentage_mana_cost = 'has_percentage_mana_cost',
            has_reservation_mana_cost = 'has_reservation_mana_cost', 
            -- progression
            level = 'level',
            level_requirement = 'level_requirement',
            dexterity_requirement = 'dexterity_requirement',
            strength_requirement = 'strength_requirement',
            intelligence_requirement = 'intelligence_requirement',
            mana_multiplier = 'mana_multiplier',
            critical_strike_chance = 'critical_strike_chance',
            damage_effectiveness = 'damage_effectiveness',
            stored_uses = 'stored_uses',
            cooldown = 'cooldown',
            vaal_souls_requirement = 'vaal_souls_requirement',
            vaal_stored_uses = 'vaal_stored_uses',
            vaal_soul_gain_prevention_time = 'vaal_soul_gain_prevention_time',
            damage_multiplier = 'damage_multiplier',
            attack_speed_multiplier = 'attack_speed_multiplier',
            duration = 'duration',
            experience = 'experience',
            mana_cost = 'mana_cost',
            -- costs
            skill_cost = 'skill_cost',
            cost = 'cost',
            cost_type = 'type',
            cost_is_reservation = 'is_reservation',
            cost_amount = 'amount',
            -- stats
            stat = 'stat',
            stat_id = 'id',
            stat_value = 'value',
        },
        progression = {
            column = 'c',
            header = 'header',  
            abbr = 'abbr',
            pattern_extract = 'pattern_extract',
            pattern_value = 'pattern_value',
        },
     },
 
     errors = {
     errors = {
         all_format_keys_specified = 'All formatting keys must be specified for index "%s"',
         skill = {
         no_results_for_skill_id = "Couldn't find a page for the specified skill id",
            invalid_item_class_id = 'The item class id "%s" is invalid.',
        no_results_for_skill_page = "Couldn't find the queried data on the skill page",
            invalid_cost_type = 'The cost type "%s" is invalid. Acceptable values are "mana", "life", "energy_shield", "rage", "mana_percent" and "life_percent".',
        missing_level_data = 'No gem level progression data found',
         },
        progression = {
            argument_unspecified = 'The argument "%s" is unspecified.',
            no_results_for_skill_id = 'Unable to find skill data for skill id "%s".',
            no_results_for_skill_page = 'Unable to find skill data on page "%s".',
            missing_level_data = 'Unable to find skill level progression data.',
        },
    },
 
    templates = {
        incorrect_title = 'Template:Incorrect title',
        cargo_attach = 'Template:Skill/cargo/attach/%s',
     },
     },
      
      
     categories = {
     categories = {
         broken_progression_table = 'Pages with broken skill progression tables'
        skill_data = 'Skill data',
        deprecated_arguments = 'Pages with deprecated arguments for skill data',
         broken_progression_table = 'Pages with broken skill progression tables',
    },
 
    files = {
        skill_icon = 'File:%s skill icon.png',
        skill_screenshot = 'File:%s skill screenshot.jpg',
    },
   
    messages = {
        intro_named_id = "'''%s''' is the internal id of the [[skill]] '''%s'''.\n",
        intro_unnamed_id = "'''%s''' is the internal id of an unnamed [[skill]].\n",
     },
     },
      
      
Line 56: Line 130:
         mana_multiplier = 'Mana Multiplier',
         mana_multiplier = 'Mana Multiplier',
         critical_strike_chance = 'Critical Strike Chance',
         critical_strike_chance = 'Critical Strike Chance',
         mana_cost = 'Mana Cost',
         cost = 'Cost',
         mana_reserved = 'Mana Reserved',
         reservation = 'Reservation',
         attack_speed_multiplier = 'Attack Speed',
         attack_speed_multiplier = 'Attack Speed',
         damage_effectiveness = 'Effectiveness of Added Damage',
         damage_effectiveness = 'Effectiveness of Added Damage',
Line 70: Line 144:
      
      
     progression = {
     progression = {
        level = 'Level',
         level_requirement = m_util.html.abbr('[[Image:Level_up_icon_small.png|link=|Lvl.]]', 'Required Level', 'nounderline'),
         level_requirement = m_util.html.abbr('[[Image:Level_up_icon_small.png|link=|Lvl.]]', 'Required Level', 'nounderline'),
         dexterity_requirement = m_util.html.abbr('[[Image:DexterityIcon_small.png|link=|dexterity]]', 'Required Dexterity', 'nounderline'),
         dexterity_requirement = m_util.html.abbr('[[Image:DexterityIcon_small.png|link=|dexterity]]', 'Required Dexterity', 'nounderline'),
Line 77: Line 152:
         critical_strike_chance = 'Critical<br>Strike<br>Chance',
         critical_strike_chance = 'Critical<br>Strike<br>Chance',
         mana_cost = 'Mana<br>Cost',
         mana_cost = 'Mana<br>Cost',
        life_cost = 'Life<br>Cost',
        energy_shield_cost = m_util.html.abbr('ES Cost', 'Energy shield cost'),
        rage_cost = 'Rage<br>Cost',
         mana_reserved = 'Mana<br>Reserved',
         mana_reserved = 'Mana<br>Reserved',
        life_reserved = 'Life<br>Reserved',
         attack_speed_multiplier = 'Attack<br>Speed<br>Multiplier',
         attack_speed_multiplier = 'Attack<br>Speed<br>Multiplier',
         damage_effectiveness = 'Damage<br>Effectiveness',
         damage_effectiveness = 'Damage<br>Effectiveness',
Line 85: Line 164:
         vaal_stored_uses = 'Stored<br>Uses',
         vaal_stored_uses = 'Stored<br>Uses',
         vaal_soul_gain_prevention_time = 'Soul<br>Prevention<br>Time',
         vaal_soul_gain_prevention_time = 'Soul<br>Prevention<br>Time',
         damage_multiplier = m_util.html.abbr('Damage<br>Multiplier', 'Deals x% of Base Damage'),
         damage_multiplier = m_util.html.abbr('Damage<br>Multiplier', 'Deals x% of base damage'),
         duration = m_util.html.abbr('Base duration', 'Base duration is x seconds'),
         duration = m_util.html.abbr('Base duration', 'Base duration is x seconds'),
         exp_short = 'Exp.',
         experience = m_util.html.abbr('Exp.', 'Experience needed to level up'),
        exp_long = 'Experience needed to level up',
         total_experience = m_util.html.abbr('Total Exp.', 'Total experience needed'),
         tot_exp_short = 'Total Exp.',
         na = 'N/A',
         tot_exp_long = 'Total experience needed',
     },
     },
}
}
Line 99: Line 177:
local h = {}
local h = {}


function h.map_to_arg(tpl_args, frame, properties, prefix_in, map, level)
function h.map_to_arg(tpl_args, frame, properties, prefix_in, map, level, set_name, set_id)
     if map.order then
     if map.fields then
         for _, key in ipairs(map.order) do
         for key, row in pairs(map.fields) do
            row = map.fields[key]
             if row.name then
             if row.name then
                 local val = tpl_args[prefix_in .. row.name]
                 local val = tpl_args[prefix_in .. row.name]
Line 111: Line 188:
                     val = row.default
                     val = row.default
                 end
                 end
                 if val ~= nil then  
                 if val ~= nil then
                     if level ~= nil then
                     if level ~= nil then
                         tpl_args.skill_levels[level][key] = val
                         if set_name then
                            tpl_args.skill_levels[level][set_name] = tpl_args.skill_levels[level][set_name] or {}
                            tpl_args.skill_levels[level][set_name][set_id] = tpl_args.skill_levels[level][set_name][set_id] or {}
                            tpl_args.skill_levels[level][set_name][set_id][key] = val
                        else
                            tpl_args.skill_levels[level][key] = val
                        end
 
                         -- Nuke variables since they're remapped to skill_levels
                         -- Nuke variables since they're remapped to skill_levels
                         tpl_args[prefix_in .. row.name] = nil
                         tpl_args[prefix_in .. row.name] = nil
                     else
                     else
                         tpl_args[row.name] = val
                         if set_name then
                            tpl_args[set_name] = tpl_args[set_name] or {}
                            tpl_args[set_name][set_id] = tpl_args[set_name][set_id] or {}
                            tpl_args[set_name][set_id][key] = val
 
                            -- Nuke variables since they're remapped to [set_name]
                            tpl_args[prefix_in .. row.name] = nil
                        else
                            tpl_args[key] = val
                        end
                     end
                     end
                     properties[row.field] = val
                     properties[row.field] = val
                 end
                 end
             else
 
                error(string.format('Programming error, missing field %s from order keys', key))
                -- Deprecated arguments
                if row.deprecated then
                    tpl_args.has_deprecated_arguments = true
                end
             end
        end
    end
end
 
function h.costs(tpl_args, frame, prefix_in, level)
    tpl_args.skill_costs = tpl_args.skill_costs or {}
    for i=1, #tpl_args.skill_costs do
        local cost_prefix = string.format('%s%s%d_', prefix_in, i18n.arguments.skill.cost, i) -- level<level>_cost<i>_
        local cost = {
            amount = tpl_args[cost_prefix .. tables.skill_level_costs.fields.amount.name], --level<level>_cost<i>_amount
        }
        if cost.amount ~= nil then
            local properties = {
                _table = tables.skill_level_costs.table,
                [tables.skill_level_costs.fields.set_id.field] = i,
                [tables.skill_level_costs.fields.level.field] = level,
            }
            h.map_to_arg(tpl_args, frame, properties, cost_prefix, tables.skill_level_costs, level, 'costs', i)
            if not tpl_args.test then
                m_cargo.store(frame, properties)
             end
             end
         end
         end
Line 129: Line 246:


function h.stats(tpl_args, frame, prefix_in, level)
function h.stats(tpl_args, frame, prefix_in, level)
    local type_prefix = string.format('%s%s', prefix_in, 'stat')
    tpl_args.skill_levels[level]['stats'] = {}
     for i=1, data.max_stats_per_level do
     for i=1, data.max_stats_per_level do
         local stat_id_key = string.format('%s%s_%s', type_prefix, i, 'id')
         local stat_prefix = string.format('%s%s%d_', prefix_in, i18n.arguments.skill.stat, i) -- level<level>_stat<i>_
        local stat_val_key = string.format('%s%s_%s', type_prefix, i, 'value')
         local stat = {
         local stat = {
             id = tpl_args[stat_id_key],
             id = tpl_args[stat_prefix .. tables.skill_stats_per_level.fields.id.name], --level<level>_stat<i>_id
             value = tonumber(tpl_args[stat_val_key]),
             value = tpl_args[stat_prefix .. tables.skill_stats_per_level.fields.value.name], --level<level>_stat<i>_value
         }
         }
         if stat.id ~= nil and stat.value ~= nil then
         if stat.id ~= nil and stat.value ~= nil then
             tpl_args.skill_levels[level]['stats'][#tpl_args.skill_levels[level]['stats']+1] = stat
             local properties = {
              
                _table = tables.skill_stats_per_level.table,
                [tables.skill_stats_per_level.fields.level.field] = level,
            }
            h.map_to_arg(tpl_args, frame, properties, stat_prefix, tables.skill_stats_per_level, level, 'stats', i)
             tpl_args.skill_levels.has_stats = true
             if not tpl_args.test then
             if not tpl_args.test then
                 m_cargo.store(frame, {
                 m_cargo.store(frame, properties)
                    _table = tables.skill_stats_per_level.table,
                    [tables.skill_stats_per_level.fields.level.field] = level,
                    [tables.skill_stats_per_level.fields.id.field] = stat.id,
                    [tables.skill_stats_per_level.fields.value.field] = stat.value,
                })
             end
             end
           
            -- Nuke variables since they're remapped to skill levels
            tpl_args[stat_id_key] = nil
            tpl_args[stat_val_key] = nil
         end
         end
     end
     end
Line 161: Line 270:
         :tag('td')
         :tag('td')
             :attr('class', 'table-na')
             :attr('class', 'table-na')
             :wikitext('N/A')
             :wikitext(i18n.progression.na)
             :done()
             :done()
end
end
Line 212: Line 321:
function h.display.factory.range_value(args)
function h.display.factory.range_value(args)
     return function (tpl_args, frame)
     return function (tpl_args, frame)
         local value = {
         local value = {}
             min = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[1][args.key],
        if args.set_name and args.set_id then
             max = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.key],
            -- Guard against index errors
         }
            tpl_args.skill_levels[0][args.set_name] = tpl_args.skill_levels[0][args.set_name] or {}
            tpl_args.skill_levels[0][args.set_name][args.set_id] = tpl_args.skill_levels[0][args.set_name][args.set_id] or {}
            tpl_args.skill_levels[1][args.set_name] = tpl_args.skill_levels[1][args.set_name] or {}
            tpl_args.skill_levels[1][args.set_name][args.set_id] = tpl_args.skill_levels[1][args.set_name][args.set_id] or {}
            tpl_args.skill_levels[tpl_args.max_level][args.set_name] = tpl_args.skill_levels[tpl_args.max_level][args.set_name] or {}
            tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id] = tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id] or {}
 
            value.min = tpl_args.skill_levels[0][args.set_name][args.set_id][args.key] or tpl_args.skill_levels[1][args.set_name][args.set_id][args.key]
            value.max = tpl_args.skill_levels[0][args.set_name][args.set_id][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.set_name][args.set_id][args.key]
        else
             value.min = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[1][args.key]
             value.max = tpl_args.skill_levels[0][args.key] or tpl_args.skill_levels[tpl_args.max_level][args.key]
         end
 
         -- property not set for this skill
         -- property not set for this skill
         if value.min == nil or value.max == nil then
         if value.min == nil or value.max == nil then
             return
             return
         end
         end
          
 
         local map = args.map or tables.progression
         return m_util.html.format_value(tpl_args, frame, value, {
         return m_util.html.format_value(tpl_args, frame, value, {
             fmt=args.fmt or tables.progression.fields[args.key].fmt,
             fmt=args.fmt or map.fields[args.key].fmt,
             no_color=true,
             no_color=true,
         })
         })
Line 244: Line 367:


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Data
-- Cargo tables
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


tables.static = {
tables.static = {
     table = 'skill',
     table = 'skill',
    order = {'skill_id', 'cast_time', 'gem_description', 'active_skill_name', 'skill_icon', 'item_class_id_restriction', 'item_class_restriction', 'projectile_speed', 'stat_text', 'quality_stat_text', 'has_percentage_mana_cost', 'has_reservation_mana_cost', 'radius', 'radius_secondary', 'radius_tertiary', 'radius_description', 'radius_secondary_description', 'radius_tertiary_description', 'skill_screenshot'},
     fields = {
     fields = {
         -- GrantedEffects.dat
         -- GrantedEffects.dat
         skill_id = {
         skill_id = {
             name = 'skill_id',
             name = i18n.arguments.skill.skill_id,
             field = 'skill_id',
             field = 'skill_id',
             type = 'String',
             type = 'String',
Line 260: Line 382:
         -- Active Skills.dat
         -- Active Skills.dat
         cast_time = {
         cast_time = {
             name = 'cast_time',
             name = i18n.arguments.skill.cast_time,
             field = 'cast_time',
             field = 'cast_time',
             type = 'Float',
             type = 'Float',
Line 267: Line 389:
         },
         },
         gem_description = {
         gem_description = {
             name = 'gem_description',
             name = i18n.arguments.skill.gem_description,
             field = 'description',
             field = 'description',
             type = 'Text',
             type = 'Text',
Line 273: Line 395:
         },
         },
         active_skill_name = {
         active_skill_name = {
             name = 'active_skill_name',
             name = i18n.arguments.skill.active_skill_name,
             field = 'active_skill_name',
             field = 'active_skill_name',
             type = 'String',
             type = 'String',
Line 279: Line 401:
         },
         },
         skill_icon = {
         skill_icon = {
             name = 'skill_icon',
             name = i18n.arguments.skill.skill_icon,
             field = 'skill_icon',
             field = 'skill_icon',
             type = 'Page',
             type = 'Page',
             func = function(tpl_args, frame)
             func = function(tpl_args, frame)
                 if tpl_args.active_skill_name then
                 if tpl_args.active_skill_name then
                     return string.format(i18n.skill_icon, tpl_args.active_skill_name)
                     return string.format(i18n.files.skill_icon, tpl_args.active_skill_name)
                 end
                 end
             end,
             end,
         },
         },
         item_class_id_restriction = {
         item_class_id_restriction = {
             name = 'item_class_id_restriction',
             name = i18n.arguments.skill.item_class_id_restriction,
             field = 'item_class_id_restriction',
             field = 'item_class_id_restriction',
             type = 'List (,) of String',
             type = 'List (,) of String',
Line 299: Line 421:
                 for _, v in ipairs(value) do
                 for _, v in ipairs(value) do
                     if m_game.constants.item.classes[v] == nil then
                     if m_game.constants.item.classes[v] == nil then
                         error(string.format('Invalid item class id: %s', v))
                         error(string.format(i18n.errors.skill.invalid_item_class_id, v))
                     end
                     end
                 end
                 end
Line 306: Line 428:
         },
         },
         item_class_restriction = {
         item_class_restriction = {
             name = 'item_class_restriction',
             name = i18n.arguments.skill.item_class_restriction,
             field = 'item_class_restriction',
             field = 'item_class_restriction',
             type = 'List (,) of String',
             type = 'List (,) of String',
Line 324: Line 446:
         -- Projectiles.dat - manually mapped to the skills
         -- Projectiles.dat - manually mapped to the skills
         projectile_speed = {
         projectile_speed = {
             name = 'projectile_speed',
             name = i18n.arguments.skill.projectile_speed,
             field = 'projectile_speed',
             field = 'projectile_speed',
             type = 'Integer',
             type = 'Integer',
Line 331: Line 453:
         -- Misc data derieved from stats
         -- Misc data derieved from stats
         stat_text = {
         stat_text = {
             name = 'stat_text',
             name = i18n.arguments.skill.stat_text,
             field = 'stat_text',
             field = 'stat_text',
             type = 'Text',
             type = 'Text',
Line 337: Line 459:
         },
         },
         quality_stat_text = {
         quality_stat_text = {
             name = 'quality_stat_text',
             name = i18n.arguments.skill.quality_stat_text,
             field = 'quality_stat_text',
             field = 'quality_stat_text',
             type = 'Text',
             type = 'Text',
Line 343: Line 465:
         },
         },
         -- Misc data currently not from game data
         -- Misc data currently not from game data
        has_percentage_mana_cost = {
            name = 'has_percentage_mana_cost',
            field = 'has_percentage_mana_cost',
            type = 'Boolean',
            func = h.cast.wrap(m_util.cast.boolean),
            default = false,
        },
        has_reservation_mana_cost = {
            name = 'has_reservation_mana_cost',
            field = 'has_reservation_mana_cost',
            type = 'Boolean',
            func = h.cast.wrap(m_util.cast.boolean),
            default = false,
        },
         radius = {
         radius = {
             name = 'radius',
             name = i18n.arguments.skill.radius,
             field = 'radius',
             field = 'radius',
             type = 'Integer',
             type = 'Integer',
Line 364: Line 472:
         },
         },
         radius_description = {
         radius_description = {
             name = 'radius_description',
             name = i18n.arguments.skill.radius_description,
             field = 'radius_description',
             field = 'radius_description',
             type = 'Text',
             type = 'Text',
             func = nil,
             func = h.cast.wrap(m_util.cast.text),
         },
         },
         radius_secondary = {
         radius_secondary = {
             name = 'radius_secondary',
             name = i18n.arguments.skill.radius_secondary,
             field = 'radius_secondary',
             field = 'radius_secondary',
             type = 'Integer',
             type = 'Integer',
Line 376: Line 484:
         },
         },
         radius_secondary_description = {
         radius_secondary_description = {
             name = 'radius_secondary_description',
             name = i18n.arguments.skill.radius_secondary_description,
             field = 'radius_secondary_description',
             field = 'radius_secondary_description',
             type = 'Text',
             type = 'Text',
             func = nil,
             func = h.cast.wrap(m_util.cast.text),
         },
         },
         -- not sure if any skill actually has 3 radius componets
         radius_tertiary = { -- not sure if any skill actually has 3 radius componets
        radius_tertiary = {
             name = i18n.arguments.skill.radius_tertiary,
             name = 'radius_tertiary',
             field = 'radius_tertiary',
             field = 'radius_tertiary',
             type = 'Integer',
             type = 'Integer',
Line 389: Line 496:
         },
         },
         radius_tertiary_description = {
         radius_tertiary_description = {
             name = 'radius_tertiary_description',
             name = i18n.arguments.skill.radius_tertiary_description,
             field = 'radius_tertiary_description',
             field = 'radius_tertiary_description',
             type = 'Text',
             type = 'Text',
             func = nil,
             func = h.cast.wrap(m_util.cast.text),
        },
        -- Set manually
        max_level = {
            field = 'max_level',
            type = 'Integer',
        },
        html = {
            field = 'html',
            type = 'Text',
         },
         },
         skill_screenshot = {
         skill_screenshot = {
             name = 'skill_screenshot',
             name = i18n.arguments.skill.skill_screenshot,
             field = 'skill_screenshot',
             field = 'skill_screenshot',
             type = 'Page',
             type = 'Page',
Line 412: Line 510:
                     ss = string.format('File:%s', tpl_args.skill_screenshot_file)
                     ss = string.format('File:%s', tpl_args.skill_screenshot_file)
                 elseif tpl_args.skill_screenshot ~= nil then
                 elseif tpl_args.skill_screenshot ~= nil then
                     ss = string.format(i18n.skill_screenshot, tpl_args.skill_screenshot)
                     ss = string.format(i18n.files.skill_screenshot, tpl_args.skill_screenshot)
                 elseif tpl_args.active_skill_name then
                 elseif tpl_args.active_skill_name then
                     -- When this parameter is set manually, we assume/expect it to be exist, but otherwise it probably doesn't and we don't need dead links in that case
                     -- When this parameter is set manually, we assume/expect it to be exist, but otherwise it probably doesn't and we don't need dead links in that case
                     ss = string.format(i18n.skill_screenshot, tpl_args.active_skill_name)
                     ss = string.format(i18n.files.skill_screenshot, tpl_args.active_skill_name)
                     page = mw.title.new(ss)
                     page = mw.title.new(ss)
                     if page == nil or not page.exists then
                     if page == nil or not page.exists then
Line 423: Line 521:
                 return ss
                 return ss
             end,
             end,
        },
        -- Set programmatically
        max_level = {
            name = nil,
            field = 'max_level',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        html = {
            name = nil,
            field = 'html',
            type = 'Text',
            func = nil,
        },
        -- Deprecated
        has_percentage_mana_cost = {
            name = i18n.arguments.skill.has_percentage_mana_cost,
            field = 'has_percentage_mana_cost',
            type = 'Boolean',
            func = h.cast.wrap(m_util.cast.boolean),
            default = false,
            deprecated = true,
        },
        has_reservation_mana_cost = {
            name = i18n.arguments.skill.has_reservation_mana_cost,
            field = 'has_reservation_mana_cost',
            type = 'Boolean',
            func = h.cast.wrap(m_util.cast.boolean),
            default = false,
            deprecated = true,
         },
         },
     },
     },
Line 429: Line 557:
tables.progression = {
tables.progression = {
     table = 'skill_levels',
     table = 'skill_levels',
    order = {'level_requirement', 'dexterity_requirement', 'intelligence_requirement', 'strength_requirement', 'mana_multiplier', 'critical_strike_chance', 'mana_cost', 'attack_speed_multiplier', 'damage_effectiveness', 'stored_uses', 'cooldown', 'vaal_souls_requirement', 'vaal_stored_uses', 'vaal_soul_gain_prevention_time', 'damage_multiplier', 'duration', 'experience', 'stat_text'},
     fields = {
     fields = {
         level = {
         level = {
Line 436: Line 563:
             type = 'Integer',
             type = 'Integer',
             func = nil,
             func = nil,
            header = nil,
         },
         },
         level_requirement = {
         level_requirement = {
             name = 'level_requirement',
             name = i18n.arguments.skill.level_requirement,
             field = 'level_requirement',
             field = 'level_requirement',
             type = 'Integer',
             type = 'Integer',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.level_requirement,
         },
         },
         dexterity_requirement = {
         dexterity_requirement = {
             name = 'dexterity_requirement',
             name = i18n.arguments.skill.dexterity_requirement,
             field = 'dexterity_requirement',
             field = 'dexterity_requirement',
             type = 'Integer',
             type = 'Integer',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.dexterity_requirement,
         },
         },
         strength_requirement = {
         strength_requirement = {
             name = 'strength_requirement',
             name = i18n.arguments.skill.strength_requirement,
             field = 'strength_requirement',
             field = 'strength_requirement',
             type = 'Integer',
             type = 'Integer',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.strength_requirement,
         },
         },
         intelligence_requirement = {
         intelligence_requirement = {
             name = 'intelligence_requirement',
             name = i18n.arguments.skill.intelligence_requirement,
             field = 'intelligence_requirement',
             field = 'intelligence_requirement',
             type = 'Integer',
             type = 'Integer',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.intelligence_requirement,
         },
         },
         mana_multiplier = {
         mana_multiplier = {
             name = 'mana_multiplier',
             name = i18n.arguments.skill.mana_multiplier,
             field = 'mana_multiplier',
             field = 'mana_multiplier',
             type = 'Float',
             type = 'Float',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.mana_multiplier,
             fmt = '%s%%',
             fmt = '%s%%',
         },
         },
         critical_strike_chance = {
         critical_strike_chance = {
             name = 'critical_strike_chance',
             name = i18n.arguments.skill.critical_strike_chance,
             field = 'critical_strike_chance',
             field = 'critical_strike_chance',
             type = 'Float',
             type = 'Float',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.critical_strike_chance,
             fmt = '%s%%',
             fmt = '%s%%',
        },
        mana_cost = {
            name = 'mana_cost',
            field = 'mana_cost',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = function (skill_data)
                if skill_data["skill.has_reservation_mana_cost"] then
                    return i18n.progression.mana_reserved
                else
                    return i18n.progression.mana_cost
                end
            end,
            fmt = function (tpl_args, frame)
                if tpl_args.has_percentage_mana_cost or (tpl_args.skill_data and tpl_args.skill_data['skill.has_percentage_mana_cost']) then
                    str = '%s%%'
                else
                    str = '%s'
                end
           
                return str
            end,
         },
         },
         damage_effectiveness = {
         damage_effectiveness = {
             name = 'damage_effectiveness',
             name = i18n.arguments.skill.damage_effectiveness,
             field = 'damage_effectiveness',
             field = 'damage_effectiveness',
             type = 'Float',
             type = 'Float',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.damage_effectiveness,
             fmt = '%s%%',
             fmt = '%s%%',
         },
         },
         stored_uses = {
         stored_uses = {
             name = 'stored_uses',
             name = i18n.arguments.skill.stored_uses,
             field = 'stored_uses',
             field = 'stored_uses',
             type = 'Integer',
             type = 'Integer',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.stored_uses,
         },
         },
         cooldown = {
         cooldown = {
             name = 'cooldown',
             name = i18n.arguments.skill.cooldown,
             field = 'cooldown',
             field = 'cooldown',
             type = 'Float',
             type = 'Float',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.cooldown,
             fmt = '%ss',
             fmt = '%ss',
         },
         },
         vaal_souls_requirement = {
         vaal_souls_requirement = {
             name = 'vaal_souls_requirement',
             name = i18n.arguments.skill.vaal_souls_requirement,
             field = 'vaal_souls_requirement',
             field = 'vaal_souls_requirement',
             type = 'Integer',
             type = 'Integer',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_souls_requirement,
         },
         },
         vaal_stored_uses = {
         vaal_stored_uses = {
             name = 'vaal_stored_uses',
             name = i18n.arguments.skill.vaal_stored_uses,
             field = 'vaal_stored_uses',
             field = 'vaal_stored_uses',
             type = 'Integer',
             type = 'Integer',
Line 542: Line 636:
         },
         },
         vaal_soul_gain_prevention_time = {
         vaal_soul_gain_prevention_time = {
             name = 'vaal_soul_gain_prevention_time',
             name = i18n.arguments.skill.vaal_soul_gain_prevention_time,
             field = 'vaal_soul_gain_prevention_time',
             field = 'vaal_soul_gain_prevention_time',
             type = 'Float',
             type = 'Float',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_soul_gain_prevention_time,
             fmt = '%ss',
             fmt = '%ss',
         },
         },
         damage_multiplier = {
         damage_multiplier = {
             name = 'damage_multiplier',
             name = i18n.arguments.skill.damage_multiplier,
             field = 'damage_multiplier',
             field = 'damage_multiplier',
             type = 'Float',
             type = 'Float',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.damage_multiplier,
             fmt = '%s%%',
             fmt = '%s%%',
         },
         },
         attack_speed_multiplier = {
         attack_speed_multiplier = {
             name = 'attack_speed_multiplier',
             name = i18n.arguments.skill.attack_speed_multiplier,
             field = 'attack_speed_multiplier',
             field = 'attack_speed_multiplier',
             type = 'Integer',
             type = 'Integer',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.attack_speed_multiplier,
             fmt = '%s%%',
             fmt = '%s%%',
         },
         },
         duration = {
         duration = {
             name = 'duration',
             name = i18n.arguments.skill.duration,
             field = 'duration',
             field = 'duration',
             type = 'Float',
             type = 'Float',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.duration,
             fmt = '%ss',
             fmt = '%ss',
         },
         },
         -- from gem experience, optional
         -- from gem experience, optional
         experience = {
         experience = {
             name = 'experience',
             name = i18n.arguments.skill.experience,
             field = 'experience',
             field = 'experience',
             type = 'Integer',
             type = 'Integer',
             func = h.cast.wrap(m_util.cast.number),
             func = h.cast.wrap(m_util.cast.number),
            hide = true,
         },
         },
         stat_text = {
         stat_text = {
             name = 'stat_text',
             name = i18n.arguments.skill.stat_text,
             field = 'stat_text',
             field = 'stat_text',
             type = 'Text',
             type = 'Text',
            func = h.cast.wrap(m_util.cast.text),
        },
        -- Deprecated
        mana_cost = {
            name = i18n.arguments.skill.mana_cost,
            field = 'mana_cost',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            deprecated = true,
        },
    }
}
tables.skill_costs = {
    table = 'skill_costs',
    fields = {
        set_id = {
            name = nil,
            field = 'set_id',
            type = 'Integer',
             func = nil,
             func = nil,
             hide = true,
        },
        type = {
            name = i18n.arguments.skill.cost_type,
            field = 'type',
            type = 'String',
            func = function(tpl_args, frame, value)
                if value == nil then
                    return nil
                end
                if m_game.constants.skill.cost_types[value] == nil then
                    error(string.format(i18n.errors.skill.invalid_cost_type, value))
                end
                return value
            end,
        },
        is_reservation = {
            name = i18n.arguments.skill.cost_is_reservation,
            field = 'is_reservation',
            type = 'Boolean',
            func = h.cast.wrap(m_util.cast.boolean),
             default = false,
         },
         },
     }
     }
}
}


data.progression_display_order = {'level_requirement', 'dexterity_requirement', 'strength_requirement', 'intelligence_requirement', 'mana_multiplier', 'critical_strike_chance', 'mana_cost', 'damage_effectiveness', 'stored_uses', 'cooldown', 'vaal_souls_requirement', 'vaal_stored_uses', 'vaal_soul_gain_prevention_time', 'damage_multiplier', 'duration', 'attack_speed_multiplier'}
tables.skill_level_costs = {
    table = 'skill_level_costs',
    fields = {
        set_id = {
            name = nil,
            field = 'set_id',
            type = 'Integer',
            func = nil,
        },
        level = {
            name = nil,
            field = 'level',
            type = 'Integer',
            func = nil,
        },
        amount = {
            name = i18n.arguments.skill.cost_amount,
            field = 'amount',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
    },
}


tables.skill_stats_per_level = {
tables.skill_stats_per_level = {
Line 597: Line 748:
     fields = {
     fields = {
         level = {
         level = {
            name = nil,
             field = 'level',
             field = 'level',
             type = 'Integer',
             type = 'Integer',
            func = nil,
         },
         },
         id = {
         id = {
            name = i18n.arguments.skill.stat_id,
             field = 'id',
             field = 'id',
             type = 'String',
             type = 'String',
            func = h.cast.wrap(m_util.cast.text),
         },
         },
         value = {
         value = {
            name = i18n.arguments.skill.stat_value,
             field = 'value',
             field = 'value',
             type = 'Integer',
             type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
         },
         },
     },
     },
}
}
data.max_stats_per_level = 8


tables.skill_quality = {
tables.skill_quality = {
Line 648: Line 803:
     },
     },
}
}
-- ----------------------------------------------------------------------------
-- Data
-- ----------------------------------------------------------------------------
data.skill_progression_table = {
    order = {
        'level',
        'level_requirement',
        'dexterity_requirement',
        'strength_requirement',
        'intelligence_requirement',
        'mana_multiplier',
        'critical_strike_chance',
        'mana_cost',
        'life_cost',
        'energy_shield_cost',
        'rage_cost',
        'mana_reserved',
        'life_reserved',
        'damage_effectiveness',
        'stored_uses',
        'cooldown',
        'vaal_souls_requirement',
        'vaal_stored_uses',
        'vaal_soul_gain_prevention_time',
        'damage_multiplier',
        'duration',
        'attack_speed_multiplier',
    },
    columns = {
        level = {
            name = nil,
            field = 'level',
            func = nil,
            header = i18n.progression.level,
        },
        level_requirement = {
            name = i18n.arguments.skill.level_requirement,
            field = 'level_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.level_requirement,
        },
        dexterity_requirement = {
            name = i18n.arguments.skill.dexterity_requirement,
            field = 'dexterity_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.dexterity_requirement,
        },
        strength_requirement = {
            name = i18n.arguments.skill.strength_requirement,
            field = 'strength_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.strength_requirement,
        },
        intelligence_requirement = {
            name = i18n.arguments.skill.intelligence_requirement,
            field = 'intelligence_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.intelligence_requirement,
        },
        mana_multiplier = {
            name = i18n.arguments.skill.mana_multiplier,
            field = 'mana_multiplier',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.mana_multiplier,
            fmt = '%s%%',
        },
        critical_strike_chance = {
            name = i18n.arguments.skill.critical_strike_chance,
            field = 'critical_strike_chance',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.critical_strike_chance,
            fmt = '%s%%',
        },
        --[[mana_cost = {
            name = i18n.arguments.skill.mana_cost,
            field = 'mana_cost',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = function (skill_data)
                if skill_data["skill.has_reservation_mana_cost"] then
                    return i18n.progression.mana_reserved
                else
                    return i18n.progression.mana_cost
                end
            end,
            deprecated = true,
        },--]]
        mana_cost = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.mana_cost,
        },
        life_cost = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.life_cost,
        },
        energy_shield_cost = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.energy_shield_cost,
        },
        rage_cost = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.rage_cost,
        },
        mana_reserved = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.mana_reserved,
        },
        life_reserved = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.life_reserved,
        },
        damage_effectiveness = {
            name = i18n.arguments.skill.damage_effectiveness,
            field = 'damage_effectiveness',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.damage_effectiveness,
            fmt = '%s%%',
        },
        stored_uses = {
            name = i18n.arguments.skill.stored_uses,
            field = 'stored_uses',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.stored_uses,
        },
        cooldown = {
            name = i18n.arguments.skill.cooldown,
            field = 'cooldown',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.cooldown,
            fmt = '%ss',
        },
        vaal_souls_requirement = {
            name = i18n.arguments.skill.vaal_souls_requirement,
            field = 'vaal_souls_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_souls_requirement,
        },
        vaal_stored_uses = {
            name = i18n.arguments.skill.vaal_stored_uses,
            field = 'vaal_stored_uses',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_stored_uses,
        },
        vaal_soul_gain_prevention_time = {
            name = i18n.arguments.skill.vaal_soul_gain_prevention_time,
            field = 'vaal_soul_gain_prevention_time',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_soul_gain_prevention_time,
            fmt = '%ss',
        },
        damage_multiplier = {
            name = i18n.arguments.skill.damage_multiplier,
            field = 'damage_multiplier',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.damage_multiplier,
            fmt = '%s%%',
        },
        duration = {
            name = i18n.arguments.skill.duration,
            field = 'duration',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.duration,
            fmt = '%ss',
        },
        attack_speed_multiplier = {
            name = i18n.arguments.skill.attack_speed_multiplier,
            field = 'attack_speed_multiplier',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.attack_speed_multiplier,
            fmt = '%s%%',
        },
    },
}
data.max_stats_per_level = 8


data.infobox_table = {
data.infobox_table = {
Line 715: Line 1,053:
     },
     },
     {
     {
         header = i18n.infobox.mana_cost,
         header = i18n.infobox.cost,
         func = h.display.factory.range_value{key='mana_cost'},
        func = function (tpl_args, frame)
            if not tpl_args.skill_costs.has_flat_cost then
                -- Try falling back to deprecated arguments
                if not tpl_args.has_reservation_mana_cost then
                    local range = h.display.factory.range_value{key='mana_cost'}(tpl_args, frame)
                    if range then
                        return string.format('%s %s', range, m_game.constants.skill.cost_types.mana.long_upper)
                    end
                    return
                end
                return
            end
            local sets = {}
            for i=1, #tpl_args.skill_costs do
                if not tpl_args.skill_costs[i].is_reservation then -- Only get flat costs
                    local cost_type = tpl_args.skill_costs[i].type
                    local range = h.display.factory.range_value{key='amount', set_name='costs', set_id=i, map=tables.skill_level_costs}(tpl_args, frame)
                    if range then
                        local fmt
                        if string.find(cost_type, 'percent', 1, true) then
                            fmt = '%s%% %s'
                        else
                            fmt = '%s %s'
                        end
                        sets[#sets+1] = string.format(fmt, range, m_game.constants.skill.cost_types[cost_type].long_upper)
                    end
                end
            end
            return table.concat(sets, ', ')
        end,
    },
    {
        header = i18n.infobox.reservation,
         func = function (tpl_args, frame)
            if not tpl_args.skill_costs.has_reservation_cost then
                -- Try falling back to deprecated arguments
                if tpl_args.has_reservation_mana_cost then
                    local range = h.display.factory.range_value{key='mana_cost'}(tpl_args, frame)
                    if range then
                        if tpl_args.has_percentage_mana_cost then
                            return string.format('%s%% %s', range, m_game.constants.skill.cost_types.mana.long_upper)
                        end
                        return string.format('%s %s', range, m_game.constants.skill.cost_types.mana.long_upper)
                    end
                    return
                end
                return
            end
            local sets = {}
            for i=1, #tpl_args.skill_costs do
                if tpl_args.skill_costs[i].is_reservation then -- Only get reservation costs
                    local cost_type = tpl_args.skill_costs[i].type
                    local range = h.display.factory.range_value{key='amount', set_name='costs', set_id=i, map=tables.skill_level_costs}(tpl_args, frame)
                    if range then
                        local fmt
                        if string.find(cost_type, 'percent', 1, true) then
                            fmt = '%s%% %s'
                        else
                            fmt = '%s %s'
                        end
                        sets[#sets+1] = string.format(fmt, range, m_game.constants.skill.cost_types[cost_type].long_upper)
                    end
                end
            end
            return table.concat(sets, ', ')
        end,
     },
     },
     {
     {
Line 773: Line 1,176:
p.table_skills = m_cargo.declare_factory{data=tables.static}
p.table_skills = m_cargo.declare_factory{data=tables.static}
p.table_skill_levels = m_cargo.declare_factory{data=tables.progression}
p.table_skill_levels = m_cargo.declare_factory{data=tables.progression}
p.table_skill_costs = m_cargo.declare_factory{data=tables.skill_costs}
p.table_skill_level_costs = m_cargo.declare_factory{data=tables.skill_level_costs}
p.table_skill_stats_per_level = m_cargo.declare_factory{data=tables.skill_stats_per_level}
p.table_skill_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 = m_cargo.declare_factory{data=tables.skill_quality}
Line 796: Line 1,201:
     end
     end
     frame = m_util.misc.get_frame(frame)
     frame = m_util.misc.get_frame(frame)
   
    --
    -- Args
    --
    local properties
      
      
     tpl_args.skill_levels = {
     tpl_args.skill_levels = {
Line 843: Line 1,243:
         end
         end
     until q.stat_text == nil
     until q.stat_text == nil
    -- Costs
    for i=1, math.huge do -- repeat until no more cost sets are found
        local prefix = string.format('%s%d_', i18n.arguments.skill.skill_cost, i)
        if tpl_args[prefix .. tables.skill_costs.fields.type.field] == nil then
            break
        end
        local properties = {
            _table = tables.skill_costs.table,
            [tables.skill_costs.fields.set_id.field] = i,
        }
        h.map_to_arg(tpl_args, frame, properties, prefix, tables.skill_costs, nil, 'skill_costs', i)
        if properties.is_reservation then
            tpl_args.skill_costs.has_reservation_cost = true
        else
            tpl_args.skill_costs.has_flat_cost = true
        end
        tpl_args.skill_costs[i] = {
            set_id = properties.set_id,
            type = properties.type,
            is_reservation = properties.is_reservation,
        }
        if not tpl_args.test then
            m_cargo.store(frame, properties)
        end
    end
      
      
     -- Handle level progression
     -- Handle level progression
Line 848: Line 1,274:
     repeat  
     repeat  
         i = i + 1
         i = i + 1
         local prefix = 'level' .. i  
         local prefix = i18n.arguments.skill.level .. i
         local level = m_util.cast.boolean(tpl_args[prefix])
         local level = m_util.cast.boolean(tpl_args[prefix])
         if level == true then
         if level == true then
Line 856: Line 1,282:
             prefix = prefix .. '_'
             prefix = prefix .. '_'
          
          
             if tpl_args[prefix .. 'experience'] ~= nil then
             if tpl_args[prefix .. i18n.arguments.skill.experience] ~= nil then
                 tpl_args.max_level = i
                 tpl_args.max_level = i
             end
             end
              
              
             properties = {
             local properties = {
                 _table = tables.progression.table,
                 _table = tables.progression.table,
                 [tables.progression.fields.level.field] = i
                 [tables.progression.fields.level.field] = i
Line 869: Line 1,295:
             end
             end
              
              
            h.costs(tpl_args, frame, prefix, i)
             h.stats(tpl_args, frame, prefix, i)
             h.stats(tpl_args, frame, prefix, i)
         end
         end
     until level ~= true
     until level ~= true
     -- If no experience is given, assume this is a non skill gem skill.
     -- If no experience is given, assume this is a non skill gem skill.
     tpl_args.max_level = tpl_args.max_level or (i - 1)
     tpl_args.max_level = tpl_args.max_level or (i - 1)
     -- handle static progression
     -- handle static progression
     properties = {
     local prefix = i18n.arguments.skill.static .. '_'
        _table = tables.progression.table,
    do
        [tables.progression.fields.level.field] = 0
        local properties = {
    }
            _table = tables.progression.table,
    h.map_to_arg(tpl_args, frame, properties, 'static_', tables.progression, 0)
            [tables.progression.fields.level.field] = 0
    if not tpl_args.test then
        }
        m_cargo.store(frame, properties)
        h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, 0)
        if not tpl_args.test then
            m_cargo.store(frame, properties)
        end
     end
     end
      
      
     -- Handle static arguments
     -- Handle static arguments
     properties = {
     local properties = {
         _table = tables.static.table,
         _table = tables.static.table,
       
         [tables.static.fields.max_level.field] = tpl_args.max_level
         [tables.static.fields.max_level.field] = tpl_args.max_level
     }
     }
   
     h.map_to_arg(tpl_args, frame, properties, '', tables.static)
     h.map_to_arg(tpl_args, frame, properties, '', tables.static)
     h.stats(tpl_args, frame, 'static_', 0)
     h.costs(tpl_args, frame, prefix, 0)
      
     h.stats(tpl_args, frame, prefix, 0)
      
      
     --
     --
Line 909: Line 1,339:
             local tr = tbl:tag('tr')
             local tr = tbl:tag('tr')
             if infobox_data.header then
             if infobox_data.header then
                local header_text
                if type(infobox_data.header) == 'function' then
                    header_text = infobox_data.header(tpl_args, frame)
                else
                    header_text = infobox_data.header
                end
                 tr
                 tr
                     :tag('th')
                     :tag('th')
                         :wikitext(infobox_data.header)
                         :wikitext(header_text)
                         :done()
                         :done()
             end
             end
Line 949: Line 1,385:
     out = {}
     out = {}
     if mw.ustring.find(tpl_args.skill_id, '_') then
     if mw.ustring.find(tpl_args.skill_id, '_') then
         out[#out+1] = frame:expandTemplate{  
         out[#out+1] = frame:expandTemplate{
             title = 'Incorrect title',  
             title = i18n.templates.incorrect_title,
             args = {title=tpl_args.skill_id}  
             args = {title=tpl_args.skill_id}
         } .. '\n\n\n'
         } .. '\n\n\n'
     end
     end
     if tpl_args.active_skill_name then
     if tpl_args.active_skill_name then
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             i18n.intro_named_id,  
             i18n.messages.intro_named_id,  
             tpl_args.skill_id,  
             tpl_args.skill_id,  
             tpl_args.active_skill_name
             tpl_args.active_skill_name
Line 962: Line 1,398:
     else
     else
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             i18n.intro_unnamed_id,  
             i18n.messages.intro_unnamed_id,  
             tpl_args.skill_id
             tpl_args.skill_id
         )
         )
Line 972: Line 1,408:
             tables.static.table,
             tables.static.table,
             tables.progression.table,
             tables.progression.table,
            tables.skill_stats_per_level.table,
         }
         }
         if #tpl_args.skill_quality > 0 then
         if #tpl_args.skill_quality > 0 then
             attach_tables[#attach_tables+1] = tables.skill_quality.table
             attach_tables[#attach_tables+1] = tables.skill_quality.table
             attach_tables[#attach_tables+1] = tables.skill_quality_stats.table
             attach_tables[#attach_tables+1] = tables.skill_quality_stats.table
        end
        if #tpl_args.skill_costs > 0 then
            attach_tables[#attach_tables+1] = tables.skill_costs.table
            attach_tables[#attach_tables+1] = tables.skill_level_costs.table
        end
        if tpl_args.skill_levels.has_stats then
            attach_tables[#attach_tables+1] = tables.skill_stats_per_level.table
         end
         end
         for _, table_name in ipairs(attach_tables) do
         for _, table_name in ipairs(attach_tables) do
             frame:expandTemplate{title=string.format('Template:Skill/cargo/attach/%s', table_name), args={}}
             frame:expandTemplate{
                title = string.format(i18n.templates.cargo_attach, table_name),
                args = {}
            }
         end
         end
    end
    -- Categories
    local cats = {i18n.categories.skill_data}
    if tpl_args.has_deprecated_arguments then
        cats[#cats+1] = i18n.categories.deprecated_arguments
     end
     end
      
      
     return tostring(container) .. m_util.misc.add_category({'Skill data'}) .. '\n' .. table.concat(out)  
     return tostring(container) .. m_util.misc.add_category(cats) .. '\n' .. table.concat(out)
end
end


Line 1,002: Line 1,453:
     -- Parse column arguments:
     -- Parse column arguments:
     tpl_args.stat_format = {}
     tpl_args.stat_format = {}
     local prefix
     local argument_keys = {
    for i=1, 9 do
         i18n.arguments.progression.header,
         prefix = 'c' .. i .. '_'
        i18n.arguments.progression.abbr,
        local format_keys = {
        i18n.arguments.progression.pattern_extract,
            'header',  
        i18n.arguments.progression.pattern_value,
            'abbr',  
    }
            'pattern_extract',  
    for i=1, math.huge do -- repeat until no more columns are found
            'pattern_value'
         local prefix = string.format('%s%d_', i18n.arguments.progression.column, i)
        }
        if tpl_args[prefix .. argument_keys[1]] == nil then
         local statfmt = {counter = 0}
        for _,v in ipairs(format_keys) do
            statfmt[v] = tpl_args[prefix .. v]
        end
       
        if m_util.table.has_all_value(statfmt, format_keys) then
             break
             break
         end
         end
          
         local statfmt = {counter = 0}
         if m_util.table.has_one_value(statfmt, format_keys) then
         for _, key in ipairs(argument_keys) do
            error(string.format(i18n.errors.all_format_keys_specified, i))
            local arg = prefix .. key
            if tpl_args[arg] == nil then
                error(string.format(i18n.errors.progression.argument_unspecified, arg))
            end
            statfmt[key] = tpl_args[arg]
         end
         end
       
         statfmt.header = m_util.html.abbr(statfmt.abbr, statfmt.header)
         statfmt.header = m_util.html.abbr(statfmt.abbr, statfmt.header)
         statfmt.abbr = nil
         statfmt.abbr = nil
Line 1,029: Line 1,477:
     end
     end
      
      
      
     -- Query skill data
     local results = {}
     local results = {}
    local query = {
        groupBy = 'skill._pageID',
    }
     local skill_data
     local skill_data
   
     local fields = {
     local fields = {
         'skill._pageName',
         '_pageName',
         'skill.has_reservation_mana_cost',
         tables.static.fields.has_reservation_mana_cost.name,
         'skill.has_percentage_mana_cost',
         tables.static.fields.has_percentage_mana_cost.name,
    }
    local query = {
        groupBy = '_pageID',
     }
     }
   
     if tpl_args.skill_id then -- Query by skill id
     if tpl_args.skill_id then
         query.where = string.format('skill_id="%s"', tpl_args.skill_id)
         query.where = string.format(
         results = m_cargo.query({tables.static.table}, fields, query)
            'skill.skill_id="%s"',  
            tpl_args.skill_id
        )  
         results = m_cargo.query({'skill'}, fields, query)
         if #results == 0 then
         if #results == 0 then
             error(i18n.errors.no_results_for_skill_id)
             error(string.format(i18n.errors.progression.no_results_for_skill_id, tpl_args.skill_id))
         end
         end
        skill_data = results[1]
     else -- Query by page name
     else
         page = tpl_args.page or mw.title.getCurrentTitle().prefixedText
         if tpl_args.page then
         query.where = string.format('_pageName="%s"', page)
            page = tpl_args.page
         results = m_cargo.query({tables.static.table}, fields, query)
        else
            page = mw.title.getCurrentTitle().prefixedText
        end
         query.where = string.format('skill._pageName="%s"', page)
       
         results = m_cargo.query({'skill'}, fields, query)
         if #results == 0 then
         if #results == 0 then
             error(i18n.errors.no_results_for_skill_page)
             error(string.format(i18n.errors.progression.no_results_for_skill_page, page))
         end
         end
       
        skill_data = results[1]
     end
     end
      
     skill_data = results[1]
    skill_data[tables.static.fields.has_reservation_mana_cost.name] = m_util.cast.boolean(skill_data[tables.static.fields.has_reservation_mana_cost.name])
    skill_data[tables.static.fields.has_percentage_mana_cost.name] = m_util.cast.boolean(skill_data[tables.static.fields.has_percentage_mana_cost.name])
     tpl_args.skill_data = skill_data
     tpl_args.skill_data = skill_data
      
 
     skill_data["skill.has_reservation_mana_cost"] = m_util.cast.boolean(skill_data["skill.has_reservation_mana_cost"])
     -- Query progression data
    skill_data['skill.has_percentage_mana_cost'] = m_util.cast.boolean(skill_data["skill.has_percentage_mana_cost"])
     fields = {}
      
    for _, fmap in pairs(tables.progression.fields) do
     query.where = string.format(
        fields[#fields+1] = fmap.field
        'skill_levels._pageName="%s"',  
     end
        skill_data['skill._pageName']
     query = {
     )
        where = string.format(
            '_pageName="%s" AND %s > 0',
            skill_data['_pageName'],
            tables.progression.fields.level.field
        ),
        groupBy = string.format(
            '_pageID, %s',
            tables.progression.fields.level.field
        ),
        orderBy = string.format(
            '%s ASC',
            tables.progression.fields.level.field
        ),
     }
    results = m_cargo.query({tables.progression.table}, fields, query)
    if #results == 0 then
        error(i18n.errors.progression.missing_level_data)
    end
    skill_data.levels = results
 
    -- Query cost data
     fields = {}
     fields = {}
     for _, pdata in pairs(tables.progression.fields) do
     for _, fmap in pairs(tables.skill_costs.fields) do
         fields[#fields+1] = string.format('skill_levels.%s', pdata.field)
         fields[#fields+1] = fmap.field
     end
     end
      
     query = {
     results = m_cargo.query(
        where = string.format(
         {'skill_levels'},  
            '_pageName="%s"',
         fields,
            skill_data['_pageName']
         {
        ),
             where=string.format(
        groupBy = string.format(
                 'skill_levels._pageName="%s" AND skill_levels.level > 0',  
            '_pageID, %s',
                 skill_data['skill._pageName']
            tables.skill_costs.fields.set_id.field
        ),
        orderBy = string.format(
            '%s ASC',
            tables.skill_costs.fields.set_id.field
        ),
    }
     results = m_cargo.query({tables.skill_costs.table}, fields, query)
    skill_data.costs = results
    if #results > 0 then -- If skill has costs, query cost data by levels
         fields = {}
        for _, fmap in pairs(tables.skill_level_costs.fields) do
            fields[#fields+1] = fmap.field
         end
         query = {
             where = string.format(
                 '_pageName="%s" AND %s > 0',
                 skill_data['_pageName'],
                tables.skill_level_costs.fields.level.field
            ),
            groupBy = string.format(
                '_pageID, %s, %s',
                tables.skill_level_costs.fields.set_id.field,
                tables.skill_level_costs.fields.level.field
            ),
            orderBy = string.format(
                '%s ASC, %s ASC',
                tables.skill_level_costs.fields.set_id.field,
                tables.skill_level_costs.fields.level.field
             ),
             ),
            groupBy='skill_levels._pageID, skill_levels.level',
            orderBy='skill_levels.level ASC',
         }
         }
    )
        results = m_cargo.query({tables.skill_level_costs.table}, fields, query)
   
        skill_data.costs_by_level = results
    if #results == 0 then
 
        error(i18n.errors.missing_level_data)
        -- Interpolate cost data into level data
        local column
        for _,cdata in ipairs(skill_data.costs) do
            if m_util.cast.boolean(cdata[tables.skill_costs.fields.is_reservation.field]) then
                column = string.format('%s_reserved', cdata[tables.skill_costs.fields.type.field])
            else
                column = string.format('%s_cost', cdata[tables.skill_costs.fields.type.field])
            end
            for _,ldata in ipairs(skill_data.levels) do
                for _,rdata in ipairs(skill_data.costs_by_level) do
                    if rdata[tables.skill_level_costs.fields.set_id.field] == cdata[tables.skill_costs.fields.set_id.field] and rdata[tables.skill_level_costs.fields.level.field] == ldata[tables.progression.fields.level.field] then
                        ldata[column] = rdata[tables.skill_level_costs.fields.amount.field]
                        break
                    end
                end
            end
        end
     end
     end
      
      
    -- Set up html table headers
     headers = {}
     headers = {}
     for i, row in ipairs(results) do
     for _, row in ipairs(skill_data.levels) do
         for k, v in pairs(row) do
         for k, v in pairs(row) do
             headers[k] = true
             headers[k] = true
         end
         end
     end
     end
   
     local tbl = mw.html.create('table')
     local tbl = mw.html.create('table')
     tbl
     tbl
         :attr(
         :attr('class', 'wikitable responsive-table skill-progression-table')
            'class',  
            'wikitable responsive-table skill-progression-table'
        )
   
     local head = tbl:tag('tr')
     local head = tbl:tag('tr')
    head
     for _, key in ipairs(data.skill_progression_table.order) do
        :tag('th')
         local tmap = data.skill_progression_table.columns[key]
            :wikitext('Level')
         if headers[key] then
            :done()
             local text = type(tmap.header) == 'function' and tmap.header(skill_data) or tmap.header
   
     for _, key in ipairs(data.progression_display_order) do
         local pdata = tables.progression.fields[key]
        -- TODO should be nil?
         if pdata.hide == nil and headers['skill_levels.' .. pdata.field] then
             local text
            if type(pdata.header) == 'function' then
                text = pdata.header(skill_data)
            else
                text = pdata.header
            end
             head
             head
                 :tag('th')
                 :tag('th')
Line 1,135: Line 1,619:
         end
         end
     end
     end
   
     for _, statfmt in ipairs(tpl_args.stat_format) do
     for _, statfmt in ipairs(tpl_args.stat_format) do
         head
         head
Line 1,142: Line 1,625:
                 :done()
                 :done()
     end
     end
   
     if headers[tables.progression.fields.experience.field] then
     if headers['skill_levels.experience'] then
         head
         head
             :tag('th')
             :tag('th')
                 :wikitext(m_util.html.abbr(
                 :wikitext(i18n.progression.experience)
                    i18n.progression.exp_short,
                    i18n.progression.exp_long
                    )
                )
                 :done()
                 :done()
             :tag('th')
             :tag('th')
                 :wikitext(m_util.html.abbr(
                 :wikitext(i18n.progression.total_experience)
                    i18n.progression.tot_exp_short,
                    i18n.progression.tot_exp_long
                    )
                )
                 :done()
                 :done()
     end
     end
      
 
     -- Table rows
     local tblrow
     local tblrow
     local lastexp = 0
     local lastexp = 0
     local experience
     local experience
   
     for _, row in ipairs(skill_data.levels) do
     for i, row in ipairs(results) do
         tblrow = tbl:tag('tr')
         tblrow = tbl:tag('tr')
        tblrow
         for _, key in ipairs(data.skill_progression_table.order) do
            :tag('th')
             local tmap = data.skill_progression_table.columns[key]
                :wikitext(row['skill_levels.level'])
             if headers[key] then
                :done()
                 h.int_value_or_na(tpl_args, frame, tblrow, row[key], tmap)
       
         for _, key in ipairs(data.progression_display_order) do
             local pdata = tables.progression.fields[key]
             if pdata.hide == nil and headers['skill_levels.' .. pdata.field] then
                 h.int_value_or_na(
                    tpl_args,  
                    frame,  
                    tblrow,  
                    row['skill_levels.' .. pdata.field],  
                    pdata
                )
             end
             end
         end
         end
          
          
         -- stats
         -- stats
         if row['skill_levels.stat_text'] then
        local stats = {}
         if row[tables.progression.fields.stat_text.field] then
             stats = m_util.string.split(
             stats = m_util.string.split(
                 row['skill_levels.stat_text'],  
                 row[tables.progression.fields.stat_text.field],
                 '<br>'
                 '<br>'
             )
             )
        else
            stats = {}
         end
         end
         for _, statfmt in ipairs(tpl_args.stat_format) do
         for _, statfmt in ipairs(tpl_args.stat_format) do
Line 1,227: Line 1,689:
         -- TODO: Quality stats, afaik no gems use this atm
         -- TODO: Quality stats, afaik no gems use this atm
          
          
         if headers['skill_levels.experience'] then
         if headers[tables.progression.fields.experience.field] then
             experience = tonumber(row['skill_levels.experience'])
             experience = tonumber(row[tables.progression.fields.experience.field])
             if experience ~= nil then
             if experience ~= nil then
                 h.int_value_or_na(
                 h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
                    tpl_args,  
                    frame,  
                    tblrow,  
                    experience - lastexp,  
                    {}
                )
               
                 lastexp = experience
                 lastexp = experience
             else
             else
Line 1,255: Line 1,710:
      
      
     return tostring(tbl) .. m_util.misc.add_category(cats)  
     return tostring(tbl) .. m_util.misc.add_category(cats)  
end
function p.map(arg)
    for key, data in pairs(map[arg].fields) do
        mw.logObject(key)
    end
end
-- ----------------------------------------------------------------------------
-- Debug
-- ----------------------------------------------------------------------------
p._debug = {}
function p._debug.order(frame)
    for _, mapping in ipairs({'static', 'progression'}) do
        for _, key in ipairs(map[mapping].order) do
            if map[mapping].fields[key] == nil then
                mw.logObject(string.format('Missing key in %s.fields: %s', mapping, key))
            end
        end
        for key, _ in pairs(map[mapping].fields) do
            local missing = true
            for _, order_key in ipairs(map[mapping].order) do
                if order_key == key then
                    missing = false
                    break
                end
            end
            if missing then
                mw.logObject(string.format('Missing key in %s.order: %s', mapping, key))
            end
        end
    end
end
end


return p
return p

Revision as of 11:06, 11 May 2021

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


Lua logo

This module depends on the following other modules:

Overview

Module for handling skills with semantic media wiki support

Skill templates

Module:Item2

All templates defined in Module:Skill:

Module:Skill link

All templates defined in Module:Skill link:

ru:Модуль:Skill

-- Skill module

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

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

local mwlanguage = mw.language.getContentLanguage()

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

-- ----------------------------------------------------------------------------
-- i18n
-- ----------------------------------------------------------------------------

local i18n = {
    arguments = {
        skill = {
            -- static
            static = 'static',
            skill_id = 'skill_id',
            cast_time = 'cast_time',
            gem_description = 'gem_description',
            active_skill_name = 'active_skill_name',
            skill_icon = 'skill_icon',
            item_class_id_restriction = 'item_class_id_restriction',
            item_class_restriction = 'item_class_restriction',
            projectile_speed = 'projectile_speed',
            stat_text = 'stat_text',
            quality_stat_text = 'quality_stat_text',
            radius = 'radius',
            radius_description = 'radius_description',
            radius_secondary = 'radius_secondary',
            radius_secondary_description = 'radius_secondary_description',
            radius_tertiary = 'radius_tertiary',
            radius_tertiary_description = 'radius_tertiary_description',
            skill_screenshot = 'skill_screenshot',
            has_percentage_mana_cost = 'has_percentage_mana_cost',
            has_reservation_mana_cost = 'has_reservation_mana_cost',  
            -- progression
            level = 'level',
            level_requirement = 'level_requirement',
            dexterity_requirement = 'dexterity_requirement',
            strength_requirement = 'strength_requirement',
            intelligence_requirement = 'intelligence_requirement',
            mana_multiplier = 'mana_multiplier',
            critical_strike_chance = 'critical_strike_chance',
            damage_effectiveness = 'damage_effectiveness',
            stored_uses = 'stored_uses',
            cooldown = 'cooldown',
            vaal_souls_requirement = 'vaal_souls_requirement',
            vaal_stored_uses = 'vaal_stored_uses',
            vaal_soul_gain_prevention_time = 'vaal_soul_gain_prevention_time',
            damage_multiplier = 'damage_multiplier',
            attack_speed_multiplier = 'attack_speed_multiplier',
            duration = 'duration',
            experience = 'experience',
            mana_cost = 'mana_cost',
            -- costs
            skill_cost = 'skill_cost',
            cost = 'cost',
            cost_type = 'type',
            cost_is_reservation = 'is_reservation',
            cost_amount = 'amount',
            -- stats
            stat = 'stat',
            stat_id = 'id',
            stat_value = 'value',
        },
        progression = {
            column = 'c',
            header = 'header', 
            abbr = 'abbr', 
            pattern_extract = 'pattern_extract', 
            pattern_value = 'pattern_value',
        },
    },

    errors = {
        skill = {
            invalid_item_class_id = 'The item class id "%s" is invalid.',
            invalid_cost_type = 'The cost type "%s" is invalid. Acceptable values are "mana", "life", "energy_shield", "rage", "mana_percent" and "life_percent".',
        },
        progression = {
            argument_unspecified = 'The argument "%s" is unspecified.',
            no_results_for_skill_id = 'Unable to find skill data for skill id "%s".',
            no_results_for_skill_page = 'Unable to find skill data on page "%s".',
            missing_level_data = 'Unable to find skill level progression data.',
        },
    },

    templates = {
        incorrect_title = 'Template:Incorrect title',
        cargo_attach = 'Template:Skill/cargo/attach/%s',
    },
    
    categories = {
        skill_data = 'Skill data',
        deprecated_arguments = 'Pages with deprecated arguments for skill data',
        broken_progression_table = 'Pages with broken skill progression tables',
    },

    files = {
        skill_icon = 'File:%s skill icon.png',
        skill_screenshot = 'File:%s skill screenshot.jpg',
    },
    
    messages = {
        intro_named_id = "'''%s''' is the internal id of the [[skill]] '''%s'''.\n",
        intro_unnamed_id = "'''%s''' is the internal id of an unnamed [[skill]].\n",
    },
    
    infobox = {
        skill_id = 'Skill Id',
        active_skill_name = 'Name',
        skill_icon = 'Icon',
        cast_time = 'Cast Time',
        item_class_restrictions = 'Item Class<br>Restrictions',
        projectile_speed = 'Projectile Speed',
        radius = 'Radius',
        radius_secondary = 'Radius 2',
        radius_tertiary = 'Radius 3',
        level_requirement = 'Level Req.',
        mana_multiplier = 'Mana Multiplier',
        critical_strike_chance = 'Critical Strike Chance',
        cost = 'Cost',
        reservation = 'Reservation',
        attack_speed_multiplier = 'Attack Speed',
        damage_effectiveness = 'Effectiveness of Added Damage',
        stored_uses = 'Stored Uses',
        cooldown = 'Cooldown',
        vaal_souls_requirement = 'Vaal Souls',
        vaal_stored_uses = 'Vaal Stored Uses',
        vaal_soul_gain_prevention_time = 'Soul Gain Prevention',
        damage_multiplier = 'Damage Multiplier',
        duration = 'Base duration',
    },
    
    progression = {
        level = 'Level',
        level_requirement = m_util.html.abbr('[[Image:Level_up_icon_small.png|link=|Lvl.]]', 'Required Level', 'nounderline'),
        dexterity_requirement = m_util.html.abbr('[[Image:DexterityIcon_small.png|link=|dexterity]]', 'Required Dexterity', 'nounderline'),
        strength_requirement = m_util.html.abbr('[[Image:StrengthIcon_small.png|link=|strength]]', 'Required Strength', 'nounderline'),
        intelligence_requirement = m_util.html.abbr('[[Image:IntelligenceIcon_small.png|link=|intelligence]]', 'Required Intelligence', 'nounderline'),
        mana_multiplier = 'Mana<br>Multiplier',
        critical_strike_chance = 'Critical<br>Strike<br>Chance',
        mana_cost = 'Mana<br>Cost',
        life_cost = 'Life<br>Cost',
        energy_shield_cost = m_util.html.abbr('ES Cost', 'Energy shield cost'),
        rage_cost = 'Rage<br>Cost',
        mana_reserved = 'Mana<br>Reserved',
        life_reserved = 'Life<br>Reserved',
        attack_speed_multiplier = 'Attack<br>Speed<br>Multiplier',
        damage_effectiveness = 'Damage<br>Effectiveness',
        stored_uses = 'Stored<br>Uses',
        cooldown = 'Cooldown',
        vaal_souls_requirement = 'Vaal<br>souls',
        vaal_stored_uses = 'Stored<br>Uses',
        vaal_soul_gain_prevention_time = 'Soul<br>Prevention<br>Time',
        damage_multiplier = m_util.html.abbr('Damage<br>Multiplier', 'Deals x% of base damage'),
        duration = m_util.html.abbr('Base duration', 'Base duration is x seconds'),
        experience = m_util.html.abbr('Exp.', 'Experience needed to level up'),
        total_experience = m_util.html.abbr('Total Exp.', 'Total experience needed'),
        na = 'N/A',
    },
}

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

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

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

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

                -- Deprecated arguments
                if row.deprecated then
                    tpl_args.has_deprecated_arguments = true
                end
            end
        end
    end
end

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

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

function h.na(tr)
    tr
        :tag('td')
            :attr('class', 'table-na')
            :wikitext(i18n.progression.na)
            :done()
end

function h.int_value_or_na(tpl_args, frame, tr, value, pdata)
    value = tonumber(value)
    if value == nil then
        h.na(tr)
    else
        value = mwlanguage:formatNum(value)
        if pdata.fmt ~= nil then
            if type(pdata.fmt) == 'string' then
                value = string.format(pdata.fmt, value)
            elseif type(pdata.fmt) == 'function' then
                value = string.format(pdata.fmt(tpl_args, frame), value)
            end
        end
        tr
            :tag('td')
                :wikitext(value)
                :done()
    end
end

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

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

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

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

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

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

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

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

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

tables.progression = {
    table = 'skill_levels',
    fields = {
        level = {
            name = nil,
            field = 'level',
            type = 'Integer',
            func = nil,
        },
        level_requirement = {
            name = i18n.arguments.skill.level_requirement,
            field = 'level_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        dexterity_requirement = {
            name = i18n.arguments.skill.dexterity_requirement,
            field = 'dexterity_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        strength_requirement = {
            name = i18n.arguments.skill.strength_requirement,
            field = 'strength_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        intelligence_requirement = {
            name = i18n.arguments.skill.intelligence_requirement,
            field = 'intelligence_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        mana_multiplier = {
            name = i18n.arguments.skill.mana_multiplier,
            field = 'mana_multiplier',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        critical_strike_chance = {
            name = i18n.arguments.skill.critical_strike_chance,
            field = 'critical_strike_chance',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        damage_effectiveness = {
            name = i18n.arguments.skill.damage_effectiveness,
            field = 'damage_effectiveness',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        stored_uses = {
            name = i18n.arguments.skill.stored_uses,
            field = 'stored_uses',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        cooldown = {
            name = i18n.arguments.skill.cooldown,
            field = 'cooldown',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%ss',
        },
        vaal_souls_requirement = {
            name = i18n.arguments.skill.vaal_souls_requirement,
            field = 'vaal_souls_requirement',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        vaal_stored_uses = {
            name = i18n.arguments.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.arguments.skill.vaal_soul_gain_prevention_time,
            field = 'vaal_soul_gain_prevention_time',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%ss',
        },
        damage_multiplier = {
            name = i18n.arguments.skill.damage_multiplier,
            field = 'damage_multiplier',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        attack_speed_multiplier = {
            name = i18n.arguments.skill.attack_speed_multiplier,
            field = 'attack_speed_multiplier',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%s%%',
        },
        duration = {
            name = i18n.arguments.skill.duration,
            field = 'duration',
            type = 'Float',
            func = h.cast.wrap(m_util.cast.number),
            fmt = '%ss',
        },
        -- from gem experience, optional
        experience = {
            name = i18n.arguments.skill.experience,
            field = 'experience',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
        },
        stat_text = {
            name = i18n.arguments.skill.stat_text,
            field = 'stat_text',
            type = 'Text',
            func = h.cast.wrap(m_util.cast.text),
        },
        -- Deprecated
        mana_cost = {
            name = i18n.arguments.skill.mana_cost,
            field = 'mana_cost',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            deprecated = true,
        },
    }
}

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

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

tables.skill_stats_per_level = {
    table = 'skill_stats_per_level',
    fields = {
        level = {
            name = nil,
            field = 'level',
            type = 'Integer',
            func = nil,
        },
        id = {
            name = i18n.arguments.skill.stat_id,
            field = 'id',
            type = 'String',
            func = h.cast.wrap(m_util.cast.text),
        },
        value = {
            name = i18n.arguments.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 = {
    order = {
        'level',
        'level_requirement',
        'dexterity_requirement',
        'strength_requirement',
        'intelligence_requirement',
        'mana_multiplier',
        'critical_strike_chance',
        'mana_cost',
        'life_cost',
        'energy_shield_cost',
        'rage_cost',
        'mana_reserved',
        'life_reserved',
        'damage_effectiveness',
        'stored_uses',
        'cooldown',
        'vaal_souls_requirement',
        'vaal_stored_uses',
        'vaal_soul_gain_prevention_time',
        'damage_multiplier',
        'duration',
        'attack_speed_multiplier',
    },
    columns = {
        level = {
            name = nil,
            field = 'level',
            func = nil,
            header = i18n.progression.level,
        },
        level_requirement = {
            name = i18n.arguments.skill.level_requirement,
            field = 'level_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.level_requirement,
        },
        dexterity_requirement = {
            name = i18n.arguments.skill.dexterity_requirement,
            field = 'dexterity_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.dexterity_requirement,
        },
        strength_requirement = {
            name = i18n.arguments.skill.strength_requirement,
            field = 'strength_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.strength_requirement,
        },
        intelligence_requirement = {
            name = i18n.arguments.skill.intelligence_requirement,
            field = 'intelligence_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.intelligence_requirement,
        },
        mana_multiplier = {
            name = i18n.arguments.skill.mana_multiplier,
            field = 'mana_multiplier',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.mana_multiplier,
            fmt = '%s%%',
        },
        critical_strike_chance = {
            name = i18n.arguments.skill.critical_strike_chance,
            field = 'critical_strike_chance',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.critical_strike_chance,
            fmt = '%s%%',
        },
        --[[mana_cost = {
            name = i18n.arguments.skill.mana_cost,
            field = 'mana_cost',
            type = 'Integer',
            func = h.cast.wrap(m_util.cast.number),
            header = function (skill_data)
                if skill_data["skill.has_reservation_mana_cost"] then
                    return i18n.progression.mana_reserved
                else
                    return i18n.progression.mana_cost
                end
            end,
            deprecated = true,
        },--]]
        mana_cost = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.mana_cost,
        },
        life_cost = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.life_cost,
        },
        energy_shield_cost = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.energy_shield_cost,
        },
        rage_cost = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.rage_cost,
        },
        mana_reserved = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.mana_reserved,
        },
        life_reserved = {
            name = nil,
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.life_reserved,
        },
        damage_effectiveness = {
            name = i18n.arguments.skill.damage_effectiveness,
            field = 'damage_effectiveness',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.damage_effectiveness,
            fmt = '%s%%',
        },
        stored_uses = {
            name = i18n.arguments.skill.stored_uses,
            field = 'stored_uses',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.stored_uses,
        },
        cooldown = {
            name = i18n.arguments.skill.cooldown,
            field = 'cooldown',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.cooldown,
            fmt = '%ss',
        },
        vaal_souls_requirement = {
            name = i18n.arguments.skill.vaal_souls_requirement,
            field = 'vaal_souls_requirement',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_souls_requirement,
        },
        vaal_stored_uses = {
            name = i18n.arguments.skill.vaal_stored_uses,
            field = 'vaal_stored_uses',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_stored_uses,
        },
        vaal_soul_gain_prevention_time = {
            name = i18n.arguments.skill.vaal_soul_gain_prevention_time,
            field = 'vaal_soul_gain_prevention_time',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.vaal_soul_gain_prevention_time,
            fmt = '%ss',
        },
        damage_multiplier = {
            name = i18n.arguments.skill.damage_multiplier,
            field = 'damage_multiplier',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.damage_multiplier,
            fmt = '%s%%',
        },
        duration = {
            name = i18n.arguments.skill.duration,
            field = 'duration',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.duration,
            fmt = '%ss',
        },
        attack_speed_multiplier = {
            name = i18n.arguments.skill.attack_speed_multiplier,
            field = 'attack_speed_multiplier',
            func = h.cast.wrap(m_util.cast.number),
            header = i18n.progression.attack_speed_multiplier,
            fmt = '%s%%',
        },
    },
}

data.max_stats_per_level = 8

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

-- ----------------------------------------------------------------------------
-- Templates
-- ----------------------------------------------------------------------------
local p = {}

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

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

    ]]

    if tpl_args == nil then
        tpl_args = getArgs(frame, {
            parentFirst = true
        })
    end
    frame = m_util.misc.get_frame(frame)
    
    tpl_args.skill_levels = {
        [0] = {},
    }
    
    -- Quality
    tpl_args.skill_quality = {}
    local i = 0
    repeat
        i = i + 1
        local prefix = string.format('quality_type%s', i)
        local q = {
            _table = tables.skill_quality.table,
            set_id = i,
            weight = tonumber(tpl_args[string.format('%s_weight', prefix)]),
            stat_text = tpl_args[string.format('%s_stat_text', prefix)],
        }
        if q.stat_text then
            tpl_args.skill_quality[#tpl_args.skill_quality+1] = q
            m_cargo.store(frame, q)
            
            q.stats = {}
            q._table = nil
            local j = 0
            repeat 
                j = j + 1
                local stat_prefix = string.format('%s_stat%s', prefix, j)
                local s = {
                    _table = tables.skill_quality_stats.table,
                    set_id = i,
                    id = tpl_args[string.format('%s_id', stat_prefix)],
                    value = tonumber(tpl_args[string.format('%s_value', stat_prefix)]),
                }
                if s.id and s.value then
                    q.stats[#q.stats+1] = s
                    m_cargo.store(frame, s)
                end
                
                s._table = nil
            until s.id == nil or s.value == nil 
        end
    until q.stat_text == nil

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

    -- If no experience is given, assume this is a non skill gem skill.
    tpl_args.max_level = tpl_args.max_level or (i - 1)

    -- handle static progression
    local prefix = i18n.arguments.skill.static .. '_'
    do
        local properties = {
            _table = tables.progression.table,
            [tables.progression.fields.level.field] = 0
        }
        h.map_to_arg(tpl_args, frame, properties, prefix, tables.progression, 0)
        if not tpl_args.test then
            m_cargo.store(frame, properties)
        end
    end
    
    -- Handle static arguments
    local properties = {
        _table = tables.static.table,
        [tables.static.fields.max_level.field] = tpl_args.max_level
    }
    h.map_to_arg(tpl_args, frame, properties, '', tables.static)
    h.costs(tpl_args, frame, prefix, 0)
    h.stats(tpl_args, frame, prefix, 0)
    
    --
    -- Infobox progressing
    --
    local infobox = mw.html.create('span')
    infobox:attr('class', 'skill-box')
    
    -- tablular sections
    local tbl = infobox:tag('table')
    tbl:attr('class', 'wikitable skill-box-table')
    for _, infobox_data in ipairs(data.infobox_table) do
        local display = infobox_data.func(tpl_args, frame)
        if display then
            local tr = tbl:tag('tr')
            if infobox_data.header then
                local header_text
                if type(infobox_data.header) == 'function' then
                    header_text = infobox_data.header(tpl_args, frame)
                else
                    header_text = infobox_data.header
                end
                tr
                    :tag('th')
                        :wikitext(header_text)
                        :done()
            end
            local td = tr:tag('td')
            td:wikitext(display)
            td:attr('class', infobox_data.class or 'tc -value')
            if infobox_data.header == nil then
                td:attr('colspan', 2)
            end
        end
    end
    
    infobox = tostring(infobox)

    
    --
    -- Store data
    --
    properties[tables.static.fields.html.field] = infobox
    if not tpl_args.test then
        m_cargo.store(frame, properties)
    end
    
    --
    --
    --
    local container = mw.html.create('span')
    container
        :attr('class', 'skill-box-page-container')
        :wikitext(infobox)
    if tpl_args.skill_screenshot then
        container
            :wikitext(string.format('[[%s]]', tpl_args.skill_screenshot))
    end

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

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

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

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

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

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

        -- Interpolate cost data into level data
        local column
        for _,cdata in ipairs(skill_data.costs) do
            if m_util.cast.boolean(cdata[tables.skill_costs.fields.is_reservation.field]) then
                column = string.format('%s_reserved', cdata[tables.skill_costs.fields.type.field])
            else
                column = string.format('%s_cost', cdata[tables.skill_costs.fields.type.field])
            end
            for _,ldata in ipairs(skill_data.levels) do
                for _,rdata in ipairs(skill_data.costs_by_level) do
                    if rdata[tables.skill_level_costs.fields.set_id.field] == cdata[tables.skill_costs.fields.set_id.field] and rdata[tables.skill_level_costs.fields.level.field] == ldata[tables.progression.fields.level.field] then
                        ldata[column] = rdata[tables.skill_level_costs.fields.amount.field]
                        break
                    end
                end
            end
        end
    end
    
    -- Set up html table headers
    headers = {}
    for _, row in ipairs(skill_data.levels) do
        for k, v in pairs(row) do
            headers[k] = true
        end
    end
    local tbl = mw.html.create('table')
    tbl
        :attr('class', 'wikitable responsive-table skill-progression-table')
    local head = tbl:tag('tr')
    for _, key in ipairs(data.skill_progression_table.order) do
        local tmap = data.skill_progression_table.columns[key]
        if headers[key] then
            local text = type(tmap.header) == 'function' and tmap.header(skill_data) 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 _, key in ipairs(data.skill_progression_table.order) do
            local tmap = data.skill_progression_table.columns[key]
            if headers[key] then
                h.int_value_or_na(tpl_args, frame, tblrow, row[key], 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
                h.na(tblrow)
            else
                -- used to find broken progression due to game updates
                -- for example:
                statfmt.counter = statfmt.counter + 1
                tblrow
                    :tag('td')
                        :wikitext(string.format(
                            statfmt.pattern_value, 
                            match[1], 
                            match[2], 
                            match[3], 
                            match[4], 
                            match[5]
                            )
                        )
                        :done()
            end
        end
        
        -- TODO: Quality stats, afaik no gems use this atm
        
        if headers[tables.progression.fields.experience.field] then
            experience = tonumber(row[tables.progression.fields.experience.field])
            if experience ~= nil then
                h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
                lastexp = experience
            else
                h.na(tblrow)
            end
            h.int_value_or_na(tpl_args, frame, tblrow, experience, {})
        end
    end
    
    local cats = {}
    for _, statfmt in ipairs(tpl_args.stat_format) do
        if statfmt.counter == 0 then
            cats = i18n.categories.broken_progression_table
            break
        end
    end
    
    return tostring(tbl) .. m_util.misc.add_category(cats) 
end

return p