A large portion of relevant modules/templates have now been switched to cargo. Various usages of SMW throughout the wiki need to be replaced by the new functions, in particular item tables. If some pages do not show up but contain no errors, please null-edit them. To see how you can help with the port check out Path_of_Exile_Wiki:To-do_list/SMW_migration (and leave a comment on the talk page if you have questions).

Module:Skill

From Path of Exile Wiki
Jump to: navigation, search


Overview

Module for handling skills with semantic media wiki support

List of currently implemented templates


local p = {}

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

local mwlanguage = mw.language.getContentLanguage()

--
-- Data
--

local i18n = {
    skill_icon = 'File:%s skill icon.png',
    
    args = {
        -- skills
        skill_id = 'skill_id',
        is_support_gem = 'is_support_gem',
        support_gem_letter = 'support_gem_letter',
        cast_time = 'cast_time',
        gem_description = 'gem_description',
        active_skill_name = 'active_skill_name',
        item_class_restriction = 'item_class_restriction',
        projectile_speed = 'projectile_speed',
        stat_text = 'stat_text',
        quality_stat_text = 'quality_stat_text',
        has_percentage_mana_cost = 'has_percentage_mana_cost',
        has_reservation_mana_cost = 'has_reservation_mana_cost',
        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_levels
        level_requirement = 'level_requirement',
        dexterity_requirement = 'dexterity_requirement',
        intelligence_requirement = 'intelligence_requirement',
        strength_requirement = 'strength_requirement',
        mana_multiplier = 'mana_multiplier',
        critical_strike_chance = 'critical_strike_chance',
        mana_cost = 'mana_cost',
        damage_effectiveness = 'damage_effectiveness',
        stored_uses = 'stored_uses',
        cooldown = 'cooldown',
        vaal_souls_requirement = 'vaal_souls_requirement',
        vaal_stored_uses = 'vaal_stored_uses',
        damage_multiplier = 'damage_multiplier',
        experience = 'experience',
        stat_text = 'stat_text',
        quality_stat_text = 'quality_stat_text',
        
        -- skill_stats_per_level
        id = 'id',
        value = 'value',
        
        -- prefixes
        prefix_level = 'level',
        prefix_static = 'static_',
        prefix_quality_stat = 'quality_',
        infix_stat = 'stat'
    },
}

--
-- Data
--

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

local map = {}
map.stats = {
    {
        prefix_in = '',
        out = 'stats',
        quality = false,
    },
    {
        prefix_in = i18n.args.prefix_quality_stat,
        out = 'quality_stats',
        quality = true,
    },
}

map.static = {
    table = 'skill',
    fields = {
        -- GrantedEffects.dat
        skill_id = {
            name = i18n.args.skill_id,
            field = 'skill_id',
            type = 'String',
            func = nil,
        },
        --[[is_support_gem = {
            name = i18n.args.is_support_gem,
            field = 'is_support_gem',
            type = 'Boolean',
            func = data.cast.wrap(m_util.cast.boolean),
        },
        support_gem_letter = {
            name = i18n.args.support_gem_letter,
            field = 'support_gem_letter',
            type = 'String (size=2)',
            func = nil,
        },-]]--
        -- Active Skills.dat
        cast_time = {
            name = i18n.args.cast_time,
            field = 'cast_time',
            type = 'Float',
            func = data.cast.wrap(m_util.cast.number),
        },
        gem_description = {
            name = i18n.args.gem_description,
            field = 'description',
            type = 'Text',
            func = nil,
        },
        active_skill_name = {
            name = i18n.args.active_skill_name,
            field = 'active_skill_name',
            type = 'String',
            func = nil,
        },
        skill_icon = {
            name = i18n.args.skill_icon,
            field = 'skill_icon',
            type = 'Page',
            func = function(tpl_args, frame)
                tpl_args.skill_icon = string.format(i18n.skill_icon, tpl_args.active_skill_name)
            end
        },
        item_class_restriction = {
            name = i18n.args.item_class_restriction,
            field = 'item_class_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
                    local result = m_util.table.find_in_nested_array{value=v,key='full', tbl=m_game.constants.item.class}
                    if result == nil then
                        error(string.format('Invalid item class: %s', v))
                    end
                end
                return value
            end,
        },
        -- Projectiles.dat - manually mapped to the skills
        projectile_speed = {
            name = i18n.args.projectile_speed,
            field = 'projectile_speed',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
        },
        -- Misc data derieved from stats
        stat_text = {
            name = i18n.args.stat_text,
            field = 'stat_text',
            type = 'Text',
            func = nil,
        },
        quality_stat_text = {
            name = i18n.args.quality_stat_text,
            field = 'quality_stat_text',
            type = 'Text',
            func = nil,
        },
        -- Misc data currently not from game data
        has_percentage_mana_cost = {
            name = i18n.args.has_percentage_mana_cost,
            field = 'has_percentage_mana_cost',
            type = 'Boolean',
            func = data.cast.wrap(m_util.cast.boolean),
        },
        has_reservation_mana_cost = {
            name = i18n.args.has_reservation_mana_cost,
            field = 'has_reservation_mana_cost',
            type = 'Boolean',
            func = data.cast.wrap(m_util.cast.boolean),
        },
        radius = {
            name = i18n.args.radius,
            field = 'radius',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
        },
        radius_description = {
            name = i18n.args.radius_description,
            field = 'radius_description',
            type = 'Text',
            func = nil,
        },
        radius_secondary = {
            name = i18n.args.radius_secondary,
            field = 'radius_secondary',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
        },
        radius_secondary_description = {
            name = i18n.args.radius_secondary_description,
            field = 'radius_secondary_description',
            type = 'Text',
            func = nil,
        },
        -- not sure if any skill actually has 3 radius componets
        radius_tertiary = {
            name = i18n.args.radius_tertiary,
            field = 'radius_tertiary',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
        },
        radius_tertiary_description = {
            name = i18n.args.radius_tertiary_description,
            field = 'radius_tertiary_description',
            type = 'Text',
            func = nil,
        },
        -- Set manually
        max_level = {
            field = 'max_level',
            type = 'Integer',
        },
    },
}

map.progression = {
    table = 'skill_levels',
    fields = {
        level = {
            name = nil,
            field = 'level',
            type = 'Integer',
            func = nil,
            header = nil,
        },
        level_requirement = {
            name = i18n.args.level_requirement,
            field = 'level_requirement',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            header = m_util.html.abbr('[[Image:Level_up_icon_small.png|link=|Lvl.]]', 'Required Level', 'nounderline'),
        },
        dexterity_requirement = {
            name = i18n.args.dexterity_requirement,
            field = 'dexterity_requirement',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            header = m_util.html.abbr('[[Image:DexterityIcon_small.png|link=|dexterity]]', 'Required Dexterity', 'nounderline'),
        },
        strength_requirement = {
            name = i18n.args.strength_requirement,
            field = 'strength_requirement',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            header = m_util.html.abbr('[[Image:StrengthIcon_small.png|link=|strength]]', 'Required Strength', 'nounderline'),
        },
        intelligence_requirement = {
            name = i18n.args.intelligence_requirement,
            field = 'intelligence_requirement',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            header = m_util.html.abbr('[[Image:IntelligenceIcon_small.png|link=|intelligence]]', 'Required Intelligence', 'nounderline'),
        },
        mana_multiplier = {
            name = i18n.args.mana_multiplier,
            field = 'mana_multiplier',
            type = 'Float',
            func = data.cast.wrap(m_util.cast.number),
            header = 'Mana<br>Multiplier',
            fmt = '%s%%',
        },
        critical_strike_chance = {
            name = i18n.args.critical_strike_chance,
            field = 'critical_strike_chance',
            type = 'Float',
            func = data.cast.wrap(m_util.cast.number),
            header = 'Critical<br>Strike<br>Chance',
            fmt = '%s%%',
        },
        mana_cost = {
            name = i18n.args.mana_cost,
            field = 'mana_cost',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            header = function (skill_data)
                if skill_data["skill.has_reservation_mana_cost"] then
                    return 'Mana<br>Reserved'
                else
                    return 'Mana<br>Cost'
                end
            end,
            fmt = function (tpl_args, frame, value)
                local str
                if tpl_args.has_percentage_mana_cost then
                    str = '%s%%'
                else
                    str = '%s'
                end
            
                return string.format(str, value)
            end,
        },
        damage_effectiveness = {
            name = i18n.args.damage_effectiveness,
            field = 'damage_effectiveness',
            type = 'Float',
            func = data.cast.wrap(m_util.cast.number),
            header = 'Damage<br>Effectiveness',
            fmt = '%s%%',
        },
        stored_uses = {
            name = i18n.args.stored_uses,
            field = 'stored_uses',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            header = 'Stored<br>Uses',
        },
        cooldown = {
            name = i18n.args.cooldown,
            field = 'cooldown',
            type = 'Float',
            func = data.cast.wrap(m_util.cast.number),
            header = 'Cooldown',
            fmt = '%ss',
        },
        vaal_souls_requirement = {
            name = i18n.args.vaal_souls_requirement,
            field = 'vaal_souls_requirement',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            header = 'Vaal<br>souls',
        },
        vaal_stored_uses = {
            name = i18n.args.vaal_stored_uses,
            field = 'vaal_stored_uses',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            header = 'Stored<br>Uses',
        },
        damage_multiplier = {
            name = i18n.args.damage_multiplier,
            field = 'damage_multiplier',
            type = 'Float',
            func = data.cast.wrap(m_util.cast.number),
            header = m_util.html.abbr('Damage<br>Multiplier', 'Deals x% of Base Damage'),
            fmt = '%s%%',
        },
        -- from gem experience, optional
        experience = {
            name = i18n.args.experience,
            field = 'experience',
            type = 'Integer',
            func = data.cast.wrap(m_util.cast.number),
            hide = true,
        },
        stat_text = {
            name = i18n.args.stat_text,
            field = 'stat_text',
            type = 'Text',
            func = nil,
            hide = true,
        },
        quality_stat_text = {
            name = i18n.args.quality_stat_text,
            field = 'quality_stat_text',
            type = 'Text',
            func = nil,
            hide = true,
        },
    }
}

data.progression_display_order = {'level_requirement', 'dexterity_requirement', 'strength_requirement', 'intelligence_requirement', 'mana_multiplier', 'critical_strike_chance', 'mana_cost', 'damage_effectiveness', 'stored_uses', 'cooldown', 'vaal_souls_requirement', 'vaal_stored_uses', 'damage_multiplier'}

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

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

function h.map_to_arg(tpl_args, frame, properties, prefix_in, map, level)
    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 then    
                if level ~= nil then
                    tpl_args.skill_levels[level][key] = val
                    -- Nuke variables since they're remapped to skill_levels
                    tpl_args[prefix_in .. row.name] = nil
                else
                    tpl_args[row.name] = val
                end
                properties[row.field] = val
            end
        end
    end
end

function h.stats(tpl_args, frame, prefix_in, level)
    for _, stat_type in ipairs(map.stats) do
        local type_prefix = string.format('%s%s%s', prefix_in, stat_type.prefix_in, i18n.args.infix_stat)
        tpl_args.skill_levels[level][stat_type.out] = {}
        for i=1, 8 do
            local stat_id_key = string.format('%s%s_%s', type_prefix, i, i18n.args.id)
            local stat_val_key = string.format('%s%s_%s', type_prefix, i, i18n.args.value)
            local stat = {
                id = tpl_args[stat_id_key],
                value = tonumber(tpl_args[stat_val_key]),
            }
            if stat.id ~= nil and stat.value ~= nil then
                tpl_args.skill_levels[level][stat_type.out][#tpl_args.skill_levels[level][stat_type.out]+1] = stat
                
                m_util.cargo.store(frame, {
                    _table = map.skill_stats_per_level.table,
                    [map.skill_stats_per_level.fields.level.field] = level,
                    [map.skill_stats_per_level.fields.id.field] = stat.id,
                    [map.skill_stats_per_level.fields.value.field] = stat.value,
                    [map.skill_stats_per_level.fields.is_quality_stat.field] = stat_type.quality,
                })
                
                -- 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

function h.na(tr)
    tr
        :tag('td')
            :attr('class', 'table-na')
            :wikitext('N/A')
            :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 = pdata.fmt(tpl_args, frame, value)
            end
        end
        tr
            :tag('td')
                :wikitext(value)
                :done()
    end
end

--
-- For Template:Skill
--
p.table_skills = m_util.cargo.declare_factory{data=map.static}
p.table_skill_levels = m_util.cargo.declare_factory{data=map.progression}
p.table_skill_stats_per_level = m_util.cargo.declare_factory{data=map.skill_stats_per_level}

function p.skill(frame, tpl_args)
    if tpl_args == nil then
        tpl_args = getArgs(frame, {
            parentFirst = true
        })
    end
    frame = m_util.misc.get_frame(frame)
    
    --
    -- Args
    --
    local properties
    
    tpl_args.skill_levels = {
        [0] = {},
    }
    
    -- Handle level progression
    for i=1, 30 do
        local prefix = i18n.args.prefix_level .. i 
        if m_util.cast.boolean(tpl_args[prefix]) == true then
            -- Don't need this anymore
            tpl_args[prefix] = nil
            tpl_args.skill_levels[i] = {}
            prefix = prefix .. '_'
        
            if tpl_args[prefix .. i18n.args.experience] ~= nil then
                tpl_args.max_level = i
            end
            
            properties = {
                _table = map.progression.table,
                [map.progression.fields.level.field] = i
            }
            h.map_to_arg(tpl_args, frame, properties, prefix, map.progression, i)
            m_util.cargo.store(frame, properties)
            
            h.stats(tpl_args, frame, prefix, i)        
        end
    end
    -- handle static progression
    properties = {
        _table = map.progression.table,
        [map.progression.fields.level.field] = 0
    }
    h.map_to_arg(tpl_args, frame, properties, i18n.args.prefix_static, map.progression, 0)
    m_util.cargo.store(frame, properties)
    
    -- Handle static arguments
    properties = {
        _table = map.static.table,
        [map.static.fields.max_level.field] = tpl_args.max_level
    }
    
    h.map_to_arg(tpl_args, frame, properties, '', map.static)
    h.stats(tpl_args, frame, i18n.args.prefix_static, 0)
    
    m_util.cargo.store(frame, properties)
    mw.logObject(properties)
end

function p.progression(frame)
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    --
    tpl_args.stat_format = {}
    local prefix
    for i=1, 9 do
        prefix = 'c' .. i .. '_' 
        local statfmt = {
            header = tpl_args[prefix .. 'header'],
            abbr = tpl_args[prefix .. 'abbr'],
            pattern_extract = tpl_args[prefix .. 'pattern_extract'],
            pattern_value = tpl_args[prefix .. 'pattern_value'],
            counter = 0,
        }
        if m_util.table.has_all_value(statfmt, {'header', 'abbr', 'pattern_extract', 'pattern_value'}) then
            break
        end
        
        if m_util.table.has_one_value(statfmt, {'header', 'abbr', 'pattern_extract', 'pattern_value'}) then
            error(string.format('All formatting keys must be specified for index "%s"', i))
        end
        
        statfmt.header = m_util.html.abbr(statfmt.abbr, statfmt.header)
        statfmt.abbr = nil
        tpl_args.stat_format[#tpl_args.stat_format+1] = statfmt
    end
    
    
    local result
    local query = {
        groupBy = 'skill._pageID',
    }
    local skill_data
    
    local fields = {
        'skill._pageName',
        'skill.has_reservation_mana_cost',
    }
    
    if tpl_args.skill_id then
        query.where = string.format('skill.skill_id="%s"', tpl_args.skill_id) 
        result = m_util.cargo.query({'skill'}, fields, query)
        if #result == 0 or #result[1] == 0 then
            error('Couldn\'t find a page for the specified skill id')
        end
        skill_data = result[1]
    else
        if tpl_args.page then
            page = tpl_args.page
        else
            page = mw.title.getCurrentTitle().prefixedText
        end
        query.where = string.format('skill._pageName="%s"', page)
        
        result = m_util.cargo.query({'skill'}, fields, query)
        if #result == 0 then
            error('Couldn\'t find the queried data on the skill page')
        end
        
        skill_data = result[1]
    end
    
    skill_data["skill.has_reservation_mana_cost"] = data.cast.wrap(m_util.cast.boolean)(skill_data["skill.has_reservation_mana_cost"])
    
    query.where=string.format('skill_levels._pageName="%s"', skill_data['skill._pageName'])
    fields = {}
    for _, pdata in pairs(map.progression.fields) do
        fields[#fields+1] = string.format('skill_levels.%s', pdata.field)
    end
    
    result = m_util.cargo.query(
        {'skill_levels'}, 
        fields, 
        {
            where=string.format('skill_levels._pageName="%s" AND skill_levels.level > 0', skill_data['skill._pageName']),
            groupBy='skill_levels._pageID, skill_levels.level',
            orderBy='skill_levels.level ASC',
        }
    )
    
    if #result == 0 then
        error('No gem level progression data found')
    end
    
    headers = {}
    for i, row in ipairs(result) do
        for k, v in pairs(row) do
            headers[k] = true
        end
    end
    
    local tbl = mw.html.create('table')
    tbl:attr('class', 'wikitable skill-progression-table')
    
    local head = tbl:tag('tr')
    head
        :tag('th')
            :wikitext('Level')
            :done()
    
    for _, key in ipairs(data.progression_display_order) do
        local pdata = map.progression.fields[key]
        -- TODO should be nil?
        if pdata.hide == nil and headers['skill_levels.' .. pdata.field] then
            local text
            if type(pdata.header) == 'function' then
                text = pdata.header(skill_data)
            else
                text = pdata.header
            end
            head
                :tag('th')
                    :wikitext(text)
                    :done()
        end
    end
    
    for _, statfmt in ipairs(tpl_args.stat_format) do
        head
            :tag('th')
                :wikitext(statfmt.header)
                :done()
    end
    
    if headers['skill_levels.experience'] then
        head
            :tag('th')
                :wikitext(m_util.html.abbr('Exp.', 'Experience Needed to Level Up'))
                :done()
            :tag('th')
                :wikitext(m_util.html.abbr('Total Exp.', 'Total experience needed'))
                :done()
    end
    
    local tblrow
    local lastexp = 0
    local experience
    
    for i, row in ipairs(result) do
        tblrow = tbl:tag('tr')
        tblrow
            :tag('th')
                :wikitext(row['skill_levels.level'])
                :done()
        
        for _, key in ipairs(data.progression_display_order) do
            local pdata = map.progression.fields[key]
            if pdata.hide == nil and headers['skill_levels.' .. pdata.field] then
                h.int_value_or_na(tpl_args, frame, tblrow, row['skill_levels.' .. pdata.field], pdata)
            end
        end
        
        -- stats
        if row['skill_levels.stat_text'] then
            stats = m_util.string.split(row['skill_levels.stat_text'], '<br>')
        else
            stats = {}
        end
        for _, statfmt in ipairs(tpl_args.stat_format) do
            local match = {}
            for j, stat in ipairs(stats) do
                match = {string.match(stat, statfmt.pattern_extract)}
                if #match > 0 then
                    -- TODO maybe remove stat here to avoid testing against in future loops
                    break
                end
            end
            if #match == 0 then
                h.na(tblrow)
            else
                -- used to find broken progression (i.e. due to game updates)
                statfmt.counter = statfmt.counter + 1
                tblrow
                    :tag('td')
                        :wikitext(string.format(statfmt.pattern_value, match[1], match[2], match[3], match[4], match[5]))
                        :done()
            end
        end
        
        -- TODO: Quality stats, afaik no gems use this atm
        
        if headers['skill_levels.experience'] then
            experience = tonumber(row['skill_levels.experience'])
            if experience ~= nil then
                h.int_value_or_na(tpl_args, frame, tblrow, experience - lastexp, {})
                
                lastexp = experience
            else
                h.na(tblrow)
            end
            h.int_value_or_na(tpl_args, frame, tblrow, experience, {})
        end
    end
    
    local empty = ''
    
    for _, statfmt in ipairs(tpl_args.stat_format) do
        if statfmt.counter == 0 then
            empty = '[[Category:Pages with broken skill progression tables]]'
            break
        end
    end
    
    return empty .. tostring(tbl)
end

function p.map(arg)
    for key, data in pairs(map[arg].fields) do
        mw.logObject(key)
    end
end

return p