Module:Modifier compatibility

From Path of Exile Wiki
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]


Implements {{Item modifier compatibility}}.

-------------------------------------------------------------------------------
-- 
--                           Module:Modifier compatibility
-- 
-- This module implements Template:Item modifier compatibility
-------------------------------------------------------------------------------

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

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

local m_game = mw.loadData('Module:Game')

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Modifier compatibility/config/sandbox') or mw.loadData('Module:Modifier compatibility/config')

local i18n = cfg.i18n

-- ----------------------------------------------------------------------------
-- Helper functions
-- ----------------------------------------------------------------------------

local h = {}

function h.genericize_stat_text(str)
    --[[
    This function replaces individual numbers and ranges of numbers with a number sign (#).
    ]]

    str = string.gsub(str, '%(%d+%.?%d*%-%d+%.?%d*%)', '#')
    str = string.gsub(str, '%d+%.?%d*', '#')
    return str
end

-- ----------------------------------------------------------------------------
-- Data and configuration for item mods
-- ----------------------------------------------------------------------------

local item_mods = {}

item_mods.sections = {
    {
        heading = i18n.item_mods.standard_mods,
        where = function (item_data)
            return [[
                mods.id NOT LIKE "%%Royale%%"
            ]]
        end,
    },
    {
        show = function (item_data)
            return item_data.can_have_influences
        end,
        tags = function (item_data)
            return {item_data.addon_tags.shaper}
        end,
        heading = i18n.item_mods.shaper_mods,
    },
    {
        show = function (item_data)
            return item_data.can_have_influences
        end,
        tags = function (item_data)
            return {item_data.addon_tags.elder}
        end,
        heading = i18n.item_mods.elder_mods,
    },
    {
        show = function (item_data)
            return item_data.can_have_influences
        end,
        tags = function (item_data)
            return {item_data.addon_tags.crusader}
        end,
        heading = i18n.item_mods.crusader_mods,
    },
    {
        show = function (item_data)
            return item_data.can_have_influences
        end,
        tags = function (item_data)
            return {item_data.addon_tags.eyrie}
        end,
        heading = i18n.item_mods.redeemer_mods,
    },
    {
        show = function (item_data)
            return item_data.can_have_influences
        end,
        tags = function (item_data)
            return {item_data.addon_tags.basilisk}
        end,
        heading = i18n.item_mods.hunter_mods,
    },
    {
        show = function (item_data)
            return item_data.can_have_influences
        end,
        tags = function (item_data)
            return {item_data.addon_tags.adjudicator}
        end,
        heading = i18n.item_mods.warlord_mods,
    },
    {
        show = function (item_data)
            return item_data.can_have_veiled_mods
        end,
        heading = i18n.item_mods.veiled_mods,
        domain = cfg.mod_domains.unveiled,
    },
    {
        show = function (item_data)
            return item_data.can_be_corrupted
        end,
        heading = i18n.item_mods.corruption_implicit_mods,
        generation_types = {
            {
                id = cfg.mod_generation_types.corrupted,
            },
        },
    },
    {
        show = function (item_data)
            return cfg.eldritch_implicit_item_classes[item_data.class_id] or false
        end,
        heading = i18n.item_mods.eldritch_implicit_mods,
        generation_types = {
            {
                id = cfg.mod_generation_types.searing_exarch,
                heading = i18n.item_mods.searing_exarch,
            },
            {
                id = cfg.mod_generation_types.eater_of_worlds,
                heading = i18n.item_mods.eater_of_worlds,
            },
        },
    },
}

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

local function _item_modifier_compatibility(tpl_args)
    --[[
    This function queries mods that can spawn on an item. It compares
    the item tags and the spawn weight tags. If there's a match and
    the spawn weight is larger than zero, then that mod is added to a
    drop down list.

    To Do:
    * Add support to:
        * Forsaken masters
        * Bestiary
    * Add a proper expand/collapse toggle for the entire header row so
        it reacts together with mw-collapsible.
    * Show Mod group in a better way perhaps:
        Mod group [Collapsible, default=Expanded]
            # to Damage [Collapsible, default=Collapsed]
                3 to Damage
                5 to Damage
    * Add a where condition that somehow filters out mods that obviously
      wont match with the item. mod_spawn_weights.value>0 isn't enough due
      to possible edge cases.


    Examples:
    Weapons
    = p.drop_down_table{item='Rusted Hatchet', header='One Handed Axes'}
    = p.drop_down_table{item='Stone Axe', header='Two Handed Axes'}

    Accessories
    = p.drop_down_table{item='Amber Amulet', header='Amulets'}

    Jewels
    = p.drop_down_table{item='Cobalt Jewel', header='Jewels'}

    Armour
    = p.drop_down_table{item='Plate Vest', header='Body armours'}

    Boots
    = p.drop_down_table{item='Iron Greaves', header='Boots'}

    = p.drop_down_table{
        item='Fishing Rod',
        header='FISH PLEASE',
        item_tags='fishing_rod',
        extra_fields='mods.tags'
    }

    = p.drop_down_table{
        item='Fishing Rod',
        item_tags='axe, one_hand_weapon, onehand, weapon, default',
        extra_item_tags='fishing_rod'
    }

    = p.drop_down_table{
        item='Vaal Blade',
    }

    ]]

    -- Support legacy args
    tpl_args.item_name = tpl_args.item_name or tpl_args.item

    -- Query item
    local item_data = m_item_util.query_item(tpl_args, {
        fields = {
            'items.name=name',
            'items.tags=tags',
            'items.inventory_icon=inventory_icon',
            'items.html=html',
        }
    })
    if item_data.error then
        if not m_util.cast.boolean(tpl_args.nocat) then
            return item_data.error:get_html() .. item_data.error:get_category()
        end
        return item_data.error:get_html()
    end

    -- Get the domain, if it's not defined in the table assume it's
    -- in the item domain.
    item_data.domain = tonumber(tpl_args.domain) or cfg.mod_domains_by_item_class[item_data.class_id] or cfg.mod_domains.item

    -- Format item tags
    tpl_args.item_tags = m_util.cast.table(tpl_args.item_tags or item_data.tags)
    if tpl_args.extra_item_tags then
        local extra_tags = m_util.cast.table(tpl_args.extra_item_tags)
        for _, v in ipairs(extra_tags) do
            tpl_args.item_tags[#tpl_args.item_tags+1] = v
        end
    end

    -- Determine whether the item can have influences
    item_data.can_have_influences = m_util.cast.boolean(m_game.constants.item.classes[item_data.class_id].can_have_influences)

    -- Determine whether the item can be corrupted
    item_data.can_be_corrupted = m_util.cast.boolean(m_game.constants.item.classes[item_data.class_id].can_be_corrupted)

    -- Determine whether the item can have veiled mods
    item_data.can_have_veiled_mods = m_util.cast.boolean(m_game.constants.item.classes[item_data.class_id].can_have_veiled_mods)

    -- Get tags that are appended to influenced items
    item_data.addon_tags = m_game.constants.item.classes[item_data.class_id].tags or {}

    -- Populate mods data
    for _, section in ipairs(item_mods.sections) do
        -- Default generation types
        if type(section.generation_types) ~= 'table' then
            section.generation_types = {
                {
                    id = cfg.mod_generation_types.prefix,
                    heading = i18n.item_mods.prefixes,
                    no_results = i18n.item_mods.prefixes_no_results
                },
                {
                    id = cfg.mod_generation_types.suffix,
                    heading = i18n.item_mods.suffixes,
                    no_results = i18n.item_mods.suffixes_no_results
                }
            }
        end

        -- Show the section? Default: Show
        local show = section.show ~= false
        if type(section.show) == 'function' then
            show = section.show(item_data)
        end
        if show then
            -- Get item tags
            local section_tags = type(section.tags) == 'function' and section.tags(item_data) or section.tags or tpl_args.item_tags
            if type(section_tags) ~= 'table' or #section_tags == 0 then
                error('No tags.')
            end

            -- Build mods data for each generation type
            section.mods_data = {}
            for _, gen_type in ipairs(section.generation_types) do
                section.mods_data[gen_type.id] = {}

                -- Query mods
                local where = {
                    string.format('mods.domain = %s', section.domain or item_data.domain),
                    string.format('mods.generation_type = %s', gen_type.id),
                    'mods.stat_text IS NOT NULL',
                    string.format('mod_spawn_weights.tag IN ("%s")', table.concat(section_tags, '","')),
                }
                if section.where then
                    where[#where+1] = section.where(item_data)
                end
                local results = m_cargo.query(
                    {
                        'mods',
                        'mod_stats',
                        'mod_spawn_weights',
                    },
                    {
                        'mods._pageID',
                        'mods._pageName',
                        'mods.name',
                        'mods.id',
                        'mods.required_level',
                        'mods.generation_type',
                        'mods.domain',
                        'mods.mod_groups',
                        'mods.mod_type',
                        'mods.stat_text',
                        'mods.stat_text_raw',
                        'mods.tags',
                        'mod_stats.id',
                        'mod_spawn_weights.tag',
                        'mod_spawn_weights.value',
                        'mod_spawn_weights.ordinal',
                    },
                    {
                        join = [[
                            mods._pageID=mod_spawn_weights._pageID,
                            mods._pageID=mod_stats._pageID
                        ]],
                        where = table.concat(where, ' AND '),
                        groupBy = 'mods._pageID',
                        having = 'mod_spawn_weights.value > 0',
                        orderBy = [[
                            mods.mod_groups ASC,
                            mods.mod_type ASC,
                            mods.required_level ASC,
                            mod_spawn_weights.ordinal ASC
                        ]],
                    }
                )

                -- Group results
                if #results > 0 then
                    for _, row in ipairs(results) do
                        row['mods.mod_groups'] = m_util.cast.table(row['mods.mod_groups'])
                        row['mods.tags'] = m_util.cast.table(row['mods.tags'])
                        if #row['mods.mod_groups'] > 0 then
                            for _, group in ipairs(row['mods.mod_groups']) do
                                section.mods_data[gen_type.id][group] = section.mods_data[gen_type.id][group] or {}
                                section.mods_data[gen_type.id][group][row['mods.mod_type']] = section.mods_data[gen_type.id][group][row['mods.mod_type']] or {}
                                table.insert(section.mods_data[gen_type.id][group][row['mods.mod_type']], row)
                            end
                        end
                    end
                end
            end

            if tpl_args.debug then
                mw.logObject(section.mods_data)
            end
        end
    end

    -- Build html output
    local html = mw.html.create()
    for _, section in ipairs(item_mods.sections) do
        local section_wrapper
        local empty = true -- Section is empty
        if section.mods_data then
            section_wrapper = mw.html.create('div')
                    :addClass('mod-compat__section')
                    :tag('h3')
                        :addClass('mod-compat__section-heading')
                        :wikitext(section.heading)
                        :done()
            for _, gen_type in ipairs(section.generation_types) do
                local gentype_wrapper = section_wrapper
                    :tag('div')
                        :addClass('mod-compat__gentype')
                local gentype_header = gentype_wrapper
                    :tag('div')
                        :addClass('mod-compat__gentype-header')
                        :tag('span')
                            :addClass('mod-compat__gentype-heading')
                            :wikitext(gen_type.heading or i18n.item_mods.modifiers)
                            :done()
                if type(section.mods_data[gen_type.id]) == 'table' and m_util.table.length(section.mods_data[gen_type.id]) > 0 then
                    empty = false
                    gentype_header
                        :tag('span')
                            :addClass('accordion__controls mw-editsection-like')
                    for gkey, gval in pairs(section.mods_data[gen_type.id]) do
                        local group_wrapper = gentype_wrapper
                            :tag('div')
                                :addClass('mod-compat__group')
                                :tag('div')
                                    :addClass('mod-compat__group-label')
                                    :wikitext( string.format('%s %s', i18n.item_mods.group, gkey) )
                                    :done()
                        local mod_type_list = group_wrapper
                            :tag('dl')
                                :addClass('mod-compat__type-list accordion')
                        for tkey, tval in pairs(gval) do
                            local summary_text = tval[1]['mods.stat_text_raw']
                            if m_util.table.length(tval) > 1 then
                                summary_text = h.genericize_stat_text(summary_text)
                            end
                            local mod_type_heading = mod_type_list
                                :tag('dt')
                                    :addClass('mod-compat__type-summary accordion__toggle')
                                    :wikitext( m_util.html.poe_color('mod', summary_text) )
                            local mod_type_body = mod_type_list
                                :tag('dd')
                                    :addClass('mod-compat__type-details accordion__panel')
                            local mod_table = mod_type_body
                                :tag('table')
                                    :addClass('wikitable modifier-table')
                                    :tag('tr')
                                        :tag('th')
                                            :wikitext(i18n.item_mods.modifier)
                                            :done()
                                        :tag('th')
                                            :wikitext(i18n.item_mods.required_level)
                                            :done()
                                        :tag('th')
                                            :wikitext(i18n.item_mods.stats)
                                            :done()
                                        :tag('th')
                                            :wikitext(i18n.item_mods.tags)
                                            :done()
                                        :done()
                            for _, mod in ipairs(tval) do
                                local name = mod['mods.name'] or mod['mods.mod_type'] or mod['mods.id']
                                local tag_list = mw.html.create('ul')
                                    :addClass('modifier-table__tag-list')
                                for _, tag in ipairs(mod['mods.tags']) do
                                    tag_list
                                        :tag('li')
                                            :addClass('modifier-table__tag')
                                            :wikitext(tag)
                                end
                                mod_table
                                    :tag('tr')
                                        :tag('td')
                                            :wikitext( m_util.html.wikilink(mod['mods._pageName'], name) )
                                            :done()
                                        :tag('td')
                                            :wikitext(mod['mods.required_level'])
                                            :done()
                                        :tag('td')
                                            :wikitext( m_util.html.poe_color('mod', mod['mods.stat_text_raw']) )
                                            :done()
                                        :tag('td')
                                            :node(tag_list)
                                            :done()
                            end
                        end
                    end
                else
                    gentype_wrapper
                        :wikitext(gen_type.no_results)
                end
            end
        end
        if not empty then
            html:node(section_wrapper)
        end
    end
    return tostring(html)
end

-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------

local p = {}

-- 
-- Template:Item modifier compatibility
-- 
p.item_modifier_compatibility = m_util.misc.invoker_factory(_item_modifier_compatibility, {
    wrappers = cfg.wrappers.item_mods,
})

return p