Module:Item table

From Path of Exile Wiki
Revision as of 13:12, 8 January 2018 by >OmegaK2 (Moved p.item_table (Template:Item table) from Module:Item2 to this module)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]


This module is used on 4,000+ pages.

To avoid major disruption and server load, do not make unnecessary edits to this module. Test changes to this module first using its /sandbox and /testcases subpages . All of the changes can then be applied to this module in a single edit.

Consider discussing changes on the talk page or on Discord before implementing them.

The item module provides functionality for creating item tables.

Implemented templates

This module implements the following templates:

ru:Модуль:Item table

-- Item table
--
--

-- ----------------------------------------------------------------------------
-- Imports
-- ----------------------------------------------------------------------------
local m_util = require('Module:Util')
local getArgs = require('Module:Arguments').getArgs
local m_game = require('Module:Game')
local f_item_link = require('Module:Item link').item_link

local cargo = mw.ext.cargo

-- ----------------------------------------------------------------------------
-- Globals
-- ----------------------------------------------------------------------------

local c = {}
c.query_default = 50
c.query_max = 200

-- ----------------------------------------------------------------------------
-- Strings
-- ----------------------------------------------------------------------------
-- This section contains strings used by this module.
-- Add new strings here instead of in-code directly, this will help other
-- people to correct spelling mistakes easier and help with translation to
-- other PoE wikis.

local i18n = {
    categories = {
        -- maintenance cats
        query_limit = 'Item tables hitting query limit',
        query_hard_limit = 'Item tables hitting hard query limit',
    },

    -- Used by the item table
    item_table = {
        item = 'Item',
        skill_gem = 'Skill gem',

        physical_dps = m_util.html.abbr('pDPS', 'physical damage per second'),
        fire_dps = m_util.html.abbr('Fire DPS', 'fire damage per second'),
        cold_dps = m_util.html.abbr('Cold DPS', 'cold damage per second'),
        lightning_dps = m_util.html.abbr('Light. DPS', 'lightning damage per second'),
        chaos_dps = m_util.html.abbr('Chaos DPS', 'chaos damage per second'),
        elemental_dps = m_util.html.abbr('eDPS', 'elemental damage (i.e. fire/cold/lightning) per second'),
        poison_dps = m_util.html.abbr('Poison DPS', 'poison damage (i.e. physical/chaos) per second'),
        dps = m_util.html.abbr('DPS', 'total damage (i.e. physical/fire/cold/lightning/chaos) per second'),
        base_item = 'Base Item',
        item_class = 'Item Class',
        essence_level = 'Essence<br>Level',
        drop_level = 'Drop<br>Level',
        drop_leagues = 'Drop Leagues',
        drop_areas = 'Drop Areas',
        drop_text = 'Additional<br>Drop Restrictions',
        stack_size = 'Stack<br>Size',
        stack_size_currency_tab = m_util.html.abbr('Tab<br>Stack<br>Size', 'Stack size in the currency stash tab'),
        armour = m_util.html.abbr('AR', 'Armour'),
        evasion = m_util.html.abbr('EV', 'Evasion Rating'),
        energy_shield = m_util.html.abbr('ES', 'Energy Shield'),
        block = m_util.html.abbr('Block', 'Chance to Block'),
        damage = m_util.html.abbr('Damage', 'Colour coded damage'),
        attacks_per_second = m_util.html.abbr('APS', 'Attacks per second'),
        local_critical_strike_chance = m_util.html.abbr('Crit', 'Local weapon critical strike chance'),
        flask_life = m_util.html.abbr('Life', 'Life regenerated over the flask duration'),
        flask_mana = m_util.html.abbr('Mana', 'Mana regenerated over the flask duration'),
        flask_duration = 'Duration',
        flask_charges_per_use = m_util.html.abbr('Usage', 'Number of charges consumed on use'),
        flask_maximum_charges = m_util.html.abbr('Capacity', 'Maximum number of flask charges held'),
        item_limit = 'Limit',
        jewel_radius = 'Radius',
        map_tier = 'Map<br>Tier',
        map_level = 'Map<br>Level',
        map_guild_character = m_util.html.abbr('Char', 'Character for the guild tag'),
        buff_effects = 'Buff Effects',
        stats = 'Stats',
        effects = 'Effect(s)',
        flavour_text = 'Flavour Text',
        prediction_text = 'Prediction',
        help_text = 'Help Text',
        seal_cost = m_util.html.abbr('Seal<br>Cost', 'Silver Coin cost of sealing this prophecies into an item'), 
        objective = 'Objective',
        reward = 'Reward',
        buff_icon = 'Buff<br>Icon',

        -- Skills
        support_gem_letter = m_util.html.abbr('L', 'Support gem letter.'),
        skill_icon = 'Icon',
        description = 'Description',
        skill_critical_strike_chance = m_util.html.abbr('Crit', 'Critical Strike Chance'),
        cast_time = m_util.html.abbr('Cast<br>Time', 'Casting time of the skill in seconds'),
        damage_effectiveness = m_util.html.abbr('Dmg.<br>Eff.', 'Damage Effectiveness'),
        mana_cost_multiplier = m_util.html.abbr('MCM', 'Mana cost multiplier - missing values indicate it changes on gem level'),
        mana_cost = m_util.html.abbr('Mana', 'Mana cost'),
        reserves_mana_suffix = m_util.html.abbr('R', 'reserves mana'),
        vaal_souls_requirement = m_util.html.abbr('Souls', 'Vaal souls requirement in Normal/Cruel/Merciless difficulty'),
        stored_uses = m_util.html.abbr('Uses', 'Maximum number of stored uses'),
        primary_radius = m_util.html.abbr('R1', 'Primary radius'),
        secondary_radius = m_util.html.abbr('R2', 'Secondary radius'),
        tertiary_radius = m_util.html.abbr('R3', 'Tertiary radius'),
    },

    errors = {
        generic_argument_parameter = 'Unrecognized %s parameter "%s"',
        invalid_item_table_mode = 'Invalid mode for item table',
    },
}

-- ----------------------------------------------------------------------------
-- Helper & utility functions
-- ----------------------------------------------------------------------------

local h = {}

function h.format_value(tpl_args, frame, value, options)
    -- value: table
    --  min:
    --  max:
    -- options: table
    --  fmt: formatter to use for the value instead of valfmt
    --  fmt_range: formatter to use for the range values. Default: (%s to %s)
    --  inline: Use this format string to insert value
    --  inline_color: colour to use for the inline value; false to disable colour
    --  func: Function to adjust the value with before output
    --  color: colour code for m_util.html.poe_color, overrides mod colour
    --  no_color: set to true to ingore colour entirely
    --  return_color: also return colour

    if options.no_color == nil then
        if options.color then
            value.color = options.color
        elseif value.base ~= value.min or value.base ~= value.max then
            value.color = 'mod'
        else
            value.color = 'value'
        end
    end
    
    if options.func ~= nil then
        value.min = options.func(tpl_args, frame, value.min)
        value.max = options.func(tpl_args, frame, value.max)
    end
    
    if options.fmt == nil then
        options.fmt = '%s'
    elseif type(options.fmt) == 'function' then
        options.fmt = options.fmt(tpl_args, frame)
    end
    
    if value.min == value.max then
        value.out = string.format(options.fmt, value.min)
    else
        value.out = string.format(string.format(options.fmt_range or i18n.range, options.fmt, options.fmt), value.min, value.max)
    end
    
    if options.no_color == nil then
        value.out = m_util.html.poe_color(value.color, value.out)
    end
    
    local return_color
    if options.return_color ~= nil then
        return_color = value.color
    end
    
    local text = options.inline
    
    if type(text) == 'string' then
    elseif type(text) == 'function' then
        text = text(tpl_args, frame)
    else
        text = nil
    end
    
    if text and text ~= '' then
        local color
        if options.inline_color == nil then
            color = 'default'
        elseif options.inline_color ~= false then
            color = color.inline_color
        end 
        
        if color ~= nil then
            text = m_util.html.poe_color(color, text)
        end
        
        return string.format(text, value.out), return_color
    end
    
    -- If we didn't return before, return here
    return value.out, return_color
end

h.tbl = {}

function h.tbl.range_fields(field)
    return function()
        local fields = {}
        for _, partial_field in ipairs({'maximum', 'text', 'colour'}) do
            fields[#fields+1] = string.format('%s_range_%s', field, partial_field)
        end
        return fields
    end
end

h.tbl.display = {}
function h.tbl.display.na_or_val(tr, value, data)
    return h.na_or_val(tr, value)
end

function h.tbl.display.seconds(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('%ss', value)
    end)
end

function h.tbl.display.percent(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('%s%%', value)
    end)
end

function h.tbl.display.wikilink(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('[[%s]]', value)
    end)
end

h.tbl.display.factory = {}
function h.tbl.display.factory.value(args)
    args.options = args.options or {}

    return function(tr, data, properties)
        values = {}
        
        for index, prop in ipairs(properties) do
            local value = data[prop]
            if args.options[index] and args.options[index].fmt then
                value = string.format(args.options[index].fmt, value)
            end
            values[#values+1] = value
        end
        
        
        local td = tr:tag('td')
        td:attr('data-sort-value', table.concat(values, ', '))
        td:wikitext(table.concat(values, ', '))
        if args.colour then
            td:attr('class', 'tc -' .. args.colour)
        end
    end
end

function h.tbl.display.factory.range(args)
    -- args: table
    --  property
    return function (tr, data, fields)
        tr
            :tag('td')
                :attr('data-sort-value', data[string.format('%s_range_maximum', args.field)] or '0')
                :attr('class', 'tc -' .. (data[string.format('%s_range_colour', args.field)] or 'default'))
                :wikitext(data[string.format('%s_range_text', args.field)])
                :done()
    end
end

-- ----------------------------------------------------------------------------
-- Data mappings
-- ----------------------------------------------------------------------------

local data_map = {}

-- for sort type see:
-- https://meta.wikimedia.org/wiki/Help:Sorting
data_map.generic_item = {
    {
        arg = 'base_item',
        header = i18n.item_table.base_item,
        fields = {'items.base_item', 'items.base_item_page'},
        display = function(tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['items.base_item'])
                    :wikitext(string.format('[[%s|%s]]', data['items.base_item_page'], data['items.base_item']))
        end,
        order = 1000,
        sort_type = 'text',
    },
    {
        arg = 'class',
        header = i18n.item_table.item_class,
        fields = {'items.class'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='[[%s]]',
            },
        }},
        order = 1001,
        sort_type = 'text',
    },
    {
        arg = 'essence',
        header = i18n.item_table.essence_level,
        fields = {'essences.level'},
        display = h.tbl.display.factory.value{},
        order = 2000,
    },
    {
        arg = {'drop', 'drop_level'},
        header = i18n.item_table.drop_level,
        fields = {'items.drop_level'},
        display = h.tbl.display.factory.value{},
        order = 3000,
    },
    {
        arg = 'stack_size',
        header = i18n.item_table.stack_size,
        fields = {'stackables.stack_size'},
        display = h.tbl.display.factory.value{},
        order = 4000,
    },
    {
        arg = 'stack_size_currency_tab',
        header = i18n.item_table.stack_size_currency_tab,
        fields = {'stackables.stack_size_currency_tab'},
        display = h.tbl.display.factory.value{},
        order = 4001,
    },
    {
        arg = 'level',
        header = m_game.level_requirement.icon,
        fields = h.tbl.range_fields('items.required_level'),
        display = h.tbl.display.factory.range{field='items.required_level'},
        order = 5000,
    },
    {
        arg = 'ar',
        header = i18n.item_table.armour,
        fields = h.tbl.range_fields('armours.armour'),
        display = h.tbl.display.factory.range{field='armours.armour'},
        order = 6000,
    },
    {
        arg = 'ev',
        header =i18n.item_table.evasion,
        fields = h.tbl.range_fields('armours.evasion'),
        display = h.tbl.display.factory.range{field='armours.evasion'},
        order = 6001,
    },
    {
        arg = 'es',
        header = i18n.item_table.energy_shield,
        fields = h.tbl.range_fields('armours.energy_shield'),
        display = h.tbl.display.factory.range{field='armours.energy_shield'},
        order = 6002,
    },
    {
        arg = 'block',
        header = i18n.item_table.block,
        fields = h.tbl.range_fields('shields.block'),
        display = h.tbl.display.factory.range{field='shields.block'},
        order = 6003,
    },
    --[[{
        arg = 'physical_damage_min',
        header = m_util.html.abbr('Min', 'Local minimum weapon damage'),
        fields = h.tbl.range_fields('minimum physical damage'),
        display = h.tbl.display.factory.range{field='minimum physical damage'},
        order = 7000,
    },
    {
        arg = 'physical_damage_max',
        header = m_util.html.abbr('Max', 'Local maximum weapon damage'),
        fields = h.tbl.range_fields('maximum physical damage'),
        display = h.tbl.display.factory.range{field='maximum physical damage'},
        order = 7001,
        
    },]]--
    {
        arg = {'weapon', 'damage'},
        header = i18n.item_table.damage,
        fields = {'weapons.damage_html', 'weapons.damage_avg'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['weapons.damage_avg'])
                    :wikitext(data['weapons.damage_html'])
        end,
        order = 8000,
    },
    {
        arg = {'weapon', 'aps'},
        header = i18n.item_table.attacks_per_second,
        fields = h.tbl.range_fields('weapons.attack_speed'),
        display = h.tbl.display.factory.range{field='weapons.attack_speed'},
        order = 8001,
    },
    {
        arg = {'weapon', 'crit'},
        header = i18n.item_table.local_critical_strike_chance,
        fields = h.tbl.range_fields('weapons.critical_strike_chance'),
        display = h.tbl.display.factory.range{field='weapons.critical_strike_chance'},
        order = 8002,
    },
    {
        arg = {'physical_dps'},
        header = i18n.item_table.physical_dps,
        fields = h.tbl.range_fields('weapons.physical_dps'),
        display = h.tbl.display.factory.range{field='weapons.physical_dps'},
        order = 8100,
    },
    {
        arg = {'lightning_dps'},
        header = i18n.item_table.lightning_dps,
        fields = h.tbl.range_fields('weapons.lightning_dps'),
        display = h.tbl.display.factory.range{field='weapons.lightning_dps'},
        order = 8101,
    },
    {
        arg = {'cold_dps'},
        header = i18n.item_table.cold_dps,
        fields = h.tbl.range_fields('weapons.cold_dps'),
        display = h.tbl.display.factory.range{field='weapons.cold_dps'},
        order = 8102,
    },
    {
        arg = {'fire_dps'},
        header = i18n.item_table.fire_dps,
        fields = h.tbl.range_fields('weapons.fire_dps'),
        display = h.tbl.display.factory.range{field='weapons.fire_dps'},
        order = 8103,
    },
    {
        arg = {'chaos_dps'},
        header = i18n.item_table.chaos_dps,
        fields = h.tbl.range_fields('weapons.chaos_dps'),
        display = h.tbl.display.factory.range{field='weapons.chaos_dps'},
        order = 8104,
    },
    {
        arg = {'elemental_dps'},
        header = i18n.item_table.elemental_dps,
        fields = h.tbl.range_fields('weapons.elemental_dps'),
        display = h.tbl.display.factory.range{field='weapons.elemental_dps'},
        order = 8105,
    },
    {
        arg = {'poison_dps'},
        header = i18n.item_table.poison_dps,
        fields = h.tbl.range_fields('weapons.poison_dps'),
        display = h.tbl.display.factory.range{field='weapons.poison_dps'},
        order = 8106,
    },
    {
        arg = {'dps'},
        header = i18n.item_table.dps,
        fields = h.tbl.range_fields('weapons.dps'),
        display = h.tbl.display.factory.range{field='weapons.dps'},
        order = 8107,
    },
    {
        arg = 'flask_life',
        header = i18n.item_table.flask_life,
        fields = h.tbl.range_fields('flasks.life'),
        display = h.tbl.display.factory.range{field='flasks.life'},
        order = 9000,
    },
    {
        arg = 'flask_mana',
        header = i18n.item_table.flask_mana,
        fields = h.tbl.range_fields('flasks.mana'),
        display = h.tbl.display.factory.range{field='flasks.mana'},
        order = 9001,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_duration,
        fields = h.tbl.range_fields('flasks.duration'),
        display = h.tbl.display.factory.range{field='flasks.duration'},
        order = 9002,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_charges_per_use,
        fields = h.tbl.range_fields('flasks.charges_per_use'),
        display = h.tbl.display.factory.range{field='flasks.charges_per_use'},
        order = 9003,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_maximum_charges,
        fields = h.tbl.range_fields('flasks.charges_max'),
        display = h.tbl.display.factory.range{field='flasks.charges_max'},
        order = 9004,
    },
    {
        arg = 'item_limit',
        header = i18n.item_table.item_limit,
        fields = {'jewels.item_limit'},
        display = h.tbl.display.factory.value{},
        order = 10000,
    },
    {
        arg = 'jewel_radius',
        header = i18n.item_table.jewel_radius,
        fields = {'jewels.radius_html'},
        display = function (tr, data)
            tr
                :tag('td')
                    :wikitext(data['jewels.radius_html'])
        end,
        order = 10001,
    },
    {
        arg = 'map_tier',
        header = i18n.item_table.map_tier,
        fields = {'maps.tier'},
        display = h.tbl.display.factory.value{},
        order = 11000,
    },
    {
        arg = 'map_level',
        header = i18n.item_table.map_level,
        fields = {'maps.area_level'},
        display = h.tbl.display.factory.value{},
        order = 11010,
    },
    {
        arg = 'map_guild_character',
        header = i18n.item_table.map_guild_character,
        fields = {'maps.guild_character'},
        display = h.tbl.display.factory.value{colour='value'},
        order = 11020,
        sort_type = 'text',
    },
    {
        arg = 'buff',
        header = i18n.item_table.buff_effects,
        fields = {'item_buffs.stat_text'},
        display = h.tbl.display.factory.value{colour='mod'},
        order = 12000,
        sort_type = 'text',
    },
    {
        arg = 'stat',
        header = i18n.item_table.stats,
        fields = {'items.stat_text'},
        display = h.tbl.display.factory.value{colour='mod'},
        order = 12001,
        sort_type = 'text',
    },
    {
        arg = 'description',
        header = i18n.item_table.effects,
        fields = {'items.description'},
        display = h.tbl.display.factory.value{colour='mod'},
        order = 12002,
        sort_type = 'text',
    },
    {
        arg = 'flavour_text',
        header = i18n.item_table.flavour_text,
        fields = {'items.flavour_text'},
        display = h.tbl.display.factory.value{colour='flavour'},
        order = 12003,
        sort_type = 'text',
    },
    {
        arg = 'help_text',
        header = i18n.item_table.help_text,
        fields = {'items.help_text'},
        display = h.tbl.display.factory.value{colour='help'},
        order = 12005,
        sort_type = 'text',
    },
    {
        arg = {'prophecy', 'objective'},
        header = i18n.item_table.objective,
        fields = {'prophecies.objective'},
        display = h.tbl.display.factory.value{},
        order = 13002,
    },
        {
        arg = {'prophecy', 'reward'},
        header = i18n.item_table.reward,
        fields = {'prophecies.reward'},
        display = h.tbl.display.factory.value{},
        order = 13001,
    },
    {
        arg = {'prophecy', 'seal_cost'},
        header = i18n.item_table.seal_cost,
        fields = {'prophecies.seal_cost'},
        display = h.tbl.display.factory.value{colour='currency'},
        order = 13002,
    },
    {
        arg = {'prediction_text'},
        header = i18n.item_table.prediction_text,
        fields = {'prophecies.prediction_text'},
        display = h.tbl.display.factory.value{colour='value'},
        order = 12004,
        sort_type = 'text',
    },
    {
        arg = 'buff_icon',
        header = i18n.item_table.buff_icon,
        fields = {'item_buffs.icon'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='[[%s]]',
            },
        }},
        order = 14000,
        sort_type = 'text',
    },
    {
        arg = {'drop', 'drop_leagues'},
        header = i18n.item_table.drop_leagues,
        fields = {'items.drop_leagues'},
        display = function (tr, data)
            tr
                :tag('td')
                    :wikitext(table.concat(m_util.string.split(data['items.drop_leagues'], ','), '<br>'))
        end,
        order = 15000,
    },
    {
        arg = {'drop', 'drop_areas'},
        header = i18n.item_table.drop_areas,
        fields = {'items.drop_areas_html'},
        display = h.tbl.display.factory.value{},
        order = 15001,
    },
    {
        arg = {'drop', 'drop_text'},
        header = i18n.item_table.drop_text,
        fields = {'items.drop_text'},
        display = h.tbl.display.factory.value{},
        order = 15002,
    },
}

data_map.skill_gem_new = {
    {
        arg = 'icon',
        header = i18n.item_table.support_gem_letter,
        fields = {'skill_gems.support_gem_letter_html'},
        display = h.tbl.display.factory.value{},
        order = 1000,
        sort_type = 'text',
    },
    {
        arg = 'skill_icon',
        header = i18n.item_table.skill_icon,
        fields = {'skill.skill_icon'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='[[%s]]',
            },
        }},
        order = 1001,
        sort_type = 'text',
    },
    {
        arg = 'description',
        header = i18n.item_table.description,
        fields = {'skill.description'},
        display = h.tbl.display.factory.value{},
        order = 2000,
        sort_type = 'text',
    },
    {
        arg = 'level',
        header = m_game.level_requirement.icon,
        fields = h.tbl.range_fields('items.required_level'),
        display = h.tbl.display.factory.range{field='items.required_level'},
        order = 3004,
    },
    {
        arg = 'crit',
        header = i18n.item_table.skill_critical_strike_chance,
        fields = {'skill_levels.critical_strike_chance'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 4000,
    },
    {
        arg = 'cast_time',
        header = i18n.item_table.cast_time,
        fields = {'skill_levels.cast_time'},
        display = h.tbl.display.factory.value{},
        order = 4001,
    },
    {
        arg = 'dmgeff',
        header = i18n.item_table.damage_effectiveness,
        fields = {'skill_levels.damage_effectiveness'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 4002,
    },
    {
        arg = 'mcm',
        header = i18n.item_table.mana_cost_multiplier,
        fields = {'skill_levels.mana_multiplier'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 5000,
    },
    {
        arg = 'mana',
        header = i18n.item_table.mana_cost,
        fields = {'skill_levels.mana_cost', 'skill.has_percentage_mana_cost', 'skill.has_reservation_mana_cost'},
        display = function (tr, data)
            local appendix = ''
            if m_util.cast.boolean(data['skill.has_percentage_mana_cost']) then
                appendix = appendix .. '%'
            end
            if m_util.cast.boolean(data['skill.has_reservation_mana_cost']) then
                appendix = appendix .. ' ' .. i18n.item_table.reserves_mana_suffix
            end
            
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill_levels.mana_cost'])
                    :wikitext(string.format('%d', data['skill_levels.mana_cost']) .. appendix)
        end,
        order = 5001,
    },
    {
        arg = 'vaal',
        header = i18n.item_table.vaal_souls_requirement,
        fields = {'skill_levels.vaal_souls_requirement'},
        display = function (tr, data)
            local souls = tonumber(data['skill_levels.vaal_souls_requirement'])
            tr
                :tag('td')
                    :attr('data-sort-value', souls)
                    :wikitext(string.format('%d / %d / %d', souls, souls*1.5, souls*2))
        end,
        order = 6000,
    },
    {
        arg = 'vaal',
        header = i18n.item_table.stored_uses,
        fields = {'skill_levels.vaal_stored_uses'},
        display = h.tbl.display.factory.value{},
        order = 6001,
    },
    {
        arg = 'radius',
        header = i18n.item_table.primary_radius,
        fields = {'skill.radius', 'skill.radius_description'},
        options = {[2] = {optional = true}},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill.radius'])
                    :wikitext(core.factory.descriptor_value{tbl=data, key='skill.radius_description'}(nil, nil, data['skill.radius']))
        end,
        order = 7000,
    },
    {
        arg = 'radius',
        header = i18n.item_table.secondary_radius,
        fields = {'skill.radius_secondary', 'skill.radius_secondary_description'},
        options = {[2] = {optional = true}},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill.radius_secondary'])
                    :wikitext(core.factory.descriptor_value{tbl=data, key='skill.radius_secondary_description'}(nil, nil, data['skill.radius_secondary']))
        end,
        order = 7001,
    },
    {
        arg = 'radius',
        header = i18n.item_table.tertiary_radius,
        fields = {'skill.radius_tertiary', 'skill.radius_tertiary_description'},
        options = {[2] = {optional = true}},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill.radius_tertiary'])
                   :wikitext(core.factory.descriptor_value{tbl=data, key='skill.radius_tertiary_description'}(nil, nil, data['skill.radius_tertiary']))
        end,
        order = 7002,
    },
}

for i, attr in ipairs(m_game.constants.attributes) do
    table.insert(data_map.generic_item, 7, {
        arg = attr.short_lower,
        header = attr.icon,
        fields = h.tbl.range_fields(string.format('items.required_%s', attr.long_lower)),
        display = h.tbl.display.factory.range{field=string.format('items.required_%s', attr.long_lower)},
        order = 5000+i,
    })
    table.insert(data_map.skill_gem_new, 1, {
        arg = attr.short_lower,
        header = attr.icon,
        fields = {string.format('skill_gems.%s_percent', attr.long_lower)},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data[string.format('skill_gems.%s_percent', attr.long_lower)])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3000+i,
    })
end


-- ----------------------------------------------------------------------------
-- Invoke callables
-- ----------------------------------------------------------------------------

local p = {}

-- 
-- Template:Item table
-- 

function p.item_table(frame)
    -- args
    local tpl_args = getArgs(frame, {
            parentFirst = true
        })
    frame = m_util.misc.get_frame(frame)
    
    if string.find(tpl_args.q_where, '%[%[') ~= nil then
        error('SMW leftover in where clause')
    end
    
    local modes = {
        skill = {
            data = data_map.skill_gem_new,
            header = i18n.item_table.skill_gem,
        },
        item = {
            data = data_map.generic_item,
            header = i18n.item_table.item,
        },
    }
    
    if tpl_args.mode == nil then
        tpl_args.mode = 'item'
    end
    
    if modes[tpl_args.mode] == nil then
        error(i18n.errors.invalid_item_table_mode)
    end
    
    local row_infos = {}
    for _, row_info in ipairs(modes[tpl_args.mode].data) do
        local enabled = false
        if row_info.arg == nil then
            enabled = true
        elseif type(row_info.arg) == 'string' and m_util.cast.boolean(tpl_args[row_info.arg]) then
            enabled = true
        elseif type(row_info.arg) == 'table' then 
            for _, argument in ipairs(row_info.arg) do
                if m_util.cast.boolean(tpl_args[argument]) then
                    enabled = true
                    break
                end
            end
        end
        
        if enabled then
            row_info.options = row_info.options or {}
            row_infos[#row_infos+1] = row_info
        end
    end
    
    -- Parse stat arguments
    local stat_columns = {}
    local query_stats = {}
    local stat_results = {}
    local i = 0
    repeat
        i = i + 1
        
        local prefix = string.format('stat_column%s_', i)
        local col_info = {
            header = tpl_args[prefix .. 'header'] or tostring(i),
            format = tpl_args[prefix .. 'format'],
            stat_format = tpl_args[prefix .. 'stat_format'] or 'separate',
            order = tonumber(tpl_args[prefix .. 'order']) or (10000000 + i),
            stats = {},
            options = {},
        }
        
        local j = 0
        repeat
            j = j +1
        
            local stat_info = {
                id = tpl_args[string.format('%sstat%s_id', prefix, j)],
            }
            
            if stat_info.id then
                col_info.stats[#col_info.stats+1] = stat_info
                query_stats[stat_info.id] = {} 
            else
                -- Stop iteration entirely if this was the first index but no stat was supplied. We assume that we stop in this case.
                if j == 1 then
                    i = nil
                end
                -- stop iteration
                j = nil
            end
        until j == nil
        
        -- Don't add this column if no stats were provided. 
        if #col_info.stats > 0 then
            stat_columns[#stat_columns+1] = col_info
        end
    until i == nil
    
    for _, col_info in ipairs(stat_columns) do
        local row_info = {
            --arg
            header = col_info.header,
            fields = {},
            display = function(tr, data, properties)
                if col_info.stat_format == 'separate' then
                    local stat_texts = {}
                    local num_stats = 0
                    local vmax = 0
                    for _, stat_info in ipairs(col_info.stats) do
                        num_stats = num_stats + 1
                        -- stat results from outside body
                        local stat = (stat_results[data['items._pageName']] or {})[stat_info.id]
                        if stat ~= nil then
                            stat_texts[#stat_texts+1] = h.format_value(tpl_args, frame, stat, {no_color=true})
                            vmax = vmax + stat.max
                        end
                    end
                    
                    if num_stats ~= #stat_texts then
                        tr:wikitext(m_util.html.td.na())
                    else
                        local text
                        if col_info.format then
                            text = string.format(col_info.format, unpack(stat_texts))
                        else
                            text = table.concat(stat_texts, ', ')
                        end
                    
                        tr:tag('td')
                            :attr('data-sort-value', vmax)
                            :attr('class', 'tc -mod')
                            :wikitext(text)
                    end
                 elseif col_info.stat_format == 'add' then
                    local total_stat = {
                        min = 0,
                        max = 0,
                        avg = 0,
                    }
                    for _, stat_info in ipairs(col_info.stats) do
                        local stat = (stat_results[data['items._pageName']] or {})[stat_info.id]
                        if stat ~= nil then
                            for k, v in pairs(total_stat) do
                                total_stat[k] = v + stat[k]
                            end
                        end
                    end
                    
                    if col_info.format == nil then
                        col_info.format = '%s'
                    end
                    
                    tr:tag('td')
                        :attr('data-sort-value', total_stat.max)
                        :attr('class', 'tc -mod')
                        :wikitext(string.format(col_info.format, h.format_value(tpl_args, frame, total_stat, {no_color=true})))
                 else
                    error(string.format(i18n.errors.generic_argument_parameter, 'stat_format', col_info.stat_format))
                 end
            end,
            order = col_info.order,
        }
        table.insert(row_infos, row_info)
    end
    
    -- sort the rows
    table.sort(row_infos, function (a, b)
        return (a.order or 0) < (b.order or 0)
    end)
    
    -- Parse query arguments
    local tables_assoc = {items=true}
    local fields = {
        'items._pageID',
        'items._pageName',
        'items.name',
        'items.inventory_icon',
        'items.html',
        'items.size_x',
        'items.size_y',
    }
    
    --
    local prepend = {
        q_groupBy=true,
        q_tables=true
    }
    
    local query = {}
    for key, value in pairs(tpl_args) do 
        if string.sub(key, 0, 2) == 'q_' then
            if prepend[key] then
                value = ',' .. value
            end
            
            query[string.sub(key, 3)] = value
        end
    end
    
    for _, rowinfo in ipairs(row_infos) do
        if type(rowinfo.fields) == 'function' then
            rowinfo.fields = rowinfo.fields()
        end
        for index, field in ipairs(rowinfo.fields) do
            rowinfo.options[index] = rowinfo.options[index] or {}
            fields[#fields+1] = field
            tables_assoc[m_util.string.split(field, '%.')[1]] = true
        end
    end
    
    -- reformat the fields & tables so they can be retrieved correctly
    for index, field in ipairs(fields) do
        fields[index] = string.format('%s=%s', field, field)
    end
    
    local tables = {}
    for table_name,_ in pairs(tables_assoc) do
        tables[#tables+1] = table_name
    end
    
    -- take care of required joins according to the tables

    local joins = {}
    for index, table_name in ipairs(tables) do
        if table_name ~= 'items' then
            joins[#joins+1] = string.format('items._pageID=%s._pageID', table_name) 
        end
    end
    if #joins > 0 and query.join then
        query.join = table.concat(joins, ',') .. ',' .. query.join
    elseif #joins > 0 and not query.join then
        query.join = table.concat(joins, ',')
    elseif #joins == 0 and query.join then
        -- leave query.join as is
    end
    
    -- Cargo workaround: avoid duplicates using groupBy 
    query.groupBy = 'items._pageID' .. (query.groupBy or '')
    
    query.limit = tonumber(query.limit)
    if query.limit == nil then
        query.limit = c.query_default
    elseif query.limit > c.query_max then
        query.limit = c.query_max
    end
    
    local results = cargo.query(
        table.concat(tables,',') .. (query.tables or ''),
        table.concat(fields,','),
        query
    )
    
    if #results == 0 and tpl_args.default ~= nil then
        return tpl_args.default
    end
    
    if #results > 0 and #stat_columns > 0 then
        local pages = {}
        for _, row in ipairs(results) do
            pages[#pages+1] = string.format('item_stats._pageID="%s"', row['items._pageID'])
        end
        
        local query_stat_ids = {}
        for stat_id, _ in pairs(query_stats) do
            query_stat_ids[#query_stat_ids+1] = string.format('item_stats.id="%s"', stat_id)
        end
    
        if tpl_args.q_where then
            tpl_args.q_where = string.format(' AND (%s)', tpl_args.q_where)
        else
            tpl_args.q_where = ''
        end
        
        local temp = m_util.cargo.query(
            {'items', 'item_stats'},
            {'item_stats._pageName', 'item_stats.id', 'item_stats.min', 'item_stats.max', 'item_stats.avg'},
            {
                where=string.format('item_stats.is_implicit IS NULL AND (%s) AND (%s)', table.concat(query_stat_ids, ' OR '), table.concat(pages, ' OR ')),
                join='items._pageID=item_stats._pageID',
                -- Cargo workaround: avoid duplicates using groupBy 
                groupBy='items._pageID, item_stats.id',
                limit=5000,
            }
        )

        for _, row in ipairs(temp) do
            local stat = {
                min = tonumber(row['item_stats.min']),
                max = tonumber(row['item_stats.max']),
                avg = tonumber(row['item_stats.avg']),
            }
            
            if stat_results[row['item_stats._pageName']] == nil then
                stat_results[row['item_stats._pageName']] = {[row['item_stats.id']] = stat}
            else
                stat_results[row['item_stats._pageName']][row['item_stats.id']] = stat
            end
        end
        
        -- Cargo doesnt support offset yet
        if #temp == 5000 then
            --TODO: Cargo
            error('Stats > 5000')
        end
    end
    
    local tbl = mw.html.create('table')
    tbl:attr('class', 'wikitable sortable item-table')
    
    -- Header
    
    local tr = tbl:tag('tr')
    tr
        :tag('th')
            :wikitext(modes[tpl_args.mode].header)
            :done()
            
    for _, row_info in ipairs(row_infos) do
        tr
            :tag('th')
                :attr('data-sort-type', row_info.sort_type or 'number')
                :wikitext(row_info.header)
                :done()
    end
    
    for _, row in ipairs(results) do
        tr = tbl:tag('tr')
        
        local il_args = {
            skip_query=true,
            page=row['items._pageName'], 
            name=row['items.name'], 
            inventory_icon=row['items.inventory_icon'], 
            html=row['items.html'],
            width=row['items.size_x'],
            height=row['items.size_y'],
        }
        
        if tpl_args.large then
            il_args.large = tpl_args.large
        end
        
        tr
            :tag('td')
                :wikitext(f_item_link(il_args))
                :done()
                
        for _, rowinfo in ipairs(row_infos) do
            -- this has been cast from a function in an earlier step
            local display = true
            for index, field in ipairs(rowinfo.fields) do
                -- this will bet set to an empty value not nil confusingly
                if row[field] == '' then
                    if rowinfo.options[index].optional ~= true then
                        display = false
                        break
                    else
                        row[field] = nil
                    end
                end
            end
            if display then
                rowinfo.display(tr, row, rowinfo.fields)
            else
                tr:wikitext(m_util.html.td.na())
            end
        end
    end
    
    cats = {}
    if #results == query.limit then
        cats[#cats+1] = i18n.categories.query_limit
    end
    
    if #results == c.query_max then
        cats[#cats+1] = i18n.categories.query_hard_limit
    end
    
    return tostring(tbl) .. m_util.misc.add_category(cats, {ingore_blacklist=tpl_args.debug})
end

-- ----------------------------------------------------------------------------
-- Debug stuff
-- ----------------------------------------------------------------------------
p.debug = {}

function p.debug._tbl_data(tbl)
    keys = {}
    for _, data in ipairs(data_map.generic_item) do
        if type(data.arg) == 'string' then 
            keys[data.arg] = 1
        elseif type(data.arg) == 'table' then
            for _, arg in ipairs(data.arg) do
                keys[arg] = 1
            end
        end
    end
    
    local out = {}
    for key, _ in pairs(keys) do
        out[#out+1] = string.format("['%s'] = '1'", key)
    end
    
    return table.concat(out, ', ')
end

function p.debug.generic_item_all()
    return p.debug._tbl_data(data_map.generic_item)
end

function p.debug.skill_gem_all()
    return p.debug._tbl_data(data_map.skill_gem_new)
end

-- ----------------------------------------------------------------------------
-- Return
-- ----------------------------------------------------------------------------

return p