Module:Skill: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
(Avoiding magic numbers is one of the most fundamental rules of programming.)
(Support for new costs structure in version 3.14.0)
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)
                    return string.format('%s %s', range, m_game.constants.skill.cost_types.mana.long_upper)
                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 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
            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,170:
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,195:
     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,237:
         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,268:
     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,276:
             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,289:
             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,333:
             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,379:
     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,392:
     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,402:
             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,447:
     -- 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,471:
     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,613:
         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,619:
                 :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,683:
         -- 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,704:
      
      
     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 07:45, 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)
                    return string.format('%s %s', range, m_game.constants.skill.cost_types.mana.long_upper)
                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 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
            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