Module:Item table: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>OmegaK2
(Moved p.item_table (Template:Item table) from Module:Item2 to this module)
 
No edit summary
 
(121 intermediate revisions by 9 users not shown)
Line 1: Line 1:
-- Item table
-------------------------------------------------------------------------------
--
--
--
--                            Module:Item table
--
-- This module implements Template:Item table and other templates that query
-- and display tables or lists of items.
-------------------------------------------------------------------------------


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


local cargo = mw.ext.cargo
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Item table')


-- ----------------------------------------------------------------------------
local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or mw.loadData('Module:Game')
-- Globals
 
-- ----------------------------------------------------------------------------
-- Lazy loading
local m_item_util -- require('Module:Item util')
local f_item_link -- require('Module:Item link').item_link
local f_skill_link -- require('Module:Skill link').skill_link
 
-- 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:Item table/config/sandbox') or mw.loadData('Module:Item table/config')


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


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Strings
-- Helper functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- 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 = {
local h = {}
    categories = {
h.string = {}
        -- maintenance cats
        query_limit = 'Item tables hitting query limit',
        query_hard_limit = 'Item tables hitting hard query limit',
    },


    -- Used by the item table
function h.string.format(str, vars)
    item_table = {
    --[[
        item = 'Item',
    Allow string replacement using named arguments.
        skill_gem = 'Skill gem',
   
 
    TODO:
        physical_dps = m_util.html.abbr('pDPS', 'physical damage per second'),
    * Support %d ?
        fire_dps = m_util.html.abbr('Fire DPS', 'fire damage per second'),
    * Support 0.2f ?
        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 = {
     Parameters
        generic_argument_parameter = 'Unrecognized %s parameter "%s"',
     ----------
        invalid_item_table_mode = 'Invalid mode for item table',
    str : String to replace.
     },
    vars : Table of arguments.
}
 
-- ----------------------------------------------------------------------------
-- Helper & utility functions
-- ----------------------------------------------------------------------------


local h = {}
    Examples
    --------
    = h.string.format('{foo} is {bar}.', {foo='Dinner', bar='nice'})


function h.format_value(tpl_args, frame, value, options)
     References
     -- value: table
     ----------
     -- min:
     http://lua-users.org/wiki/StringInterpolation
    -- 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 not vars then
         if options.color then
         vars = str
            value.color = options.color
         str = vars[1]
        elseif value.base ~= value.min or value.base ~= value.max then
            value.color = 'mod'
         else
            value.color = 'value'
        end
     end
     end
      
      
     if options.func ~= nil then
     return (string.gsub(str, "({([^}]+)})",
         value.min = options.func(tpl_args, frame, value.min)
         function(whole, i)
         value.max = options.func(tpl_args, frame, value.max)
          return vars[i] or whole
        end))
end
 
-- Lazy loading for Module:Item link
function h.item_link(args)
    if not f_item_link then
         f_item_link = require('Module:Item link').item_link
     end
     end
      
     return f_item_link(args)
     if options.fmt == nil then
end
         options.fmt = '%s'
 
    elseif type(options.fmt) == 'function' then
-- Lazy loading for Module:Skill link
        options.fmt = options.fmt(tpl_args, frame)
function h.skill_link(args)
     if not f_skill_link then
         f_skill_link = require('Module:Skill link').skill_link
     end
     end
      
     return f_skill_link(args)
    if value.min == value.max then
end
        value.out = string.format(options.fmt, value.min)
 
    else
function h.na_or_val(tr, value, func)
        value.out = string.format(string.format(options.fmt_range or i18n.range, options.fmt, options.fmt), value.min, value.max)
     if value == nil or value == '' then
    end
         tr:wikitext(m_util.html.td.na())
   
     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
     else
        text = nil
         local raw_value = value
    end
         if func ~= nil then
   
             value = func(value)
    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
         end
          
         tr
        return string.format(text, value.out), return_color
            :tag('td')
                :attr('data-sort-value', raw_value)
                :wikitext(value)
                :done()
     end
     end
   
    -- If we didn't return before, return here
    return value.out, return_color
end
end


h.tbl = {}
h.tbl = {}


function h.tbl.range_fields(field)
function h.tbl.range_fields(args)
    local suffixes = {'maximum', 'text', 'colour'}
    if args.full then
        suffixes[#suffixes+1] = 'minimum'
    end
       
     return function()
     return function()
         local fields = {}
         local fields = {}
         for _, partial_field in ipairs({'maximum', 'text', 'colour'}) do
         local inner = function (field)
            fields[#fields+1] = string.format('%s_range_%s', field, partial_field)
            for _, partial_field in ipairs(suffixes) do
                fields[#fields+1] = string.format('%s_range_%s', field, partial_field)
            end
        end
       
        if type(args.field) == 'table' then
            for _, field in ipairs(args.field) do
                inner(field)
            end
        else
            inner(args.field)
         end
         end
         return fields
         return fields
Line 232: Line 153:
     args.options = args.options or {}
     args.options = args.options or {}


     return function(tr, data, properties)
     return function(tr, data, fields, data2)
         values = {}
         local values = {}
          
         local fmt_values = {}
         for index, prop in ipairs(properties) do
        local sdata = data2.skill_levels[data['items._pageName']]
             local value = data[prop]
 
             if args.options[index] and args.options[index].fmt then
         for index, field in ipairs(fields) do
                 value = string.format(args.options[index].fmt, value)
             local value = {
                min=data[field],
                max=data[field],
                base=data[field],
            }
             if sdata then --For skill data
                -- Use the fixed value, or the first-level value.
                value.min = value.min or sdata['0'][field] or sdata['1'][field]
                -- Fall back to the fixed value, and then the max level value.
                value.max = value.max or sdata['0'][field] or sdata[data['skill.max_level']][field]
            end
            if value.min then
                 values[#values+1] = value.max
                local opts = args.options[index] or {}
                -- global colour is set, no overrides
                if args.colour ~= nil then
                    opts.no_color = true
                end
                fmt_values[#fmt_values+1] = m_util.html.format_value(nil, value, opts)
             end
             end
            values[#values+1] = value
         end
         end
          
 
          
         if #values == 0 then
        local td = tr:tag('td')
            tr:wikitext(m_util.html.td.na())
        td:attr('data-sort-value', table.concat(values, ', '))
         else
        td:wikitext(table.concat(values, ', '))
            local td = tr:tag('td')
        if args.colour then
            td:attr('data-sort-value', table.concat(values, ', '))
            td:attr('class', 'tc -' .. args.colour)
            td:wikitext(table.concat(fmt_values, ', '))
            if args.colour then
                td:attr('class', 'tc -' .. args.colour)
            end
         end
         end
     end
     end
Line 263: Line 204:
                 :wikitext(data[string.format('%s_range_text', args.field)])
                 :wikitext(data[string.format('%s_range_text', args.field)])
                 :done()
                 :done()
    end
end
function h.tbl.display.factory.range_composite(args)
    -- division by default
    if args.func == nil then
        args.func = function (a, b)
            if b == 0 then
                return 'fail'
            end
            return a / b
        end
    end
   
    return function(tr, data, fields)
        local field = {}
        for i=1, 2 do
            local fieldn = args['field' .. i]
            field[i] = {
                min = tonumber(data[string.format('%s_range_minimum', fieldn)]) or 0,
                max = tonumber(data[string.format('%s_range_maximum', fieldn)]) or 0,
                color = data[string.format('%s_range_colour', fieldn)] or 'default',
            }
        end
       
        field.min = args.func(field[1].min, field[2].min)
        field.max = args.func(field[1].max, field[2].max)
       
        if field.min == 'fail' or field.max == 'fail' then
            field.text = ''
            field.color = 'default'
        else
            for i=1, 2 do
                if field[i].color ~= 'default' then
                    field.color = field[i].color
                    break
                end
            end
            if field.min == field.max then
                field.text = string.format('%.2f', field.min)
            else
                field.text = string.format('(%.2f-%.2f)', field.min, field.max)
            end
        end
   
        tr
            :tag('td')
                :attr('data-sort-value', field.max)
                :attr('class', 'tc -' .. field.color)
                :wikitext(field.text)
                :done()
    end
end
function h.tbl.display.factory.descriptor_value(args)
    -- Arguments:
    --  key
    --  tbl
    args = args or {}
    return function (value)
        if args.tbl[args.key] then
            value = m_util.html.abbr(value, args.tbl[args.key])
        end
        return value
    end
end
function h.tbl.display.factory.atlas_tier(args)
    args = args or {}
    return function (tr, data)
        for i=0,4 do
            local t = tonumber(data['atlas_maps.map_tier' .. i])
            if t == 0 then
                tr
                    :tag('td')
                        :attr('table-sort-value', 0)
                        :attr('class', 'table-cell-xmark')
                        :wikitext('✗')
            else
                if args.is_level then
                    t = t + 67
                end
                tr
                    :tag('td')
                        :attr('class', 'tc -value')
                        :wikitext(t)
                        :done()
            end
        end
     end
     end
end
end
Line 299: Line 330:
         order = 1001,
         order = 1001,
         sort_type = 'text',
         sort_type = 'text',
    },
    {
        arg = 'rarity',
        header = i18n.item_table.rarity,
        fields = {'items.rarity'},
        display = h.tbl.display.factory.value{},
        order = 1002,
    },
    {
        arg = 'rarity_id',
        header = i18n.item_table.rarity_id,
        fields = {'items.rarity_id'},
        display = h.tbl.display.factory.value{},
        order = 1003,
    },
    {
        arg = 'metadata_id',
        header = i18n.item_table.metadata_id,
        fields = {'items.metadata_id'},
        display = h.tbl.display.factory.value{},
        order = 1004,
     },
     },
     {
     {
Line 305: Line 357:
         fields = {'essences.level'},
         fields = {'essences.level'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 2000,
         order = 1100,
     },
     },
     {
     {
Line 312: Line 364:
         fields = {'items.drop_level'},
         fields = {'items.drop_level'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 3000,
         order = 1200,
    },
    {
        arg = {'drop_level_maximum'},
        header = i18n.item_table.drop_level_maximum,
        fields = {'items.drop_level_maximum'},
        display = h.tbl.display.factory.value{},
        order = 1201,
     },
     },
     {
     {
Line 319: Line 378:
         fields = {'stackables.stack_size'},
         fields = {'stackables.stack_size'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 4000,
         order = 1300,
     },
     },
     {
     {
Line 326: Line 385:
         fields = {'stackables.stack_size_currency_tab'},
         fields = {'stackables.stack_size_currency_tab'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 4001,
         order = 1301,
     },
     },
    -- Requirements
     {
     {
         arg = 'level',
         arg = 'level',
         header = m_game.level_requirement.icon,
         header = m_game.level_requirement.icon,
         fields = h.tbl.range_fields('items.required_level'),
         fields = h.tbl.range_fields{field='items.required_level'},
         display = h.tbl.display.factory.range{field='items.required_level'},
         display = h.tbl.display.factory.range{field='items.required_level'},
         order = 5000,
         order = 1400,
    },
    {
        arg = 'str',
        header = '[[File:StrengthIcon small.png|link=|alt=Required strength]]',
        fields = h.tbl.range_fields{field='items.required_strength'},
        display = h.tbl.display.factory.range{field='items.required_strength'},
        order = 1401,
    },
    {
        arg = 'dex',
        header = '[[File:DexterityIcon small.png|link=|alt=Required dexterity]]',
        fields = h.tbl.range_fields{field='items.required_dexterity'},
        display = h.tbl.display.factory.range{field='items.required_dexterity'},
        order = 1402,
    },
    {
        arg = 'int',
        header = '[[File:IntelligenceIcon small.png|link=|alt=Required intelligence]]',
        fields = h.tbl.range_fields{field='items.required_intelligence'},
        display = h.tbl.display.factory.range{field='items.required_intelligence'},
        order = 1403,
     },
     },
    -- Armour 15xx
     {
     {
         arg = 'ar',
         arg = 'ar',
         header = i18n.item_table.armour,
         header = i18n.item_table.armour,
         fields = h.tbl.range_fields('armours.armour'),
         fields = h.tbl.range_fields{field='armours.armour'},
         display = h.tbl.display.factory.range{field='armours.armour'},
         display = h.tbl.display.factory.range{field='armours.armour'},
         order = 6000,
         order = 1500,
     },
     },
     {
     {
         arg = 'ev',
         arg = 'ev',
         header =i18n.item_table.evasion,
         header =i18n.item_table.evasion,
         fields = h.tbl.range_fields('armours.evasion'),
         fields = h.tbl.range_fields{field='armours.evasion'},
         display = h.tbl.display.factory.range{field='armours.evasion'},
         display = h.tbl.display.factory.range{field='armours.evasion'},
         order = 6001,
         order = 1501,
     },
     },
     {
     {
         arg = 'es',
         arg = 'es',
         header = i18n.item_table.energy_shield,
         header = i18n.item_table.energy_shield,
         fields = h.tbl.range_fields('armours.energy_shield'),
         fields = h.tbl.range_fields{field='armours.energy_shield'},
         display = h.tbl.display.factory.range{field='armours.energy_shield'},
         display = h.tbl.display.factory.range{field='armours.energy_shield'},
         order = 6002,
         order = 1502,
    },
    {
        arg = 'wd',
        header = i18n.item_table.ward,
        fields = h.tbl.range_fields{field='armours.ward'},
        display = h.tbl.display.factory.range{field='armours.ward'},
        order = 1503,
     },
     },
     {
     {
         arg = 'block',
         arg = 'block',
         header = i18n.item_table.block,
         header = i18n.item_table.block,
         fields = h.tbl.range_fields('shields.block'),
         fields = h.tbl.range_fields{field='shields.block'},
         display = h.tbl.display.factory.range{field='shields.block'},
         display = h.tbl.display.factory.range{field='shields.block'},
         order = 6003,
         order = 1504,
     },
     },
     --[[{
     --[[{
Line 376: Line 465:
         display = h.tbl.display.factory.range{field='maximum physical damage'},
         display = h.tbl.display.factory.range{field='maximum physical damage'},
         order = 7001,
         order = 7001,
       
 
     },]]--
     },]]--
    -- Weapons 16xx
     {
     {
         arg = {'weapon', 'damage'},
         arg = {'weapon', 'damage'},
Line 388: Line 478:
                     :wikitext(data['weapons.damage_html'])
                     :wikitext(data['weapons.damage_html'])
         end,
         end,
         order = 8000,
         order = 1600,
     },
     },
     {
     {
         arg = {'weapon', 'aps'},
         arg = {'weapon', 'aps'},
         header = i18n.item_table.attacks_per_second,
         header = i18n.item_table.attacks_per_second,
         fields = h.tbl.range_fields('weapons.attack_speed'),
         fields = h.tbl.range_fields{field='weapons.attack_speed'},
         display = h.tbl.display.factory.range{field='weapons.attack_speed'},
         display = h.tbl.display.factory.range{field='weapons.attack_speed'},
         order = 8001,
         order = 1601,
     },
     },
     {
     {
         arg = {'weapon', 'crit'},
         arg = {'weapon', 'crit'},
         header = i18n.item_table.local_critical_strike_chance,
         header = i18n.item_table.local_critical_strike_chance,
         fields = h.tbl.range_fields('weapons.critical_strike_chance'),
         fields = h.tbl.range_fields{field='weapons.critical_strike_chance'},
         display = h.tbl.display.factory.range{field='weapons.critical_strike_chance'},
         display = h.tbl.display.factory.range{field='weapons.critical_strike_chance'},
         order = 8002,
         order = 1602,
     },
     },
     {
     {
         arg = {'physical_dps'},
         arg = {'physical_dps'},
         header = i18n.item_table.physical_dps,
         header = i18n.item_table.physical_dps,
         fields = h.tbl.range_fields('weapons.physical_dps'),
         fields = h.tbl.range_fields{field='weapons.physical_dps'},
         display = h.tbl.display.factory.range{field='weapons.physical_dps'},
         display = h.tbl.display.factory.range{field='weapons.physical_dps'},
         order = 8100,
         order = 1603,
     },
     },
     {
     {
         arg = {'lightning_dps'},
         arg = {'lightning_dps'},
         header = i18n.item_table.lightning_dps,
         header = i18n.item_table.lightning_dps,
         fields = h.tbl.range_fields('weapons.lightning_dps'),
         fields = h.tbl.range_fields{field='weapons.lightning_dps'},
         display = h.tbl.display.factory.range{field='weapons.lightning_dps'},
         display = h.tbl.display.factory.range{field='weapons.lightning_dps'},
         order = 8101,
         order = 1604,
     },
     },
     {
     {
         arg = {'cold_dps'},
         arg = {'cold_dps'},
         header = i18n.item_table.cold_dps,
         header = i18n.item_table.cold_dps,
         fields = h.tbl.range_fields('weapons.cold_dps'),
         fields = h.tbl.range_fields{field='weapons.cold_dps'},
         display = h.tbl.display.factory.range{field='weapons.cold_dps'},
         display = h.tbl.display.factory.range{field='weapons.cold_dps'},
         order = 8102,
         order = 1605,
     },
     },
     {
     {
         arg = {'fire_dps'},
         arg = {'fire_dps'},
         header = i18n.item_table.fire_dps,
         header = i18n.item_table.fire_dps,
         fields = h.tbl.range_fields('weapons.fire_dps'),
         fields = h.tbl.range_fields{field='weapons.fire_dps'},
         display = h.tbl.display.factory.range{field='weapons.fire_dps'},
         display = h.tbl.display.factory.range{field='weapons.fire_dps'},
         order = 8103,
         order = 1606,
     },
     },
     {
     {
         arg = {'chaos_dps'},
         arg = {'chaos_dps'},
         header = i18n.item_table.chaos_dps,
         header = i18n.item_table.chaos_dps,
         fields = h.tbl.range_fields('weapons.chaos_dps'),
         fields = h.tbl.range_fields{field='weapons.chaos_dps'},
         display = h.tbl.display.factory.range{field='weapons.chaos_dps'},
         display = h.tbl.display.factory.range{field='weapons.chaos_dps'},
         order = 8104,
         order = 1607,
     },
     },
     {
     {
         arg = {'elemental_dps'},
         arg = {'elemental_dps'},
         header = i18n.item_table.elemental_dps,
         header = i18n.item_table.elemental_dps,
         fields = h.tbl.range_fields('weapons.elemental_dps'),
         fields = h.tbl.range_fields{field='weapons.elemental_dps'},
         display = h.tbl.display.factory.range{field='weapons.elemental_dps'},
         display = h.tbl.display.factory.range{field='weapons.elemental_dps'},
         order = 8105,
         order = 1608,
     },
     },
     {
     {
         arg = {'poison_dps'},
         arg = {'poison_dps'},
         header = i18n.item_table.poison_dps,
         header = i18n.item_table.poison_dps,
         fields = h.tbl.range_fields('weapons.poison_dps'),
         fields = h.tbl.range_fields{field='weapons.poison_dps'},
         display = h.tbl.display.factory.range{field='weapons.poison_dps'},
         display = h.tbl.display.factory.range{field='weapons.poison_dps'},
         order = 8106,
         order = 1609,
     },
     },
     {
     {
         arg = {'dps'},
         arg = {'dps'},
         header = i18n.item_table.dps,
         header = i18n.item_table.dps,
         fields = h.tbl.range_fields('weapons.dps'),
         fields = h.tbl.range_fields{field='weapons.dps'},
         display = h.tbl.display.factory.range{field='weapons.dps'},
         display = h.tbl.display.factory.range{field='weapons.dps'},
         order = 8107,
         order = 1610,
     },
     },
    -- Flasks 17xx
     {
     {
         arg = 'flask_life',
         arg = 'flask_life',
         header = i18n.item_table.flask_life,
         header = i18n.item_table.flask_life,
         fields = h.tbl.range_fields('flasks.life'),
         fields = h.tbl.range_fields{field='flasks.life'},
         display = h.tbl.display.factory.range{field='flasks.life'},
         display = h.tbl.display.factory.range{field='flasks.life'},
         order = 9000,
         order = 1700,
    },
    {
        arg = 'flask_life_per_second',
        header = i18n.item_table.flask_life_per_second,
        fields = h.tbl.range_fields{field={'flasks.life', 'flasks.duration'}, full=true},
        display = h.tbl.display.factory.range_composite{field1='flasks.life', field2='flasks.duration'},
        order = 1701,
    },
    {
        arg = 'flask_life_per_charge',
        header = i18n.item_table.flask_life_per_charge,
        fields = h.tbl.range_fields{field={'flasks.life', 'flasks.charges_per_use'}, full=true},
        display = h.tbl.display.factory.range_composite{field1='flasks.life', field2='flasks.charges_per_use'},
        order = 1702,
     },
     },
     {
     {
         arg = 'flask_mana',
         arg = 'flask_mana',
         header = i18n.item_table.flask_mana,
         header = i18n.item_table.flask_mana,
         fields = h.tbl.range_fields('flasks.mana'),
         fields = h.tbl.range_fields{field='flasks.mana'},
         display = h.tbl.display.factory.range{field='flasks.mana'},
         display = h.tbl.display.factory.range{field='flasks.mana'},
         order = 9001,
         order = 1703,
    },
    {
        arg = 'flask_mana_per_second',
        header = i18n.item_table.flask_mana_per_second,
        fields = h.tbl.range_fields{field={'flasks.mana', 'flasks.duration'}, full=true},
        display = h.tbl.display.factory.range_composite{field1='flasks.mana', field2='flasks.duration'},
        order = 1704,
    },
    {
        arg = 'flask_mana_per_charge',
        header = i18n.item_table.flask_mana_per_charge,
        fields = h.tbl.range_fields{field={'flasks.mana', 'flasks.charges_per_use'}, full=true},
        display = h.tbl.display.factory.range_composite{field1='flasks.mana', field2='flasks.charges_per_use'},
        order = 1705,
     },
     },
     {
     {
         arg = 'flask',
         arg = 'flask',
         header = i18n.item_table.flask_duration,
         header = i18n.item_table.flask_duration,
         fields = h.tbl.range_fields('flasks.duration'),
         fields = h.tbl.range_fields{field='flasks.duration'},
         display = h.tbl.display.factory.range{field='flasks.duration'},
         display = h.tbl.display.factory.range{field='flasks.duration'},
         order = 9002,
         order = 1706,
     },
     },
     {
     {
         arg = 'flask',
         arg = 'flask',
         header = i18n.item_table.flask_charges_per_use,
         header = i18n.item_table.flask_charges_per_use,
         fields = h.tbl.range_fields('flasks.charges_per_use'),
         fields = h.tbl.range_fields{field='flasks.charges_per_use'},
         display = h.tbl.display.factory.range{field='flasks.charges_per_use'},
         display = h.tbl.display.factory.range{field='flasks.charges_per_use'},
         order = 9003,
         order = 1707,
     },
     },
     {
     {
         arg = 'flask',
         arg = 'flask',
         header = i18n.item_table.flask_maximum_charges,
         header = i18n.item_table.flask_maximum_charges,
         fields = h.tbl.range_fields('flasks.charges_max'),
         fields = h.tbl.range_fields{field='flasks.charges_max'},
         display = h.tbl.display.factory.range{field='flasks.charges_max'},
         display = h.tbl.display.factory.range{field='flasks.charges_max'},
         order = 9004,
         order = 1708,
     },
     },
    -- Jewels 18xx
     {
     {
         arg = 'item_limit',
         arg = 'jewel_limit',
         header = i18n.item_table.item_limit,
         header = i18n.item_table.jewel_limit,
         fields = {'jewels.item_limit'},
         fields = {'jewels.jewel_limit'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 10000,
         order = 1800,
     },
     },
     {
     {
Line 511: Line 631:
                     :wikitext(data['jewels.radius_html'])
                     :wikitext(data['jewels.radius_html'])
         end,
         end,
         order = 10001,
         order = 1801,
     },
     },
    -- Maps 19xx
     {
     {
         arg = 'map_tier',
         arg = 'map_tier',
Line 518: Line 639:
         fields = {'maps.tier'},
         fields = {'maps.tier'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 11000,
         order = 1900,
     },
     },
     {
     {
Line 525: Line 646:
         fields = {'maps.area_level'},
         fields = {'maps.area_level'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 11010,
         order = 1901,
     },
     },
     {
     {
Line 531: Line 652:
         header = i18n.item_table.map_guild_character,
         header = i18n.item_table.map_guild_character,
         fields = {'maps.guild_character'},
         fields = {'maps.guild_character'},
         display = h.tbl.display.factory.value{colour='value'},
        display = h.tbl.display.factory.value{},
         order = 11020,
        order = 1902,
        sort_type = 'text',
    },
    {
        arg = 'map_series',
        header = i18n.item_table.map_series,
        fields = {'maps.series'},
        display = h.tbl.display.factory.value{},
        order = 1903,
    },
    {
        arg = 'atlas_tier',
        header = i18n.item_table.atlas_tier,
        colspan = 5,
        fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
        display = h.tbl.display.factory.atlas_tier{is_level=false},
        order = 1904,
    },
    {
        arg = 'atlas_level',
        header = i18n.item_table.atlas_level,
        colspan = 5,
        fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
        display = h.tbl.display.factory.atlas_tier{is_level=true},
        order = 1905,
    },
    -- Hideout decorations 20xx
    {
        arg = {'doodad', 'variation_count'},
        header = i18n.item_table.variation_count,
        fields = {'hideout_doodads.variation_count'},
         display = h.tbl.display.factory.value{colour='mod'},
        order = 2000,
    },
    -- Corpse items 21xx
    {
        arg = {'corpse', 'monster_category'},
        header = i18n.item_table.monster_category,
        fields = {'corpse_items.monster_category', 'corpse_items.monster_category_html'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', m_game.constants.monster.categories[data['corpse_items.monster_category']].id)
                    :wikitext(data['corpse_items.monster_category_html'])
        end,
        order = 2100,
    },
    {
        arg = {'corpse', 'monster_abilities'},
        header = i18n.item_table.monster_abilities,
        fields = {'corpse_items.monster_abilities'},
        display = h.tbl.display.factory.value{colour='mod'},
         order = 2101,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
    -- Seed data 91xx,
    {
        arg = {'seed', 'seed_type'},
        header = i18n.item_table.seed_type,
        fields = {'harvest_seeds.type', 'harvest_seeds.type_id'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('table-sort-value', data['harvest_seeds.type'])
                    :attr('class', 'tc -' .. data['harvest_seeds.type_id'])
                    :wikitext(data['harvest_seeds.type'])
        end,
        order = 9100,
    },
    {
        arg = {'seed', 'seed_tier'},
        header = i18n.item_table.seed_tier,
        fields = {'harvest_seeds.tier'},
        display = h.tbl.display.factory.value{},
        order = 9101,
    },
    {
        arg = {'seed', 'seed_growth_cycles'},
        header = i18n.item_table.seed_growth_cycles,
        fields = {'harvest_seeds.growth_cycles'},
        display = h.tbl.display.factory.value{},
        order = 9102,
    },
    {
        arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_primal_lifeforce_percentage'},
        header = i18n.item_table.seed_consumed_primal_lifeforce_percentage,
        fields = {'harvest_seeds.consumed_primal_lifeforce_percentage'},
        display = h.tbl.display.factory.value{color='primal'},
        order = 9110,
    },
    {
        arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_vivid_lifeforce_percentage'},
        header = i18n.item_table.seed_consumed_vivid_lifeforce_percentage,
        fields = {'harvest_seeds.consumed_vivid_lifeforce_percentage'},
        display = h.tbl.display.factory.value{color='vivid'},
        order = 9110,
    },
    {
        arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_wild_lifeforce_percentage'},
        header = i18n.item_table.seed_consumed_wild_lifeforce_percentage,
        fields = {'harvest_seeds.consumed_wild_lifeforce_percentage'},
        display = h.tbl.display.factory.value{color='wild'},
        order = 9110,
    },
    {
        arg = {'seed', 'seed_required_nearby_seeds', 'seed_required_nearby_seed_amount'},
        header = i18n.item_table.seed_required_nearby_seed_amount,
        fields = {'harvest_seeds.required_nearby_seed_amount'},
        display = h.tbl.display.factory.value{},
        order = 9113,
    },
    {
        arg = {'seed', 'seed_required_nearby_seeds', 'seed_required_nearby_seed_tier'},
        header = i18n.item_table.seed_required_nearby_seed_tier,
        fields = {'harvest_seeds.required_nearby_seed_tier'},
        display = h.tbl.display.factory.value{},
        order = 9114,
    },
   
     {
     {
         arg = 'buff',
         arg = 'buff',
Line 557: Line 795:
         display = h.tbl.display.factory.value{colour='mod'},
         display = h.tbl.display.factory.value{colour='mod'},
         order = 12002,
         order = 12002,
        sort_type = 'text',
    },
    {
        arg = {'seed', 'seed_effect'},
        header = i18n.item_table.effects,
        fields = {'harvest_seeds.effect'},
        display = h.tbl.display.factory.value{colour='crafted'},
        order = 12003,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
Line 564: Line 810:
         fields = {'items.flavour_text'},
         fields = {'items.flavour_text'},
         display = h.tbl.display.factory.value{colour='flavour'},
         display = h.tbl.display.factory.value{colour='flavour'},
         order = 12003,
         order = 12006,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
Line 572: Line 818:
         fields = {'items.help_text'},
         fields = {'items.help_text'},
         display = h.tbl.display.factory.value{colour='help'},
         display = h.tbl.display.factory.value{colour='help'},
         order = 12005,
         order = 12008,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
Line 617: Line 863:
     },
     },
     {
     {
         arg = {'drop', 'drop_leagues'},
         arg = {'version', 'release_version'},
         header = i18n.item_table.drop_leagues,
         header = i18n.item_table.release_version,
         fields = {'items.drop_leagues'},
         fields = {'items.release_version'},
         display = function (tr, data)
         display = function(tr, data)
             tr
             tr
                 :tag('td')
                 :tag('td')
                     :wikitext(table.concat(m_util.string.split(data['items.drop_leagues'], ','), '<br>'))
                     :wikitext(
                        string.format(
                            i18n.item_table.version_link,
                            data['items.release_version'],
                            data['items.release_version']
                        )
                    )
         end,
         end,
         order = 15000,
         order = 15000,
    },
    {
        arg = {'version', 'removal_version'},
        header = i18n.item_table.removal_version,
        fields = {'items.removal_version'},
        display = function(tr, data)
            tr
                :tag('td')
                    :wikitext(
                        string.format(
                            i18n.item_table.version_link,
                            data['items.removal_version'],
                            data['items.removal_version']
                        )
                    )
        end,
        order = 15001,
    },
    {
        arg = {'drop', 'drop_enabled'},
        header = i18n.item_table.drop_enabled,
        fields = {'items.drop_enabled'},
        display = h.tbl.display.factory.value{},
        order = 15002,
     },
     },
     {
     {
Line 632: Line 908:
         fields = {'items.drop_areas_html'},
         fields = {'items.drop_areas_html'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 15001,
         order = 15004,
        sort_type = 'text',
    },
    {
        arg = {'drop', 'drop_monsters'},
        header = i18n.item_table.drop_monsters,
        fields = {'items.drop_monsters'},
        display = function(tr, data, na, results2)
            if results2['drop_monsters_query'] == nil then
                results2['drop_monsters_query'] = m_cargo.query(
                    {'items', 'monsters', 'main_pages'},
                    {
                        'items._pageName',
                        'monsters._pageName',
                        'monsters.name',
                        'main_pages._pageName',
                    },
                    {
                        join=[[
                            items.drop_monsters HOLDS monsters.metadata_id,
                            monsters.metadata_id=main_pages.id
                        ]],
                        where=string.format([[
                            items._pageID IN (%s)
                            AND monsters.metadata_id IS NOT NULL
                        ]],
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='items.drop_monsters',
                    }
                )
 
                results2['drop_monsters_query'] = m_cargo.map_results_to_id{
                    results=results2['drop_monsters_query'],
                    field='items._pageName',
                }
            end
            local results = results2['drop_monsters_query'][data['items._pageName']] or {}
            local tbl = {}
            for _,v in ipairs(results) do
                local page = v['main_pages._pageName'] or v['monsters._pageName'] or ''
                local name = v['monsters.name'] or v['items.drop_monsters'] or ''
                tbl[#tbl+1] = string.format('[[%s|%s]]', page, name)
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 15005,
        sort_type = 'text',
     },
     },
     {
     {
Line 639: Line 962:
         fields = {'items.drop_text'},
         fields = {'items.drop_text'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 15002,
         order = 15006,
        sort_type = 'text',
    },
    {
        arg = {'quest'},
        header = i18n.item_table.quest_rewards,
        fields = {'items._pageName'},
        display = function(tr, data, na, results2)
            if results2['quest_query'] == nil then
                results2['quest_query'] = m_cargo.query(
                    {'items', 'quest_rewards'},
                    {
                        'quest_rewards._pageName',
                        'quest_rewards.classes',
                        'quest_rewards.act',
                        'quest_rewards.quest'
                    },
                    {
                        join='items._pageName=quest_rewards._pageName',
                        where=string.format(
                            'items._pageID IN (%s) AND quest_rewards._pageName IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='quest_rewards.act, quest_rewards.quest',
                    }
                )
                results2['quest_query'] = m_cargo.map_results_to_id{
                    results=results2['quest_query'],
                    field='quest_rewards._pageName',
                }
            end
 
            local results = results2['quest_query'][data['items._pageName']] or {}
            local tbl = {}
            for _, v in ipairs(results) do
                local classes = table.concat(m_util.string.split(v['quest_rewards.classes'] or '', ',%s*'), ', ')
                if classes == '' or classes == nil then
                    classes = i18n.item_table.quest_rewards_any_classes
                end
 
                tbl[#tbl+1] = string.format(
                    i18n.item_table.quest_rewards_row_format,
                    v['quest_rewards.act'],
                    v['quest_rewards.quest'],
                    classes
                )
            end
 
            local value = table.concat(tbl, '<br>')
            if value == nil or value == '' then
                tr:wikitext(m_util.html.td.na())
            else
                tr
                    :tag('td')
                        :attr('style', 'text-align:left')
                        :wikitext(value)
            end
        end,
        order = 16001,
        sort_type = 'text',
    },
    {
        arg = {'vendor'},
        header = i18n.item_table.vendor_rewards,
        fields = {'items._pageName'},
        display = function(tr, data, na, results2)
            if results2['vendor_query'] == nil then
                results2['vendor_query'] = m_cargo.query(
                    {'items', 'vendor_rewards'},
                    {
                        'vendor_rewards._pageName',
                        'vendor_rewards.classes',
                        'vendor_rewards.act',
                        'vendor_rewards.npc',
                        'vendor_rewards.quest',
                    },
                    {
                        join='items._pageName=vendor_rewards._pageName',
                        where=string.format(
                            'items._pageID IN (%s) AND vendor_rewards._pageName IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='vendor_rewards.act, vendor_rewards.quest',
                    }
                )
                results2['vendor_query'] = m_cargo.map_results_to_id{
                    results=results2['vendor_query'],
                    field='vendor_rewards._pageName',
                }
            end
            local results = results2['vendor_query'][data['items._pageName']] or {}
 
            local tbl = {}
            for _, v in ipairs(results) do
                local classes = table.concat(m_util.string.split(v['vendor_rewards.classes'] or '', ',%s*'), ', ')
                if classes == '' or classes == nil then
                    classes = i18n.item_table.vendor_rewards_any_classes
                end
 
                tbl[#tbl+1] = string.format(
                    i18n.item_table.vendor_rewards_row_format,
                    v['vendor_rewards.act'],
                    v['vendor_rewards.quest'],
                    v['vendor_rewards.npc'],
                    classes
                )
            end
 
            local value = table.concat(tbl, '<br>')
            if value == nil or value == '' then
                tr:wikitext(m_util.html.td.na())
            else
                tr
                    :tag('td')
                        :attr('style', 'text-align:left')
                        :wikitext(value)
            end
        end,
        order = 17001,
        sort_type = 'text',
    },
    {
        arg = {'price', 'purchase_cost'},
        header = i18n.item_table.purchase_costs,
        fields = {'item_purchase_costs.name', 'item_purchase_costs.amount'},
        display = function (tr, data)
            -- Can purchase costs have multiple currencies and rows?
            -- Just switch to the same method as in sell_price then.
            local tbl = {}
            if data['item_purchase_costs.name'] ~= nil then
                tbl[#tbl+1] = string.format(
                        '%sx %s',
                        data['item_purchase_costs.amount'],
                        h.item_link{data['item_purchase_costs.name']}
                    )
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 18001,
        sort_type = 'text',
    },
    {
        arg = {'price', 'sell_price'},
        header = i18n.item_table.sell_price,
        fields = {'item_sell_prices.name', 'item_sell_prices.amount'},
        display = function(tr, data, na, results2)
            if results2['sell_price_query'] == nil then
                results2['sell_price_query'] = m_cargo.query(
                    {'items', 'item_sell_prices'},
                    {
                        'item_sell_prices.name',
                        'item_sell_prices.amount',
                        'item_sell_prices._pageID'
                    },
                    {
                        join='items._pageID=item_sell_prices._pageID',
                        where=string.format(
                            'items._pageID IN (%s) AND item_sell_prices._pageID IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='item_sell_prices.name',
                    }
                )
                results2['sell_price_query'] = m_cargo.map_results_to_id{
                    results=results2['sell_price_query'],
                    field='item_sell_prices._pageID',
                }
            end
            local results = results2['sell_price_query'][data['items._pageID']] or {}
 
            local tbl = {}
            for _,v in ipairs(results) do
                tbl[#tbl+1] = string.format(
                    '%sx %s',
                    v['item_sell_prices.amount'],
                    h.item_link{v['item_sell_prices.name']}
                )
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 18002,
        sort_type = 'text',
    },
    {
        arg = {'boss', 'boss_name'},
        header = i18n.item_table.boss_name,
        fields = {'maps.area_id'},
        display = function(tr, data, na, results2)
            if results2['boss_query'] == nil then
                results2['boss_query'] = m_cargo.query(
                    {'items', 'maps', 'areas', 'monsters', 'main_pages'},
                    {
                        'items._pageName',
                        'maps.area_id',
                        'areas.id',
                        'areas.boss_monster_ids',
                        'monsters._pageName',
                        'monsters.name',
                        'main_pages._pageName',
                    },
                    {
                        join=[[
                            items._pageID=maps._pageID,
                            maps.area_id=areas.id,
                            areas.boss_monster_ids HOLDS monsters.metadata_id,
                            monsters.metadata_id=main_pages.id
                        ]],
                        where=string.format([[
                            items._pageID IN (%s)
                            AND maps.area_id IS NOT NULL
                            AND areas.boss_monster_ids HOLDS LIKE "%%"
                        ]],
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='areas.boss_monster_ids',
                    }
                )
 
                results2['boss_query'] = m_cargo.map_results_to_id{
                    results=results2['boss_query'],
                    field='items._pageName',
                }
            end
            local results = results2['boss_query'][data['items._pageName']] or {}
            local tbl = {}
            for _,v in ipairs(results) do
                local page = v['main_pages._pageName'] or v['monsters._pageName'] or ''
                local name = v['monsters.name'] or v['areas.boss_monster_ids'] or ''
                tbl[#tbl+1] = string.format('[[%s|%s]]', page, name)
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 19001,
        sort_type = 'text',
    },
    {
        arg = {'boss', 'boss_number'},
        header = i18n.item_table.boss_number,
        fields = {'maps.area_id'},
        display = function(tr, data, na, results2)
            if results2['boss_query'] == nil then
                results2['boss_query'] = m_cargo.query(
                    {'items', 'maps', 'areas', 'monsters', 'main_pages'},
                    {
                        'items._pageName',
                        'maps.area_id',
                        'areas.id',
                        'areas.boss_monster_ids',
                        'monsters._pageName',
                        'monsters.name',
                        'main_pages._pageName',
                    },
                    {
                        join=[[
                            items._pageID=maps._pageID,
                            maps.area_id=areas.id,
                            areas.boss_monster_ids HOLDS monsters.metadata_id,
                            monsters.metadata_id=main_pages.id
                        ]],
                        where=string.format([[
                            items._pageID IN (%s)
                            AND maps.area_id IS NOT NULL
                            AND areas.boss_monster_ids HOLDS LIKE "%%"
                        ]],
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='areas.boss_monster_ids',
                    }
                )
 
                results2['boss_query'] = m_cargo.map_results_to_id{
                    results=results2['boss_query'],
                    field='items._pageName',
                }
            end
            local results = results2['boss_query'][data['items._pageName']] or {}
            local tbl = {}
            for _,v in ipairs(results) do
                tbl[#tbl+1] = v['areas.boss_monster_ids']
            end
            h.na_or_val(tr, #tbl)
        end,
        order = 19002,
    },
    {
        arg = {'legacy'},
        header = i18n.item_table.legacy,
        fields = {'items.name'},
        display = function(tr, data, na, results2)
            if results2['legacy_query'] == nil then
                results2['legacy_query'] = m_cargo.query(
                    {'items', 'legacy_variants'},
                    {
                        'items._pageID',
                        'items._pageName',
                        'items.frame_type',
                        'legacy_variants.removal_version',
                        'legacy_variants.implicit_stat_text',
                        'legacy_variants.explicit_stat_text',
                        'legacy_variants.stat_text',
                        'legacy_variants.base_item',
                        'legacy_variants.required_level'
                    },
                    {
                        join='items._pageID=legacy_variants._pageID',
                        where='legacy_variants.removal_version IS NOT NULL',
                        where=string.format(
                            'items._pageID IN (%s) AND legacy_variants.removal_version IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='items._pageName',
                    }
                )
 
                results2['legacy_query'] = m_cargo.map_results_to_id{
                    results=results2['legacy_query'],
                    field='items._pageName',
                }
            end
            local results = results2['legacy_query'][data['items._pageName']] or {}
 
            local tbl = mw.html.create('table')
                :attr('width', '100%')
            for _, v in ipairs(results) do
                local cell = {}
                local l = {
                    'legacy_variants.base_item',
                    'legacy_variants.stat_text'
                }
 
                -- Clean up data:
                for _, k in ipairs(l) do
                    if v[k] ~= nil then
                        local s = m_util.string.split(v[k], '*')
                        local s_flt = {}
                        for _, sss in ipairs(s) do
                            if sss ~= nil and sss ~= '' then
                                s_flt[#s_flt+1] = string.gsub(sss, '\n', '')
                            end
                        end
 
                        cell[#cell+1] = table.concat(s_flt, '<br>')
                    end
                end
 
                local sep = string.format(
                    '<span class="item-stat-separator -%s"></span>',
                    v['items.frame_type']
                )
                tbl
                    :tag('tr')
                        :attr('class', 'upgraded-from-set')
                        :tag('td')
                            :wikitext(
                                v['legacy_variants.removal_version']
                            )
                            :done()
                        :tag('td')
                            :attr('class', 'group legacy-stats plainlist')
                            :wikitext(table.concat(cell, sep))
                            :done()
                    :done()
            end
            tr
                :tag('td')
                    :node(tbl)
        end,
        order = 21001,
        sort_type = 'text',
    },
    {
        arg = {'granted_skills'},
        header = i18n.item_table.granted_skills,
        fields = {'items.name'},
        display = function(tr, data, na, results2)
            if results2['granted_skills_query'] == nil then
                results2['granted_skills_query'] = m_cargo.query(
                    {'items', 'item_mods', 'mods', 'skill', 'items=items2'},
                    {
                        'items._pageName',
                        'items.name',
                        'item_mods.id',
                        'mods._pageName',
                        'mods.id',
                        'mods.granted_skill',
                        'mods.stat_text_raw',
                        'skill._pageName',
                        'skill.skill_id',
                        'skill.active_skill_name',
                        'skill.skill_icon',
                        'skill.stat_text',
                        'items2.class',
                        'items2.name',
                        'items2.inventory_icon',
                        'items2.size_x',
                        'items2.size_y',
                        'items2.html',
                    },
                    {
                        join='items._pageID=item_mods._pageID, item_mods.id=mods.id, mods.granted_skill=skill.skill_id, skill._pageID=items2._pageID',
                        where=string.format(
                            'items._pageID IN (%s) AND mods.granted_skill IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                    }
                )
 
                results2['granted_skills_query'] = m_cargo.map_results_to_id{
                    results=results2['granted_skills_query'],
                    field='items._pageName',
                }
            end
            local results = results2['granted_skills_query'][data['items._pageName']] or {}
 
            local tbl = {}
            for _, v in ipairs(results) do
 
                -- Check if a level for the skill is specified in the
                -- mod stat text.
                -- Stat ids have unreliable naming convention so using
                -- the mod stat text instead.
                local level = ''
                local stat_text = v['mods.stat_text_raw'] or ''
                local level_number = string.match(
                    stat_text:lower(),
                    h.string.format(
                        i18n.item_table.granted_skills_level_pattern,
                        {
                            granted_skills_level_label = i18n.item_table.granted_skills_level_label:lower()
                        }
                    )
                )
 
                -- If a level number was specified in the stat text
                -- then add it to the cell:
                if level_number then
                    level = h.string.format(
                        i18n.item_table.granted_skills_level_format,
                        {
                            granted_skills_level_label = i18n.item_table.granted_skills_level_label,
                            level_number = level_number,
                        }
                    )
                end
 
                -- Use different formats depending on if it's a gem or
                -- not:
                if v['items2.class'] == nil  then
                    tbl[#tbl+1] = h.string.format(
                        i18n.item_table.granted_skills_skill_output_format,
                        {
                            level = level,
                            sl = h.skill_link{
                                skip_query=true,
                                page = v['skill.active_skill_name']
                                    or v['skill._pageName']
                                    or v['mods._pageName']
                                    or '',
                                name = v['skill.active_skill_name']
                                    or v['skill.stat_text']
                                    or v['mods.granted_skill'],
                                icon = v['skill.skill_icon'],
                            },
                        }
                    )
                else
                    local il_args = {
                        skip_query=true,
                        page=v['items2._pageName'],
                        name=v['items2.name'],
                        inventory_icon=v['items2.inventory_icon'],
                        width=v['items2.size_x'],
                        height=v['items2.size_y'],
                    }
 
                    -- TODO: add in tpl_args.
                    if no_html == nil then
                        il_args.html = v['items2.html']
                    end
                    tbl[#tbl+1] = h.string.format(
                        i18n.item_table.granted_skills_gem_output_format,
                        {
                            level = level,
                            il = h.item_link(il_args),
                        }
                    )
                end
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 22001,
        sort_type = 'text',
    },
    {
        arg = 'alternate_art',
        header = i18n.item_table.alternate_art,
        fields = {'items.alternate_art_inventory_icons'},
        display = function (tr, data)
            local alt_art = m_util.string.split(
                data['items.alternate_art_inventory_icons'],
                ','
            )
 
            -- TODO: Use il instead to handle size?
            -- local size = 39
            local out = {}
            for i,v in ipairs(alt_art) do
                out[#out+1] = string.format(
                    '[[%s|link=|%s]]',
                    v,
                    v
                )
            end
 
            tr
                :tag('td')
                    :wikitext(table.concat(out, ''))
        end,
        order = 23000,
        sort_type = 'text',
     },
     },
}
}


data_map.skill_gem_new = {
data_map.skill_gem = {
     {
     {
         arg = 'icon',
         arg = 'icon',
Line 662: Line 1,504:
         }},
         }},
         order = 1001,
         order = 1001,
        sort_type = 'text',
    },
    {
        arg = {'stat', 'stat_text'},
        header = i18n.item_table.stats,
        fields = {'skill.stat_text'},
        display = h.tbl.display.factory.value{},
        order = 2000,
        sort_type = 'text',
    },
    {
        arg = {'quality', 'quality_stat_text'},
        header = i18n.item_table.quality_stats,
        fields = {'skill.quality_stat_text'},
        display = h.tbl.display.factory.value{},
        order = 2001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
Line 669: Line 1,527:
         fields = {'skill.description'},
         fields = {'skill.description'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{},
         order = 2000,
         order = 2100,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
Line 675: Line 1,533:
         arg = 'level',
         arg = 'level',
         header = m_game.level_requirement.icon,
         header = m_game.level_requirement.icon,
         fields = h.tbl.range_fields('items.required_level'),
         fields = h.tbl.range_fields{field='items.required_level'},
         display = h.tbl.display.factory.range{field='items.required_level'},
         display = h.tbl.display.factory.range{field='items.required_level'},
         order = 3004,
         order = 3000,
    },
    {
        arg = 'str',
        header = '[[File:StrengthIcon small.png|link=|alt=Gem has strength requirement]]',
        fields = {'skill_gems.strength_percent'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill_gems.strength_percent'])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3001,
    },
    {
        arg = 'dex',
        header = '[[File:DexterityIcon small.png|link=|alt=Gem has dexterity requirement]]',
        fields = {'skill_gems.dexterity_percent'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill_gems.dexterity_percent'])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3002,
    },
    {
        arg = 'int',
        header = '[[File:IntelligenceIcon small.png|link=|alt=Gem has intelligence requirement]]',
        fields = {'skill_gems.intelligence_percent'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill_gems.intelligence_percent'])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3003,
     },
     },
     {
     {
Line 686: Line 1,580:
             [1] = {
             [1] = {
                 fmt='%s%%',
                 fmt='%s%%',
                skill_levels = true,
             },
             },
         }},
         }},
         order = 4000,
         order = 4000,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
     },
     },
     {
     {
         arg = 'cast_time',
         arg = 'cast_time',
         header = i18n.item_table.cast_time,
         header = i18n.item_table.cast_time,
         fields = {'skill_levels.cast_time'},
         fields = {'skill.cast_time'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{options = {
        }},
         order = 4001,
         order = 4001,
        options = {
        },
    },
    {
        arg = {'aspd', 'attack_speed', 'attack_speed_multiplier'},
        header = i18n.item_table.attack_speed_multiplier,
        fields = {'skill_levels.attack_speed_multiplier'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
                skill_levels = true,
            },
        }},
        order = 4002,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
     },
     },
     {
     {
Line 704: Line 1,624:
             [1] = {
             [1] = {
                 fmt='%s%%',
                 fmt='%s%%',
                skill_levels = true,
             },
             },
         }},
         }},
         order = 4002,
         order = 4003,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
     },
     },
     {
     {
         arg = 'mcm',
         arg = {'mcm', 'cost_multiplier'},
         header = i18n.item_table.mana_cost_multiplier,
         header = i18n.item_table.cost_multiplier,
         fields = {'skill_levels.mana_multiplier'},
         fields = {'skill_levels.cost_multiplier'},
         display = h.tbl.display.factory.value{options = {
         display = h.tbl.display.factory.value{options = {
             [1] = {
             [1] = {
                 fmt='%s%%',
                 fmt='%s%%',
                skill_levels = true,
             },
             },
         }},
         }},
         order = 5000,
         order = 5000,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
     },
     },
     {
     {
         arg = 'mana',
         arg = 'mana',
         header = i18n.item_table.mana_cost,
         header = i18n.item_table.mana_cost,
         fields = {'skill_levels.mana_cost', 'skill.has_percentage_mana_cost', 'skill.has_reservation_mana_cost'},
         fields = {'skill_levels.cost_amounts', 'skill_levels.mana_reservation_percent', 'skill_levels.mana_reservation_flat'},
         display = function (tr, data)
         display = function (tr, data, fields, data2)
             local appendix = ''
             local appendix = ''
             if m_util.cast.boolean(data['skill.has_percentage_mana_cost']) then
             local cost_field = ''
                 appendix = appendix .. '%'
            local sdata = data2.skill_levels[data['items._pageName']]
             end
            -- Percentage Mana reservation is in level 0 of most of the skill_levels at this point, but spellslinger and awakened blasphemy change it per-level
            if m_util.cast.boolean(data['skill.has_reservation_mana_cost']) then
            -- Flat Mana resservation is in real gem levels, but maybe there will be fixed flat mana reservations in the future.
            -- Per-use mana costs are stored in the real gem levels if they change, or in 0 if it's fixed.
            if(sdata['1']['skill_levels.mana_reservation_percent'] ~= nil or sdata['0']['skill_levels.mana_reservation_percent'] ~= nil) then
                cost_field = 'skill_levels.mana_reservation_percent'
                 appendix = appendix .. '%%'
             elseif(sdata['1']['skill_levels.mana_reservation_flat'] ~= nil or sdata['0']['skill_levels.mana_reservation_flat'] ~= nil) then
                cost_field = 'skill_levels.mana_reservation_flat'
                 appendix = appendix .. ' ' .. i18n.item_table.reserves_mana_suffix
                 appendix = appendix .. ' ' .. i18n.item_table.reserves_mana_suffix
            elseif(sdata['1']['skill_levels.cost_amounts'] ~= nil or sdata['0']['skill_levels.cost_amounts'] ~= nil) then
                cost_field = 'skill_levels.cost_amounts'
             end
             end
           
 
             tr
             h.tbl.display.factory.value{options = {
                 :tag('td')
                 [1] = {
                    :attr('data-sort-value', data['skill_levels.mana_cost'])
                     fmt='%d' .. appendix,
                     :wikitext(string.format('%d', data['skill_levels.mana_cost']) .. appendix)
                    skill_levels = true,
                },
            }}(tr, data, {cost_field}, data2)
         end,
         end,
         order = 5001,
         order = 5001,
        -- Need one set of options per field.
        options = {
            [1] = {
                skill_levels = true,
            },
            [2] = {
                skill_levels = true,
            },
            [3] = {
                skill_levels = true,
            },
        },
     },
     },
     {
     {
Line 743: Line 1,697:
         header = i18n.item_table.vaal_souls_requirement,
         header = i18n.item_table.vaal_souls_requirement,
         fields = {'skill_levels.vaal_souls_requirement'},
         fields = {'skill_levels.vaal_souls_requirement'},
         display = function (tr, data)
         display = h.tbl.display.factory.value{options = {
             local souls = tonumber(data['skill_levels.vaal_souls_requirement'])
             [1] = {
            tr
                 skill_levels = true,
                 :tag('td')
            },
                    :attr('data-sort-value', souls)
         }},
                    :wikitext(string.format('%d / %d / %d', souls, souls*1.5, souls*2))
         end,
         order = 6000,
         order = 6000,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
     },
     },
     {
     {
Line 756: Line 1,713:
         header = i18n.item_table.stored_uses,
         header = i18n.item_table.stored_uses,
         fields = {'skill_levels.vaal_stored_uses'},
         fields = {'skill_levels.vaal_stored_uses'},
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.factory.value{options = {
            [1] = {
                skill_levels = true,
            },
        }},
         order = 6001,
         order = 6001,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
     },
     },
     {
     {
Line 768: Line 1,734:
                 :tag('td')
                 :tag('td')
                     :attr('data-sort-value', data['skill.radius'])
                     :attr('data-sort-value', data['skill.radius'])
                     :wikitext(core.factory.descriptor_value{tbl=data, key='skill.radius_description'}(nil, nil, data['skill.radius']))
                     :wikitext(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_description'}(data['skill.radius']))
         end,
         end,
         order = 7000,
         order = 7000,
Line 781: Line 1,747:
                 :tag('td')
                 :tag('td')
                     :attr('data-sort-value', data['skill.radius_secondary'])
                     :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']))
                     :wikitext(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_secondary_description'}(data['skill.radius_secondary']))
         end,
         end,
         order = 7001,
         order = 7001,
Line 794: Line 1,760:
                 :tag('td')
                 :tag('td')
                     :attr('data-sort-value', data['skill.radius_tertiary'])
                     :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']))
                   :wikitext(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_tertiary_description'}(data['skill.radius_tertiary']))
         end,
         end,
         order = 7002,
         order = 7002,
Line 800: Line 1,766:
}
}


for i, attr in ipairs(m_game.constants.attributes) do
-- ----------------------------------------------------------------------------
    table.insert(data_map.generic_item, 7, {
-- Main functions
        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


local function _item_table(args)
    --[[
    Creates a generic table for items.


-- ----------------------------------------------------------------------------
    Examples
-- Invoke callables
    --------
-- ----------------------------------------------------------------------------
    = p.item_table{
        q_tables='vendor_rewards, quest_rewards, skill_gems',
        q_join='items._pageID=vendor_rewards._pageID, items._pageID=quest_rewards._pageID, items._pageID=skill_gems._pageID',
        q_where='(items.class="Active Skill Gems" OR items.class = "Support Skill Gems") AND (vendor_rewards.quest_id IS NOT NULL OR quest_rewards.quest_id IS NOT NULL)',
        vendor=1,
    }
    ]]


local p = {}
    m_item_util = m_item_util or require('Module:Item util')


--
    local t = os.clock()
-- Template:Item table
--


function p.item_table(frame)
     args.mode = args.mode or 'item'
     -- 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 = {
     local modes = {
         skill = {
         skill = {
             data = data_map.skill_gem_new,
             data = data_map.skill_gem,
             header = i18n.item_table.skill_gem,
             header = i18n.item_table.skill_gem,
         },
         },
Line 854: Line 1,799:
         },
         },
     }
     }
   
     if modes[args.mode] == nil then
    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)
         error(i18n.errors.invalid_item_table_mode)
     end
     end
      
 
     local results2 = {
        stats = {},
        skill_levels = {},
        pageIDs = {},
    }
 
     local row_infos = {}
     local row_infos = {}
     for _, row_info in ipairs(modes[tpl_args.mode].data) do
     for _, row_info in ipairs(modes[args.mode].data) do
         local enabled = false
         local enabled = false
         if row_info.arg == nil then
         if row_info.arg == nil then
             enabled = true
             enabled = true
         elseif type(row_info.arg) == 'string' and m_util.cast.boolean(tpl_args[row_info.arg]) then
         elseif type(row_info.arg) == 'string' and m_util.cast.boolean(args[row_info.arg]) then
             enabled = true
             enabled = true
         elseif type(row_info.arg) == 'table' then  
         elseif type(row_info.arg) == 'table' then
             for _, argument in ipairs(row_info.arg) do
             for _, argument in ipairs(row_info.arg) do
                 if m_util.cast.boolean(tpl_args[argument]) then
                 if m_util.cast.boolean(args[argument]) then
                     enabled = true
                     enabled = true
                     break
                     break
Line 878: Line 1,824:
             end
             end
         end
         end
       
 
         if enabled then
         if enabled then
             row_info.options = row_info.options or {}
             row_info.options = row_info.options or {}
Line 884: Line 1,830:
         end
         end
     end
     end
   
 
     -- Parse stat arguments
     -- Parse stat arguments
     local stat_columns = {}
     local stat_columns = {}
     local query_stats = {}
     local query_stats = {}
     local stat_results = {}
     for i=1, math.huge do -- repeat until no more columns are found
    local i = 0
    repeat
        i = i + 1
       
         local prefix = string.format('stat_column%s_', i)
         local prefix = string.format('stat_column%s_', i)
        if args[prefix .. 'stat1_id'] == nil then
            -- Each column requires at least one stat id
            break
        end
         local col_info = {
         local col_info = {
             header = tpl_args[prefix .. 'header'] or tostring(i),
             header = args[prefix .. 'header'] or tostring(i),
             format = tpl_args[prefix .. 'format'],
             format = args[prefix .. 'format'],
             stat_format = tpl_args[prefix .. 'stat_format'] or 'separate',
             stat_format = args[prefix .. 'stat_format'] or 'separate',
             order = tonumber(tpl_args[prefix .. 'order']) or (10000000 + i),
             order = tonumber(args[prefix .. 'order']) or (10000000 + i),
             stats = {},
             stats = {},
            options = {},
         }
         }
          
         for j=1, math.huge do
        local j = 0
             local stat_id = args[string.format('%sstat%s_id', prefix, j)]
        repeat
             if stat_id == nil then
            j = j +1
                 break
       
             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
             end
        until j == nil
            table.insert(col_info.stats, {id=stat_id})
       
             query_stats[stat_id] = true
        -- Don't add this column if no stats were provided.  
        if #col_info.stats > 0 then
             stat_columns[#stat_columns+1] = col_info
         end
         end
     until i == nil
        table.insert(stat_columns, col_info)
   
     end
 
     for _, col_info in ipairs(stat_columns) do
     for _, col_info in ipairs(stat_columns) do
         local row_info = {
         local row_info = {
Line 943: Line 1,871:
                         num_stats = num_stats + 1
                         num_stats = num_stats + 1
                         -- stat results from outside body
                         -- stat results from outside body
                         local stat = (stat_results[data['items._pageName']] or {})[stat_info.id]
                         local stat = (results2.stats[data['items._pageName']] or {})[stat_info.id]
                         if stat ~= nil then
                         if stat ~= nil then
                             stat_texts[#stat_texts+1] = h.format_value(tpl_args, frame, stat, {no_color=true})
                             stat_texts[#stat_texts+1] = m_util.html.format_value(args, stat, {no_color=true})
                             vmax = vmax + stat.max
                             vmax = vmax + stat.max
                         end
                         end
                     end
                     end
                   
 
                     if num_stats ~= #stat_texts then
                     if num_stats ~= #stat_texts then
                         tr:wikitext(m_util.html.td.na())
                         tr:wikitext(m_util.html.td.na())
Line 959: Line 1,887:
                             text = table.concat(stat_texts, ', ')
                             text = table.concat(stat_texts, ', ')
                         end
                         end
                   
 
                         tr:tag('td')
                         tr:tag('td')
                             :attr('data-sort-value', vmax)
                             :attr('data-sort-value', vmax)
Line 972: Line 1,900:
                     }
                     }
                     for _, stat_info in ipairs(col_info.stats) do
                     for _, stat_info in ipairs(col_info.stats) do
                         local stat = (stat_results[data['items._pageName']] or {})[stat_info.id]
                         local stat = (results2.stats[data['items._pageName']] or {})[stat_info.id]
                         if stat ~= nil then
                         if stat ~= nil then
                             for k, v in pairs(total_stat) do
                             for k, v in pairs(total_stat) do
Line 979: Line 1,907:
                         end
                         end
                     end
                     end
                   
 
                     if col_info.format == nil then
                     if col_info.format == nil then
                         col_info.format = '%s'
                         col_info.format = '%s'
                     end
                     end
                   
 
                     tr:tag('td')
                     tr:tag('td')
                         :attr('data-sort-value', total_stat.max)
                         :attr('data-sort-value', total_stat.max)
                         :attr('class', 'tc -mod')
                         :attr('class', 'tc -mod')
                         :wikitext(string.format(col_info.format, h.format_value(tpl_args, frame, total_stat, {no_color=true})))
                         :wikitext(string.format(col_info.format, m_util.html.format_value(args, total_stat, {no_color=true})))
                 else
                 else
                     error(string.format(i18n.errors.generic_argument_parameter, 'stat_format', col_info.stat_format))
                     error(string.format(i18n.errors.generic_argument_parameter, 'stat_format', col_info.stat_format))
Line 996: Line 1,924:
         table.insert(row_infos, row_info)
         table.insert(row_infos, row_info)
     end
     end
   
 
     -- sort the rows
     -- sort the rows
     table.sort(row_infos, function (a, b)
     table.sort(row_infos, function (a, b)
         return (a.order or 0) < (b.order or 0)
         return (a.order or 0) < (b.order or 0)
     end)
     end)
   
 
     -- Parse query arguments
     -- Parse query arguments
     local tables_assoc = {items=true}
     local tables_assoc = {items=true}
Line 1,013: Line 1,941:
         'items.size_y',
         'items.size_y',
     }
     }
   
 
     --
     --
     local prepend = {
     local prepend = {
         q_groupBy=true,
         q_groupBy=true,
         q_tables=true
         q_tables=true,
     }
     }
   
 
     local query = {}
     local query = {}
     for key, value in pairs(tpl_args) do  
     for key, value in pairs(args) do
         if string.sub(key, 0, 2) == 'q_' then
         if string.sub(key, 0, 2) == 'q_' then
             if prepend[key] then
             if prepend[key] then
                 value = ',' .. value
                 value = ',' .. value
             end
             end
           
 
             query[string.sub(key, 3)] = value
             query[string.sub(key, 3)] = value
         end
         end
     end
     end
      
 
     -- Namespace condition
    -- This is mainly to prevent items from user pages or other testing pages
    -- from being returned in the query results.
    if args.namespaces ~= 'any' then
        local namespaces = m_util.cast.table(args.namespaces, {callback=m_util.cast.number})
        if #namespaces > 0 then
            namespaces = table.concat(namespaces, ',')
        else
            namespaces = m_item_util.get_item_namespaces{format = 'list'}
        end
        query.where = string.format('(%s) AND items._pageNamespace IN (%s)', query.where, namespaces)
    end
 
    local skill_levels = {}
     for _, rowinfo in ipairs(row_infos) do
     for _, rowinfo in ipairs(row_infos) do
         if type(rowinfo.fields) == 'function' then
         if type(rowinfo.fields) == 'function' then
Line 1,037: Line 1,979:
         for index, field in ipairs(rowinfo.fields) do
         for index, field in ipairs(rowinfo.fields) do
             rowinfo.options[index] = rowinfo.options[index] or {}
             rowinfo.options[index] = rowinfo.options[index] or {}
             fields[#fields+1] = field
             if rowinfo.options[index].skill_levels then
            tables_assoc[m_util.string.split(field, '%.')[1]] = true
                skill_levels[#skill_levels+1] = field
            else
                fields[#fields+1] = field
                tables_assoc[m_util.string.split(field, '%.')[1]] = true
            end
         end
         end
     end
     end
   
 
    -- reformat the fields & tables so they can be retrieved correctly
     if #skill_levels > 0 then
     for index, field in ipairs(fields) do
         fields[#fields+1] = 'skill.max_level'
         fields[index] = string.format('%s=%s', field, field)
        tables_assoc.skill = true
     end
     end
      
 
     -- Reformat the tables and fields so they can be retrieved correctly:
     local tables = {}
     local tables = {}
     for table_name,_ in pairs(tables_assoc) do
     for table_name,_ in pairs(tables_assoc) do
         tables[#tables+1] = table_name
         tables[#tables+1] = table_name
     end
     end
      
     local tbls = table.concat(tables,',') .. (query.tables or '')
     -- take care of required joins according to the tables
     query.tables = m_util.string.split(tbls, ',')
 
    for index, field in ipairs(fields) do
        fields[index] = string.format('%s=%s', field, field)
    end
    query.fields = fields


    -- Take care of the minimum required joins, joins from templates
    -- must still be userdefined:
     local joins = {}
     local joins = {}
     for index, table_name in ipairs(tables) do
     for index, table_name in ipairs(tables) do
         if table_name ~= 'items' then
         if table_name ~= 'items' then
             joins[#joins+1] = string.format('items._pageID=%s._pageID', table_name)  
             joins[#joins+1] = string.format('items._pageID=%s._pageID', table_name)
         end
         end
     end
     end
Line 1,067: Line 2,021:
         -- leave query.join as is
         -- leave query.join as is
     end
     end
   
 
     -- Cargo workaround: avoid duplicates using groupBy
     -- Needed to eliminate duplicates supplied via table joins:
     query.groupBy = 'items._pageID' .. (query.groupBy or '')
     query.groupBy = 'items._pageID' .. (query.groupBy or '')
      
 
     query.limit = tonumber(query.limit)
     -- Query results:
     if query.limit == nil then
     local results = m_cargo.query(query.tables, query.fields, query)
        query.limit = c.query_default
 
    elseif query.limit > c.query_max then
     if #results == 0 and args.default ~= nil then
         query.limit = c.query_max
         return args.default
     end
     end
   
 
    local results = cargo.query(
     if #results > 0 then
        table.concat(tables,',') .. (query.tables or ''),
         -- Create a list of found pageIDs for column specific queries:
        table.concat(fields,','),
         for _,v in ipairs(results) do
        query
             results2.pageIDs[#results2.pageIDs+1] = v['items._pageID']
    )
   
    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
         end
          
 
         local query_stat_ids = {}
         -- fetch skill level information
        for stat_id, _ in pairs(query_stats) do
         if #skill_levels > 0 then
            query_stat_ids[#query_stat_ids+1] = string.format('item_stats.id="%s"', stat_id)
            skill_levels[#skill_levels+1] = 'skill_levels._pageName'
            skill_levels[#skill_levels+1] = 'skill_levels.level'
            local pages = {}
            for _, row in ipairs(results) do
                pages[#pages+1] = string.format('(skill_levels._pageID="%s" AND skill_levels.level IN (0, 1, %s))', row['items._pageID'], row['skill.max_level'])
            end
            local temp = m_cargo.query(
                {'skill_levels'},
                skill_levels,
                {
                    where=table.concat(pages, ' OR '),
                    groupBy='skill_levels._pageID, skill_levels.level',
                }
            )
            -- map to results
            for _, row in ipairs(temp) do
                if results2.skill_levels[row['skill_levels._pageName']] == nil then
                  results2.skill_levels[row['skill_levels._pageName']] = {}
                end
                -- TODO: convert to int?
                results2.skill_levels[row['skill_levels._pageName']][row['skill_levels.level']] = row
            end
         end
         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
         if #stat_columns > 0 then
            local stat = {
            local stat_results = m_cargo.query(
                min = tonumber(row['item_stats.min']),
                {'items', 'item_stats'},
                max = tonumber(row['item_stats.max']),
                {'item_stats._pageName', 'item_stats.id', 'item_stats.min', 'item_stats.max', 'item_stats.avg'},
                avg = tonumber(row['item_stats.avg']),
                {
            }
                    join = 'items._pageID=item_stats._pageID',
           
                    where = string.format(
            if stat_results[row['item_stats._pageName']] == nil then
                        'item_stats._pageID IN (%s) AND item_stats.id IN ("%s")',
                stat_results[row['item_stats._pageName']] = {[row['item_stats.id']] = stat}
                        table.concat(m_util.table.column(results, 'items._pageID'), ','),
            else
                        table.concat(m_util.table.keys(query_stats), '","')
                stat_results[row['item_stats._pageName']][row['item_stats.id']] = stat
                    ),
                    groupBy = 'items._pageID, item_stats.id',
                }
            )
            for _, row in ipairs(stat_results) do
                local stat = {
                    min = tonumber(row['item_stats.min']),
                    max = tonumber(row['item_stats.max']),
                    avg = tonumber(row['item_stats.avg']),
                }
 
                if results2.stats[row['item_stats._pageName']] == nil then
                    results2.stats[row['item_stats._pageName']] = {[row['item_stats.id']] = stat}
                else
                    results2.stats[row['item_stats._pageName']][row['item_stats.id']] = stat
                end
             end
             end
        end
       
        -- Cargo doesnt support offset yet
        if #temp == 5000 then
            --TODO: Cargo
            error('Stats > 5000')
         end
         end
     end
     end
      
 
 
     --
    -- Display the table
    --
 
     local tbl = mw.html.create('table')
     local tbl = mw.html.create('table')
     tbl:attr('class', 'wikitable sortable item-table')
     tbl:attr('class', 'wikitable sortable item-table')
      
     if m_util.cast.boolean(args.responsive) then
     -- Header
        tbl:addClass('responsive-table')
   
    end
 
     -- Headers:
     local tr = tbl:tag('tr')
     local tr = tbl:tag('tr')
     tr
     tr
         :tag('th')
         :tag('th')
             :wikitext(modes[tpl_args.mode].header)
             :wikitext(modes[args.mode].header)
             :done()
             :done()
           
     for _, row_info in ipairs(row_infos) do
     for _, row_info in ipairs(row_infos) do
         tr
         local th = tr:tag('th')
             :tag('th')
 
                :attr('data-sort-type', row_info.sort_type or 'number')
        if row_info.colspan then
                :wikitext(row_info.header)
             th:attr('colspan', row_info.colspan)
                :done()
        end
 
        th
            :attr('data-sort-type', row_info.sort_type or 'number')
            :wikitext(row_info.header)
     end
     end
      
 
     -- Rows:
     for _, row in ipairs(results) do
     for _, row in ipairs(results) do
         tr = tbl:tag('tr')
         tr = tbl:tag('tr')
       
 
         local il_args = {
         local il_args = {
             skip_query=true,
             skip_query=true,
             page=row['items._pageName'],  
             page=row['items._pageName'],
             name=row['items.name'],  
             name=row['items.name'],
             inventory_icon=row['items.inventory_icon'],
             inventory_icon=row['items.inventory_icon'],
            html=row['items.html'],
             width=row['items.size_x'],
             width=row['items.size_x'],
             height=row['items.size_y'],
             height=row['items.size_y'],
         }
         }
          
 
         if tpl_args.large then
         if args.no_html == nil then
             il_args.large = tpl_args.large
            il_args.html = row['items.html']
        end
 
         if args.large then
             il_args.large = args.large
         end
         end
       
 
         tr
         tr
             :tag('td')
             :tag('td')
                 :wikitext(f_item_link(il_args))
                 :wikitext(h.item_link(il_args))
                 :done()
                 :done()
               
 
         for _, rowinfo in ipairs(row_infos) do
         for _, rowinfo in ipairs(row_infos) do
             -- this has been cast from a function in an earlier step
             -- this has been cast from a function in an earlier step
             local display = true
             local display = true
           
             for index, field in ipairs(rowinfo.fields) do
             for index, field in ipairs(rowinfo.fields) do
                 -- this will bet set to an empty value not nil confusingly
                 -- this will bet set to an empty value not nil confusingly
                 if row[field] == '' then
                 if row[field] == nil or row[field] == '' then
                     if rowinfo.options[index].optional ~= true then
                     local opts = rowinfo.options[index]
                    if opts.optional ~= true and opts.skill_levels ~= true then
                         display = false
                         display = false
                         break
                         break
Line 1,194: Line 2,166:
             end
             end
             if display then
             if display then
                 rowinfo.display(tr, row, rowinfo.fields)
                 rowinfo.display(tr, row, rowinfo.fields, results2)
             else
             else
                 tr:wikitext(m_util.html.td.na())
                 tr:wikitext(m_util.html.td.na())
Line 1,200: Line 2,172:
         end
         end
     end
     end
   
 
     cats = {}
     local cats = {}
     if #results == query.limit then
     if #results == query.limit then
         cats[#cats+1] = i18n.categories.query_limit
         cats[#cats+1] = i18n.categories.query_limit
     end
     end
   
 
     if #results == c.query_max then
     if #results == 0 then
         cats[#cats+1] = i18n.categories.query_hard_limit
         cats[#cats+1] = i18n.categories.no_results
    end
 
    mw.logObject({os.clock() - t, query})
 
    return tostring(tbl) .. m_util.misc.add_category(cats, {ignore_blacklist=args.debug})
end
 
 
-------------------------------------------------------------------------------
-- Map item drops
-------------------------------------------------------------------------------
 
local function _map_item_drops(args)
    --[[
    Gets the area id from the map item and activates
    Template:Area_item_drops.
 
    Examples:
    = p.map_item_drops{page='Underground River Map (War for the Atlas)'}
    ]]
 
    local tables = {'maps'}
    local fields = {
        'maps.area_id',
    }
    local query = {
        -- Only need each page name once
        groupBy = 'maps._pageName',
    }
    if args.page then
        -- Join with _pageData in order to check for page redirect
        tables[#tables+1] = '_pageData'
        fields[#fields+1] = '_pageData._pageNameOrRedirect'
        query.where = string.format(
            '_pageData._pageName="%s"',
            args.page
        )
        query.join = 'maps._pageName = _pageData._pageNameOrRedirect'
    else
        query.where = string.format(
            'maps._pageName="%s"',
            tostring(mw.title.getCurrentTitle())
        )
    end
    query.where = query.where .. ' AND maps.area_id > ""' -- area_id must not be empty or null
    local results = m_cargo.query(tables, fields, query)
    local id = ''
    if #results > 0 then
        id = results[1]['maps.area_id']
    end
    return mw.getCurrentFrame():expandTemplate{ title = 'Area item drops', args = {area_id=id} }
end
 
-------------------------------------------------------------------------------
-- Prophecy description
-------------------------------------------------------------------------------
 
local function _prophecy_description(args)
    args.page = args.page or tostring(mw.title.getCurrentTitle())
 
    local results = m_cargo.query(
        {'prophecies'},
        {'prophecies.objective', 'prophecies.reward'},
        {
            where=string.format('prophecies._pageName="%s"', args.page),
            -- Only need each page name once
            groupBy='prophecies._pageName',
        }
    )
 
    results = results[1]
 
    local out = {}
 
    if results['prophecies.objective'] then
        out[#out+1] = string.format('<h2>%s</h2>', i18n.prophecy_description.objective)
        out[#out+1] = results['prophecies.objective']
    end
 
    if results['prophecies.reward'] then
        out[#out+1] = string.format('<h2>%s</h2>', i18n.prophecy_description.reward)
        out[#out+1] = results['prophecies.reward']
    end
 
    return table.concat(out, '\n')
end
 
 
local function _simple_item_list(args)
    --[[
    Creates a simple list of items.
 
    Examples
    --------
    = p.simple_item_list{
        q_tables='maps',
        q_join='items._pageID=maps._pageID',
        q_where='maps.tier=1 AND items.drop_enabled=1 AND items.rarity_id="normal"',
        no_html=1,
        link_from_name=1,
    }
    ]]
 
    local query = {}
    for key, value in pairs(args) do
        if string.sub(key, 0, 2) == 'q_' then
            query[string.sub(key, 3)] = value
        end
    end
 
    local fields = {
        'items._pageName',
        'items.name',
        'items.class',
    }
 
    if args.no_icon == nil then
        fields[#fields+1] = 'items.inventory_icon'
    end
 
    if args.no_html == nil then
        fields[#fields+1] = 'items.html'
    end
 
    local tables = m_util.cast.table(args.q_tables)
    table.insert(tables, 1, 'items')
 
    query.groupBy = query.groupBy or 'items._pageID'
 
    local results = m_cargo.query(
        tables,
        fields,
        query
    )
 
    local out = {}
    for _, row in ipairs(results) do
        local page
        if args.use_name_as_link ~= nil then
            page = row['items.name']
        else
            page = row['items._pageName']
        end
 
        local link = h.item_link{
            page=page,
            name=row['items.name'],
            inventory_icon=row['items.inventory_icon'] or '',
            html=row['items.html'] or '',
            skip_query=true
        }
 
        if args.format == nil then
            out[#out+1] = string.format('* %s', link)
        elseif args.format == 'none' then
            out[#out+1] = link
        elseif args.format == 'li' then
            out[#out+1] = string.format('<li>%s</li>', link)
        else
            error(string.format(i18n.errors.generic_argument_parameter, 'format', args.format))
        end
    end
 
    if args.format == nil then
        return table.concat(out, '\n')
    elseif args.format == 'none' then
        return table.concat(out, '\n')
    elseif args.format == 'li' then
        return table.concat(out)
     end
     end
   
    return tostring(tbl) .. m_util.misc.add_category(cats, {ingore_blacklist=tpl_args.debug})
end
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Debug stuff
-- Exported functions
-- ----------------------------------------------------------------------------
 
local p = {}
 
--
-- Template:Item table
--
p.item_table = m_util.misc.invoker_factory(_item_table)
 
--
-- Template:Map item drops
--
p.map_item_drops = m_util.misc.invoker_factory(_map_item_drops, {
    wrappers = cfg.wrappers.map_item_drops,
})
 
--
-- Template:Prophecy description
--
p.prophecy_description = m_util.misc.invoker_factory(_prophecy_description, {
    wrappers = cfg.wrappers.prophecy_description,
})
 
--
-- Template:Simple item list
--
p.simple_item_list = m_util.misc.invoker_factory(_simple_item_list, {
    wrappers = cfg.wrappers.simple_item_list,
})
 
-- ----------------------------------------------------------------------------
-- Debug
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
p.debug = {}
p.debug = {}
Line 1,220: Line 2,391:
function p.debug._tbl_data(tbl)
function p.debug._tbl_data(tbl)
     keys = {}
     keys = {}
     for _, data in ipairs(data_map.generic_item) do
     for _, data in ipairs(tbl) do
         if type(data.arg) == 'string' then  
         if type(data.arg) == 'string' then
             keys[data.arg] = 1
             keys[data.arg] = 1
         elseif type(data.arg) == 'table' then
         elseif type(data.arg) == 'table' then
Line 1,229: Line 2,400:
         end
         end
     end
     end
   
 
     local out = {}
     local out = {}
     for key, _ in pairs(keys) do
     for key, _ in pairs(keys) do
         out[#out+1] = string.format("['%s'] = '1'", key)
         out[#out+1] = string.format("['%s'] = '1'", key)
     end
     end
   
 
     return table.concat(out, ', ')
     return table.concat(out, ', ')
end
end
Line 1,243: Line 2,414:


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


return p
return p

Latest revision as of 16:02, 6 May 2024

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


This module is used on 4000+ 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

-------------------------------------------------------------------------------
-- 
--                             Module:Item table
-- 
-- This module implements Template:Item table and other templates that query
-- and display tables or lists of items.
-------------------------------------------------------------------------------

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

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

local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or mw.loadData('Module:Game')

-- Lazy loading
local m_item_util -- require('Module:Item util')
local f_item_link -- require('Module:Item link').item_link
local f_skill_link -- require('Module:Skill link').skill_link

-- 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:Item table/config/sandbox') or mw.loadData('Module:Item table/config')

local i18n = cfg.i18n

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

local h = {}
h.string = {}

function h.string.format(str, vars)
    --[[
    Allow string replacement using named arguments.
    
    TODO: 
    * Support %d ?
    * Support 0.2f ?

    Parameters
    ----------
    str : String to replace. 
    vars : Table of arguments.

    Examples
    --------
    = h.string.format('{foo} is {bar}.', {foo='Dinner', bar='nice'})

    References
    ----------
    http://lua-users.org/wiki/StringInterpolation
    ]]

    if not vars then
        vars = str
        str = vars[1]
    end
    
    return (string.gsub(str, "({([^}]+)})",
        function(whole, i)
          return vars[i] or whole
        end))
end

-- Lazy loading for Module:Item link
function h.item_link(args)
    if not f_item_link then
        f_item_link = require('Module:Item link').item_link
    end
    return f_item_link(args)
end

-- Lazy loading for Module:Skill link
function h.skill_link(args)
    if not f_skill_link then
        f_skill_link = require('Module:Skill link').skill_link
    end
    return f_skill_link(args)
end

function h.na_or_val(tr, value, func)
    if value == nil or value == '' then
        tr:wikitext(m_util.html.td.na())
    else
        local raw_value = value
        if func ~= nil then
            value = func(value)
        end
        tr
            :tag('td')
                :attr('data-sort-value', raw_value)
                :wikitext(value)
                :done()
    end
end

h.tbl = {}

function h.tbl.range_fields(args)
    local suffixes = {'maximum', 'text', 'colour'}
    if args.full then
        suffixes[#suffixes+1] = 'minimum'
    end
        
    return function()
        local fields = {}
        local inner = function (field)
            for _, partial_field in ipairs(suffixes) do
                fields[#fields+1] = string.format('%s_range_%s', field, partial_field)
            end
        end
        
        if type(args.field) == 'table' then 
            for _, field in ipairs(args.field) do
                inner(field)
            end
        else
            inner(args.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, fields, data2)
        local values = {}
        local fmt_values = {}
        local sdata = data2.skill_levels[data['items._pageName']]

        for index, field in ipairs(fields) do
            local value = {
                min=data[field],
                max=data[field],
                base=data[field],
            }
            if sdata then --For skill data
                -- Use the fixed value, or the first-level value.
                value.min = value.min or sdata['0'][field] or sdata['1'][field]
                -- Fall back to the fixed value, and then the max level value.
                value.max = value.max or sdata['0'][field] or sdata[data['skill.max_level']][field]
            end
            if value.min then
                values[#values+1] = value.max
                local opts = args.options[index] or {}
                -- global colour is set, no overrides
                if args.colour ~= nil then
                    opts.no_color = true
                end
                fmt_values[#fmt_values+1] = m_util.html.format_value(nil, value, opts)
            end
        end

        if #values == 0 then
            tr:wikitext(m_util.html.td.na())
        else
            local td = tr:tag('td')
            td:attr('data-sort-value', table.concat(values, ', '))
            td:wikitext(table.concat(fmt_values, ', '))
            if args.colour then
                td:attr('class', 'tc -' .. args.colour)
            end
        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

function h.tbl.display.factory.range_composite(args)
    -- division by default
    if args.func == nil then
        args.func = function (a, b)
            if b == 0 then
                return 'fail'
            end
            return a / b
        end
    end
    
    return function(tr, data, fields)
        local field = {}
        for i=1, 2 do 
            local fieldn = args['field' .. i]
            field[i] = {
                min = tonumber(data[string.format('%s_range_minimum', fieldn)]) or 0,
                max = tonumber(data[string.format('%s_range_maximum', fieldn)]) or 0,
                color = data[string.format('%s_range_colour', fieldn)] or 'default',
            }
        end
        
        field.min = args.func(field[1].min, field[2].min)
        field.max = args.func(field[1].max, field[2].max)
        
        if field.min == 'fail' or field.max == 'fail' then
            field.text = ''
            field.color = 'default'
        else
            for i=1, 2 do
                if field[i].color ~= 'default' then
                    field.color = field[i].color
                    break
                end
            end
            if field.min == field.max then
                field.text = string.format('%.2f', field.min)
            else
                field.text = string.format('(%.2f-%.2f)', field.min, field.max)
            end
        end
    
        tr
            :tag('td')
                :attr('data-sort-value', field.max)
                :attr('class', 'tc -' .. field.color)
                :wikitext(field.text)
                :done()
    end
end

function h.tbl.display.factory.descriptor_value(args)
    -- Arguments:
    --  key
    --  tbl
    args = args or {}
    return function (value)
        if args.tbl[args.key] then
            value = m_util.html.abbr(value, args.tbl[args.key])
        end
        return value
    end
end

function h.tbl.display.factory.atlas_tier(args)
    args = args or {}
    return function (tr, data)
        for i=0,4 do
            local t = tonumber(data['atlas_maps.map_tier' .. i])

            if t == 0 then
                tr
                    :tag('td')
                        :attr('table-sort-value', 0)
                        :attr('class', 'table-cell-xmark')
                        :wikitext('✗')
            else
                if args.is_level then
                    t = t + 67
                end
                tr
                    :tag('td')
                        :attr('class', 'tc -value')
                        :wikitext(t)
                        :done()
            end
        end
    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 = 'rarity',
        header = i18n.item_table.rarity,
        fields = {'items.rarity'},
        display = h.tbl.display.factory.value{},
        order = 1002,
    },
    {
        arg = 'rarity_id',
        header = i18n.item_table.rarity_id,
        fields = {'items.rarity_id'},
        display = h.tbl.display.factory.value{},
        order = 1003,
    },
    {
        arg = 'metadata_id',
        header = i18n.item_table.metadata_id,
        fields = {'items.metadata_id'},
        display = h.tbl.display.factory.value{},
        order = 1004,
    },
    {
        arg = 'essence',
        header = i18n.item_table.essence_level,
        fields = {'essences.level'},
        display = h.tbl.display.factory.value{},
        order = 1100,
    },
    {
        arg = {'drop', 'drop_level'},
        header = i18n.item_table.drop_level,
        fields = {'items.drop_level'},
        display = h.tbl.display.factory.value{},
        order = 1200,
    },
    {
        arg = {'drop_level_maximum'},
        header = i18n.item_table.drop_level_maximum,
        fields = {'items.drop_level_maximum'},
        display = h.tbl.display.factory.value{},
        order = 1201,
    },
    {
        arg = 'stack_size',
        header = i18n.item_table.stack_size,
        fields = {'stackables.stack_size'},
        display = h.tbl.display.factory.value{},
        order = 1300,
    },
    {
        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 = 1301,
    },
    -- Requirements
    {
        arg = 'level',
        header = m_game.level_requirement.icon,
        fields = h.tbl.range_fields{field='items.required_level'},
        display = h.tbl.display.factory.range{field='items.required_level'},
        order = 1400,
    },
    {
        arg = 'str',
        header = '[[File:StrengthIcon small.png|link=|alt=Required strength]]',
        fields = h.tbl.range_fields{field='items.required_strength'},
        display = h.tbl.display.factory.range{field='items.required_strength'},
        order = 1401,
    },
    {
        arg = 'dex',
        header = '[[File:DexterityIcon small.png|link=|alt=Required dexterity]]',
        fields = h.tbl.range_fields{field='items.required_dexterity'},
        display = h.tbl.display.factory.range{field='items.required_dexterity'},
        order = 1402,
    },
    {
        arg = 'int',
        header = '[[File:IntelligenceIcon small.png|link=|alt=Required intelligence]]',
        fields = h.tbl.range_fields{field='items.required_intelligence'},
        display = h.tbl.display.factory.range{field='items.required_intelligence'},
        order = 1403,
    },
    -- Armour 15xx
    {
        arg = 'ar',
        header = i18n.item_table.armour,
        fields = h.tbl.range_fields{field='armours.armour'},
        display = h.tbl.display.factory.range{field='armours.armour'},
        order = 1500,
    },
    {
        arg = 'ev',
        header =i18n.item_table.evasion,
        fields = h.tbl.range_fields{field='armours.evasion'},
        display = h.tbl.display.factory.range{field='armours.evasion'},
        order = 1501,
    },
    {
        arg = 'es',
        header = i18n.item_table.energy_shield,
        fields = h.tbl.range_fields{field='armours.energy_shield'},
        display = h.tbl.display.factory.range{field='armours.energy_shield'},
        order = 1502,
    },
    {
        arg = 'wd',
        header = i18n.item_table.ward,
        fields = h.tbl.range_fields{field='armours.ward'},
        display = h.tbl.display.factory.range{field='armours.ward'},
        order = 1503,
    },
    {
        arg = 'block',
        header = i18n.item_table.block,
        fields = h.tbl.range_fields{field='shields.block'},
        display = h.tbl.display.factory.range{field='shields.block'},
        order = 1504,
    },
    --[[{
        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,

    },]]--
    -- Weapons 16xx
    {
        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 = 1600,
    },
    {
        arg = {'weapon', 'aps'},
        header = i18n.item_table.attacks_per_second,
        fields = h.tbl.range_fields{field='weapons.attack_speed'},
        display = h.tbl.display.factory.range{field='weapons.attack_speed'},
        order = 1601,
    },
    {
        arg = {'weapon', 'crit'},
        header = i18n.item_table.local_critical_strike_chance,
        fields = h.tbl.range_fields{field='weapons.critical_strike_chance'},
        display = h.tbl.display.factory.range{field='weapons.critical_strike_chance'},
        order = 1602,
    },
    {
        arg = {'physical_dps'},
        header = i18n.item_table.physical_dps,
        fields = h.tbl.range_fields{field='weapons.physical_dps'},
        display = h.tbl.display.factory.range{field='weapons.physical_dps'},
        order = 1603,
    },
    {
        arg = {'lightning_dps'},
        header = i18n.item_table.lightning_dps,
        fields = h.tbl.range_fields{field='weapons.lightning_dps'},
        display = h.tbl.display.factory.range{field='weapons.lightning_dps'},
        order = 1604,
    },
    {
        arg = {'cold_dps'},
        header = i18n.item_table.cold_dps,
        fields = h.tbl.range_fields{field='weapons.cold_dps'},
        display = h.tbl.display.factory.range{field='weapons.cold_dps'},
        order = 1605,
    },
    {
        arg = {'fire_dps'},
        header = i18n.item_table.fire_dps,
        fields = h.tbl.range_fields{field='weapons.fire_dps'},
        display = h.tbl.display.factory.range{field='weapons.fire_dps'},
        order = 1606,
    },
    {
        arg = {'chaos_dps'},
        header = i18n.item_table.chaos_dps,
        fields = h.tbl.range_fields{field='weapons.chaos_dps'},
        display = h.tbl.display.factory.range{field='weapons.chaos_dps'},
        order = 1607,
    },
    {
        arg = {'elemental_dps'},
        header = i18n.item_table.elemental_dps,
        fields = h.tbl.range_fields{field='weapons.elemental_dps'},
        display = h.tbl.display.factory.range{field='weapons.elemental_dps'},
        order = 1608,
    },
    {
        arg = {'poison_dps'},
        header = i18n.item_table.poison_dps,
        fields = h.tbl.range_fields{field='weapons.poison_dps'},
        display = h.tbl.display.factory.range{field='weapons.poison_dps'},
        order = 1609,
    },
    {
        arg = {'dps'},
        header = i18n.item_table.dps,
        fields = h.tbl.range_fields{field='weapons.dps'},
        display = h.tbl.display.factory.range{field='weapons.dps'},
        order = 1610,
    },
    -- Flasks 17xx
    {
        arg = 'flask_life',
        header = i18n.item_table.flask_life,
        fields = h.tbl.range_fields{field='flasks.life'},
        display = h.tbl.display.factory.range{field='flasks.life'},
        order = 1700,
    },
    {
        arg = 'flask_life_per_second',
        header = i18n.item_table.flask_life_per_second,
        fields = h.tbl.range_fields{field={'flasks.life', 'flasks.duration'}, full=true},
        display = h.tbl.display.factory.range_composite{field1='flasks.life', field2='flasks.duration'},
        order = 1701,
    },
    {
        arg = 'flask_life_per_charge',
        header = i18n.item_table.flask_life_per_charge,
        fields = h.tbl.range_fields{field={'flasks.life', 'flasks.charges_per_use'}, full=true},
        display = h.tbl.display.factory.range_composite{field1='flasks.life', field2='flasks.charges_per_use'},
        order = 1702,
    },
    {
        arg = 'flask_mana',
        header = i18n.item_table.flask_mana,
        fields = h.tbl.range_fields{field='flasks.mana'},
        display = h.tbl.display.factory.range{field='flasks.mana'},
        order = 1703,
    },
    {
        arg = 'flask_mana_per_second',
        header = i18n.item_table.flask_mana_per_second,
        fields = h.tbl.range_fields{field={'flasks.mana', 'flasks.duration'}, full=true},
        display = h.tbl.display.factory.range_composite{field1='flasks.mana', field2='flasks.duration'},
        order = 1704,
    },
    {
        arg = 'flask_mana_per_charge',
        header = i18n.item_table.flask_mana_per_charge,
        fields = h.tbl.range_fields{field={'flasks.mana', 'flasks.charges_per_use'}, full=true},
        display = h.tbl.display.factory.range_composite{field1='flasks.mana', field2='flasks.charges_per_use'},
        order = 1705,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_duration,
        fields = h.tbl.range_fields{field='flasks.duration'},
        display = h.tbl.display.factory.range{field='flasks.duration'},
        order = 1706,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_charges_per_use,
        fields = h.tbl.range_fields{field='flasks.charges_per_use'},
        display = h.tbl.display.factory.range{field='flasks.charges_per_use'},
        order = 1707,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_maximum_charges,
        fields = h.tbl.range_fields{field='flasks.charges_max'},
        display = h.tbl.display.factory.range{field='flasks.charges_max'},
        order = 1708,
    },
    -- Jewels 18xx
    {
        arg = 'jewel_limit',
        header = i18n.item_table.jewel_limit,
        fields = {'jewels.jewel_limit'},
        display = h.tbl.display.factory.value{},
        order = 1800,
    },
    {
        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 = 1801,
    },
    -- Maps 19xx
    {
        arg = 'map_tier',
        header = i18n.item_table.map_tier,
        fields = {'maps.tier'},
        display = h.tbl.display.factory.value{},
        order = 1900,
    },
    {
        arg = 'map_level',
        header = i18n.item_table.map_level,
        fields = {'maps.area_level'},
        display = h.tbl.display.factory.value{},
        order = 1901,
    },
    {
        arg = 'map_guild_character',
        header = i18n.item_table.map_guild_character,
        fields = {'maps.guild_character'},
        display = h.tbl.display.factory.value{},
        order = 1902,
        sort_type = 'text',
    },
    {
        arg = 'map_series',
        header = i18n.item_table.map_series,
        fields = {'maps.series'},
        display = h.tbl.display.factory.value{},
        order = 1903,
    },
    {
        arg = 'atlas_tier',
        header = i18n.item_table.atlas_tier,
        colspan = 5,
        fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
        display = h.tbl.display.factory.atlas_tier{is_level=false},
        order = 1904,
    },
    {
        arg = 'atlas_level',
        header = i18n.item_table.atlas_level,
        colspan = 5,
        fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
        display = h.tbl.display.factory.atlas_tier{is_level=true},
        order = 1905,
    },
    -- Hideout decorations 20xx
    {
        arg = {'doodad', 'variation_count'},
        header = i18n.item_table.variation_count,
        fields = {'hideout_doodads.variation_count'},
        display = h.tbl.display.factory.value{colour='mod'},
        order = 2000,
    },
    -- Corpse items 21xx
    {
        arg = {'corpse', 'monster_category'},
        header = i18n.item_table.monster_category,
        fields = {'corpse_items.monster_category', 'corpse_items.monster_category_html'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', m_game.constants.monster.categories[data['corpse_items.monster_category']].id)
                    :wikitext(data['corpse_items.monster_category_html'])
        end,
        order = 2100,
    },
    {
        arg = {'corpse', 'monster_abilities'},
        header = i18n.item_table.monster_abilities,
        fields = {'corpse_items.monster_abilities'},
        display = h.tbl.display.factory.value{colour='mod'},
        order = 2101,
        sort_type = 'text',
    },
    -- Seed data 91xx,
    {
        arg = {'seed', 'seed_type'},
        header = i18n.item_table.seed_type,
        fields = {'harvest_seeds.type', 'harvest_seeds.type_id'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('table-sort-value', data['harvest_seeds.type'])
                    :attr('class', 'tc -' .. data['harvest_seeds.type_id'])
                    :wikitext(data['harvest_seeds.type'])
        end,
        order = 9100,
    },
    {
        arg = {'seed', 'seed_tier'},
        header = i18n.item_table.seed_tier,
        fields = {'harvest_seeds.tier'},
        display = h.tbl.display.factory.value{},
        order = 9101,
    },
    {
        arg = {'seed', 'seed_growth_cycles'},
        header = i18n.item_table.seed_growth_cycles,
        fields = {'harvest_seeds.growth_cycles'},
        display = h.tbl.display.factory.value{},
        order = 9102,
    },
    {
        arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_primal_lifeforce_percentage'},
        header = i18n.item_table.seed_consumed_primal_lifeforce_percentage,
        fields = {'harvest_seeds.consumed_primal_lifeforce_percentage'},
        display = h.tbl.display.factory.value{color='primal'},
        order = 9110,
    },
    {
        arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_vivid_lifeforce_percentage'},
        header = i18n.item_table.seed_consumed_vivid_lifeforce_percentage,
        fields = {'harvest_seeds.consumed_vivid_lifeforce_percentage'},
        display = h.tbl.display.factory.value{color='vivid'},
        order = 9110,
    },
    {
        arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_wild_lifeforce_percentage'},
        header = i18n.item_table.seed_consumed_wild_lifeforce_percentage,
        fields = {'harvest_seeds.consumed_wild_lifeforce_percentage'},
        display = h.tbl.display.factory.value{color='wild'},
        order = 9110,
    },
    {
        arg = {'seed', 'seed_required_nearby_seeds', 'seed_required_nearby_seed_amount'},
        header = i18n.item_table.seed_required_nearby_seed_amount,
        fields = {'harvest_seeds.required_nearby_seed_amount'},
        display = h.tbl.display.factory.value{},
        order = 9113,
    },
    {
        arg = {'seed', 'seed_required_nearby_seeds', 'seed_required_nearby_seed_tier'},
        header = i18n.item_table.seed_required_nearby_seed_tier,
        fields = {'harvest_seeds.required_nearby_seed_tier'},
        display = h.tbl.display.factory.value{},
        order = 9114,
    },

    
    {
        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 = {'seed', 'seed_effect'},
        header = i18n.item_table.effects,
        fields = {'harvest_seeds.effect'},
        display = h.tbl.display.factory.value{colour='crafted'},
        order = 12003,
        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 = 12006,
        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 = 12008,
        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 = {'version', 'release_version'},
        header = i18n.item_table.release_version,
        fields = {'items.release_version'},
        display = function(tr, data)
            tr
                :tag('td')
                    :wikitext(
                        string.format(
                            i18n.item_table.version_link,
                            data['items.release_version'],
                            data['items.release_version']
                        )
                    )
        end,
        order = 15000,
    },
    {
        arg = {'version', 'removal_version'},
        header = i18n.item_table.removal_version,
        fields = {'items.removal_version'},
        display = function(tr, data)
            tr
                :tag('td')
                    :wikitext(
                        string.format(
                            i18n.item_table.version_link,
                            data['items.removal_version'],
                            data['items.removal_version']
                        )
                    )
        end,
        order = 15001,
    },
    {
        arg = {'drop', 'drop_enabled'},
        header = i18n.item_table.drop_enabled,
        fields = {'items.drop_enabled'},
        display = h.tbl.display.factory.value{},
        order = 15002,
    },
    {
        arg = {'drop', 'drop_areas'},
        header = i18n.item_table.drop_areas,
        fields = {'items.drop_areas_html'},
        display = h.tbl.display.factory.value{},
        order = 15004,
        sort_type = 'text',
    },
    {
        arg = {'drop', 'drop_monsters'},
        header = i18n.item_table.drop_monsters,
        fields = {'items.drop_monsters'},
        display = function(tr, data, na, results2)
            if results2['drop_monsters_query'] == nil then
                results2['drop_monsters_query'] = m_cargo.query(
                    {'items', 'monsters', 'main_pages'},
                    {
                        'items._pageName',
                        'monsters._pageName',
                        'monsters.name',
                        'main_pages._pageName',
                    },
                    {
                        join=[[
                            items.drop_monsters HOLDS monsters.metadata_id,
                            monsters.metadata_id=main_pages.id
                        ]],
                        where=string.format([[
                            items._pageID IN (%s)
                            AND monsters.metadata_id IS NOT NULL
                        ]],
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='items.drop_monsters',
                    }
                )

                results2['drop_monsters_query'] = m_cargo.map_results_to_id{
                    results=results2['drop_monsters_query'],
                    field='items._pageName',
                }
            end
            local results = results2['drop_monsters_query'][data['items._pageName']] or {}
            local tbl = {}
            for _,v in ipairs(results) do
                local page = v['main_pages._pageName'] or v['monsters._pageName'] or ''
                local name = v['monsters.name'] or v['items.drop_monsters'] or ''
                tbl[#tbl+1] = string.format('[[%s|%s]]', page, name)
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 15005,
        sort_type = 'text',
    },
    {
        arg = {'drop', 'drop_text'},
        header = i18n.item_table.drop_text,
        fields = {'items.drop_text'},
        display = h.tbl.display.factory.value{},
        order = 15006,
        sort_type = 'text',
    },
    {
        arg = {'quest'},
        header = i18n.item_table.quest_rewards,
        fields = {'items._pageName'},
        display = function(tr, data, na, results2)
            if results2['quest_query'] == nil then
                results2['quest_query'] = m_cargo.query(
                    {'items', 'quest_rewards'},
                    {
                        'quest_rewards._pageName',
                        'quest_rewards.classes',
                        'quest_rewards.act',
                        'quest_rewards.quest'
                    },
                    {
                        join='items._pageName=quest_rewards._pageName',
                        where=string.format(
                            'items._pageID IN (%s) AND quest_rewards._pageName IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='quest_rewards.act, quest_rewards.quest',
                    }
                )
                results2['quest_query'] = m_cargo.map_results_to_id{
                    results=results2['quest_query'],
                    field='quest_rewards._pageName',
                }
            end

            local results = results2['quest_query'][data['items._pageName']] or {}
            local tbl = {}
            for _, v in ipairs(results) do
                local classes = table.concat(m_util.string.split(v['quest_rewards.classes'] or '', ',%s*'), ', ')
                if classes == '' or classes == nil then
                    classes = i18n.item_table.quest_rewards_any_classes
                end

                tbl[#tbl+1] = string.format(
                    i18n.item_table.quest_rewards_row_format,
                    v['quest_rewards.act'],
                    v['quest_rewards.quest'],
                    classes
                )
            end

            local value = table.concat(tbl, '<br>')
            if value == nil or value == '' then
                tr:wikitext(m_util.html.td.na())
            else
                tr
                    :tag('td')
                        :attr('style', 'text-align:left')
                        :wikitext(value)
            end
        end,
        order = 16001,
        sort_type = 'text',
    },
    {
        arg = {'vendor'},
        header = i18n.item_table.vendor_rewards,
        fields = {'items._pageName'},
        display = function(tr, data, na, results2)
            if results2['vendor_query'] == nil then
                results2['vendor_query'] = m_cargo.query(
                    {'items', 'vendor_rewards'},
                    {
                        'vendor_rewards._pageName',
                        'vendor_rewards.classes',
                        'vendor_rewards.act',
                        'vendor_rewards.npc',
                        'vendor_rewards.quest',
                    },
                    {
                        join='items._pageName=vendor_rewards._pageName',
                        where=string.format(
                            'items._pageID IN (%s) AND vendor_rewards._pageName IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='vendor_rewards.act, vendor_rewards.quest',
                    }
                )
                results2['vendor_query'] = m_cargo.map_results_to_id{
                    results=results2['vendor_query'],
                    field='vendor_rewards._pageName',
                }
            end
            local results = results2['vendor_query'][data['items._pageName']] or {}

            local tbl = {}
            for _, v in ipairs(results) do
                local classes = table.concat(m_util.string.split(v['vendor_rewards.classes'] or '', ',%s*'), ', ')
                if classes == '' or classes == nil then
                    classes = i18n.item_table.vendor_rewards_any_classes
                end

                tbl[#tbl+1] = string.format(
                    i18n.item_table.vendor_rewards_row_format,
                    v['vendor_rewards.act'],
                    v['vendor_rewards.quest'],
                    v['vendor_rewards.npc'],
                    classes
                )
            end

            local value = table.concat(tbl, '<br>')
            if value == nil or value == '' then
                tr:wikitext(m_util.html.td.na())
            else
                tr
                    :tag('td')
                        :attr('style', 'text-align:left')
                        :wikitext(value)
            end
        end,
        order = 17001,
        sort_type = 'text',
    },
    {
        arg = {'price', 'purchase_cost'},
        header = i18n.item_table.purchase_costs,
        fields = {'item_purchase_costs.name', 'item_purchase_costs.amount'},
        display = function (tr, data)
            -- Can purchase costs have multiple currencies and rows?
            -- Just switch to the same method as in sell_price then.
            local tbl = {}
            if data['item_purchase_costs.name'] ~= nil then
                tbl[#tbl+1] = string.format(
                        '%sx %s',
                        data['item_purchase_costs.amount'],
                        h.item_link{data['item_purchase_costs.name']}
                    )
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 18001,
        sort_type = 'text',
    },
    {
        arg = {'price', 'sell_price'},
        header = i18n.item_table.sell_price,
        fields = {'item_sell_prices.name', 'item_sell_prices.amount'},
        display = function(tr, data, na, results2)
            if results2['sell_price_query'] == nil then
                results2['sell_price_query'] = m_cargo.query(
                    {'items', 'item_sell_prices'},
                    {
                        'item_sell_prices.name',
                        'item_sell_prices.amount',
                        'item_sell_prices._pageID'
                    },
                    {
                        join='items._pageID=item_sell_prices._pageID',
                        where=string.format(
                            'items._pageID IN (%s) AND item_sell_prices._pageID IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='item_sell_prices.name',
                    }
                )
                results2['sell_price_query'] = m_cargo.map_results_to_id{
                    results=results2['sell_price_query'],
                    field='item_sell_prices._pageID',
                }
            end
            local results = results2['sell_price_query'][data['items._pageID']] or {}

            local tbl = {}
            for _,v in ipairs(results) do
                tbl[#tbl+1] = string.format(
                    '%sx %s',
                    v['item_sell_prices.amount'],
                    h.item_link{v['item_sell_prices.name']}
                )
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 18002,
        sort_type = 'text',
    },
    {
        arg = {'boss', 'boss_name'},
        header = i18n.item_table.boss_name,
        fields = {'maps.area_id'},
        display = function(tr, data, na, results2)
            if results2['boss_query'] == nil then
                results2['boss_query'] = m_cargo.query(
                    {'items', 'maps', 'areas', 'monsters', 'main_pages'},
                    {
                        'items._pageName',
                        'maps.area_id',
                        'areas.id',
                        'areas.boss_monster_ids',
                        'monsters._pageName',
                        'monsters.name',
                        'main_pages._pageName',
                    },
                    {
                        join=[[
                            items._pageID=maps._pageID,
                            maps.area_id=areas.id,
                            areas.boss_monster_ids HOLDS monsters.metadata_id,
                            monsters.metadata_id=main_pages.id
                        ]],
                        where=string.format([[
                            items._pageID IN (%s)
                            AND maps.area_id IS NOT NULL
                            AND areas.boss_monster_ids HOLDS LIKE "%%"
                        ]],
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='areas.boss_monster_ids',
                    }
                )

                results2['boss_query'] = m_cargo.map_results_to_id{
                    results=results2['boss_query'],
                    field='items._pageName',
                }
            end
            local results = results2['boss_query'][data['items._pageName']] or {}
            local tbl = {}
            for _,v in ipairs(results) do
                local page = v['main_pages._pageName'] or v['monsters._pageName'] or ''
                local name = v['monsters.name'] or v['areas.boss_monster_ids'] or ''
                tbl[#tbl+1] = string.format('[[%s|%s]]', page, name)
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 19001,
        sort_type = 'text',
    },
    {
        arg = {'boss', 'boss_number'},
        header = i18n.item_table.boss_number,
        fields = {'maps.area_id'},
        display = function(tr, data, na, results2)
            if results2['boss_query'] == nil then
                results2['boss_query'] = m_cargo.query(
                    {'items', 'maps', 'areas', 'monsters', 'main_pages'},
                    {
                        'items._pageName',
                        'maps.area_id',
                        'areas.id',
                        'areas.boss_monster_ids',
                        'monsters._pageName',
                        'monsters.name',
                        'main_pages._pageName',
                    },
                    {
                        join=[[
                            items._pageID=maps._pageID,
                            maps.area_id=areas.id,
                            areas.boss_monster_ids HOLDS monsters.metadata_id,
                            monsters.metadata_id=main_pages.id
                        ]],
                        where=string.format([[
                            items._pageID IN (%s)
                            AND maps.area_id IS NOT NULL
                            AND areas.boss_monster_ids HOLDS LIKE "%%"
                        ]],
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='areas.boss_monster_ids',
                    }
                )

                results2['boss_query'] = m_cargo.map_results_to_id{
                    results=results2['boss_query'],
                    field='items._pageName',
                }
            end
            local results = results2['boss_query'][data['items._pageName']] or {}
            local tbl = {}
            for _,v in ipairs(results) do
                tbl[#tbl+1] = v['areas.boss_monster_ids']
            end
            h.na_or_val(tr, #tbl)
        end,
        order = 19002,
    },
    {
        arg = {'legacy'},
        header = i18n.item_table.legacy,
        fields = {'items.name'},
        display = function(tr, data, na, results2)
            if results2['legacy_query'] == nil then
                results2['legacy_query'] = m_cargo.query(
                    {'items', 'legacy_variants'},
                    {
                        'items._pageID',
                        'items._pageName',
                        'items.frame_type',
                        'legacy_variants.removal_version',
                        'legacy_variants.implicit_stat_text',
                        'legacy_variants.explicit_stat_text',
                        'legacy_variants.stat_text',
                        'legacy_variants.base_item',
                        'legacy_variants.required_level'
                    },
                    {
                        join='items._pageID=legacy_variants._pageID',
                        where='legacy_variants.removal_version IS NOT NULL',
                        where=string.format(
                            'items._pageID IN (%s) AND legacy_variants.removal_version IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                        orderBy='items._pageName',
                    }
                )

                results2['legacy_query'] = m_cargo.map_results_to_id{
                    results=results2['legacy_query'],
                    field='items._pageName',
                }
            end
            local results = results2['legacy_query'][data['items._pageName']] or {}

            local tbl = mw.html.create('table')
                :attr('width', '100%')
            for _, v in ipairs(results) do
                local cell = {}
                local l = {
                    'legacy_variants.base_item',
                    'legacy_variants.stat_text'
                }

                -- Clean up data:
                for _, k in ipairs(l) do
                    if v[k] ~= nil then
                        local s = m_util.string.split(v[k], '*')
                        local s_flt = {}
                        for _, sss in ipairs(s) do
                            if sss ~= nil and sss ~= '' then
                                s_flt[#s_flt+1] = string.gsub(sss, '\n', '')
                            end
                        end

                        cell[#cell+1] = table.concat(s_flt, '<br>')
                    end
                end

                local sep = string.format(
                    '<span class="item-stat-separator -%s"></span>',
                    v['items.frame_type']
                )
                tbl
                    :tag('tr')
                        :attr('class', 'upgraded-from-set')
                        :tag('td')
                            :wikitext(
                                v['legacy_variants.removal_version']
                            )
                            :done()
                        :tag('td')
                            :attr('class', 'group legacy-stats plainlist')
                            :wikitext(table.concat(cell, sep))
                            :done()
                    :done()
            end
            tr
                :tag('td')
                    :node(tbl)
        end,
        order = 21001,
        sort_type = 'text',
    },
    {
        arg = {'granted_skills'},
        header = i18n.item_table.granted_skills,
        fields = {'items.name'},
        display = function(tr, data, na, results2)
            if results2['granted_skills_query'] == nil then
                results2['granted_skills_query'] = m_cargo.query(
                    {'items', 'item_mods', 'mods', 'skill', 'items=items2'},
                    {
                        'items._pageName',
                        'items.name',
                        'item_mods.id',
                        'mods._pageName',
                        'mods.id',
                        'mods.granted_skill',
                        'mods.stat_text_raw',
                        'skill._pageName',
                        'skill.skill_id',
                        'skill.active_skill_name',
                        'skill.skill_icon',
                        'skill.stat_text',
                        'items2.class',
                        'items2.name',
                        'items2.inventory_icon',
                        'items2.size_x',
                        'items2.size_y',
                        'items2.html',
                    },
                    {
                        join='items._pageID=item_mods._pageID, item_mods.id=mods.id, mods.granted_skill=skill.skill_id, skill._pageID=items2._pageID',
                        where=string.format(
                            'items._pageID IN (%s) AND mods.granted_skill IS NOT NULL',
                            table.concat(results2.pageIDs, ', ')
                        ),
                    }
                )

                results2['granted_skills_query'] = m_cargo.map_results_to_id{
                    results=results2['granted_skills_query'],
                    field='items._pageName',
                }
            end
            local results = results2['granted_skills_query'][data['items._pageName']] or {}

            local tbl = {}
            for _, v in ipairs(results) do

                -- Check if a level for the skill is specified in the
                -- mod stat text.
                -- Stat ids have unreliable naming convention so using
                -- the mod stat text instead.
                local level = ''
                local stat_text = v['mods.stat_text_raw'] or ''
                local level_number = string.match(
                    stat_text:lower(),
                    h.string.format(
                        i18n.item_table.granted_skills_level_pattern,
                        {
                            granted_skills_level_label = i18n.item_table.granted_skills_level_label:lower()
                        }
                    )
                )

                -- If a level number was specified in the stat text
                -- then add it to the cell:
                if level_number then
                    level = h.string.format(
                        i18n.item_table.granted_skills_level_format,
                        {
                            granted_skills_level_label = i18n.item_table.granted_skills_level_label,
                            level_number = level_number,
                        }
                    )
                end

                -- Use different formats depending on if it's a gem or
                -- not:
                if v['items2.class'] == nil  then
                    tbl[#tbl+1] = h.string.format(
                        i18n.item_table.granted_skills_skill_output_format,
                        {
                            level = level,
                            sl = h.skill_link{
                                skip_query=true,
                                page = v['skill.active_skill_name']
                                    or v['skill._pageName']
                                    or v['mods._pageName']
                                    or '',
                                name = v['skill.active_skill_name']
                                    or v['skill.stat_text']
                                    or v['mods.granted_skill'],
                                icon = v['skill.skill_icon'],
                            },
                        }
                    )
                else
                    local il_args = {
                        skip_query=true,
                        page=v['items2._pageName'],
                        name=v['items2.name'],
                        inventory_icon=v['items2.inventory_icon'],
                        width=v['items2.size_x'],
                        height=v['items2.size_y'],
                    }

                    -- TODO: add in tpl_args.
                    if no_html == nil then
                        il_args.html = v['items2.html']
                    end
                    tbl[#tbl+1] = h.string.format(
                        i18n.item_table.granted_skills_gem_output_format,
                        {
                            level = level,
                            il = h.item_link(il_args),
                        }
                    )
                end
            end
            h.na_or_val(tr, table.concat(tbl, '<br>'))
        end,
        order = 22001,
        sort_type = 'text',
    },
    {
        arg = 'alternate_art',
        header = i18n.item_table.alternate_art,
        fields = {'items.alternate_art_inventory_icons'},
        display = function (tr, data)
            local alt_art = m_util.string.split(
                data['items.alternate_art_inventory_icons'],
                ','
            )

            -- TODO: Use il instead to handle size?
            -- local size = 39
            local out = {}
            for i,v in ipairs(alt_art) do
                out[#out+1] = string.format(
                    '[[%s|link=|%s]]',
                    v,
                    v
                )
            end

            tr
                :tag('td')
                    :wikitext(table.concat(out, ''))
        end,
        order = 23000,
        sort_type = 'text',
    },
}

data_map.skill_gem = {
    {
        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 = {'stat', 'stat_text'},
        header = i18n.item_table.stats,
        fields = {'skill.stat_text'},
        display = h.tbl.display.factory.value{},
        order = 2000,
        sort_type = 'text',
    },
    {
        arg = {'quality', 'quality_stat_text'},
        header = i18n.item_table.quality_stats,
        fields = {'skill.quality_stat_text'},
        display = h.tbl.display.factory.value{},
        order = 2001,
        sort_type = 'text',
    },
    {
        arg = 'description',
        header = i18n.item_table.description,
        fields = {'skill.description'},
        display = h.tbl.display.factory.value{},
        order = 2100,
        sort_type = 'text',
    },
    {
        arg = 'level',
        header = m_game.level_requirement.icon,
        fields = h.tbl.range_fields{field='items.required_level'},
        display = h.tbl.display.factory.range{field='items.required_level'},
        order = 3000,
    },
    {
        arg = 'str',
        header = '[[File:StrengthIcon small.png|link=|alt=Gem has strength requirement]]',
        fields = {'skill_gems.strength_percent'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill_gems.strength_percent'])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3001,
    },
    {
        arg = 'dex',
        header = '[[File:DexterityIcon small.png|link=|alt=Gem has dexterity requirement]]',
        fields = {'skill_gems.dexterity_percent'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill_gems.dexterity_percent'])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3002,
    },
    {
        arg = 'int',
        header = '[[File:IntelligenceIcon small.png|link=|alt=Gem has intelligence requirement]]',
        fields = {'skill_gems.intelligence_percent'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill_gems.intelligence_percent'])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3003,
    },
    {
        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%%',
                skill_levels = true,
            },
        }},
        order = 4000,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        arg = 'cast_time',
        header = i18n.item_table.cast_time,
        fields = {'skill.cast_time'},
        display = h.tbl.display.factory.value{options = {
        }},
        order = 4001,
        options = {
        },
    },
    {
        arg = {'aspd', 'attack_speed', 'attack_speed_multiplier'},
        header = i18n.item_table.attack_speed_multiplier,
        fields = {'skill_levels.attack_speed_multiplier'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
                skill_levels = true,
            },
        }},
        order = 4002,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        arg = 'dmgeff',
        header = i18n.item_table.damage_effectiveness,
        fields = {'skill_levels.damage_effectiveness'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
                skill_levels = true,
            },
        }},
        order = 4003,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        arg = {'mcm', 'cost_multiplier'},
        header = i18n.item_table.cost_multiplier,
        fields = {'skill_levels.cost_multiplier'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
                skill_levels = true,
            },
        }},
        order = 5000,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        arg = 'mana',
        header = i18n.item_table.mana_cost,
        fields = {'skill_levels.cost_amounts', 'skill_levels.mana_reservation_percent', 'skill_levels.mana_reservation_flat'},
        display = function (tr, data, fields, data2)
            local appendix = ''
            local cost_field = ''
            local sdata = data2.skill_levels[data['items._pageName']]
            -- Percentage Mana reservation is in level 0 of most of the skill_levels at this point, but spellslinger and awakened blasphemy change it per-level
            -- Flat Mana resservation is in real gem levels, but maybe there will be fixed flat mana reservations in the future.
            -- Per-use mana costs are stored in the real gem levels if they change, or in 0 if it's fixed.
            if(sdata['1']['skill_levels.mana_reservation_percent'] ~= nil or sdata['0']['skill_levels.mana_reservation_percent'] ~= nil) then
                cost_field = 'skill_levels.mana_reservation_percent'
                appendix = appendix .. '%%'
            elseif(sdata['1']['skill_levels.mana_reservation_flat'] ~= nil or sdata['0']['skill_levels.mana_reservation_flat'] ~= nil) then
                cost_field = 'skill_levels.mana_reservation_flat'
                appendix = appendix .. ' ' .. i18n.item_table.reserves_mana_suffix
            elseif(sdata['1']['skill_levels.cost_amounts'] ~= nil or sdata['0']['skill_levels.cost_amounts'] ~= nil) then
                cost_field = 'skill_levels.cost_amounts'
            end

            h.tbl.display.factory.value{options = {
                [1] = {
                    fmt='%d' .. appendix,
                    skill_levels = true,
                },
            }}(tr, data, {cost_field}, data2)
        end,
        order = 5001,
        -- Need one set of options per field.
        options = {
            [1] = {
                skill_levels = true,
            },
            [2] = {
                skill_levels = true,
            },
            [3] = {
                skill_levels = true,
            },
        },
    },
    {
        arg = 'vaal',
        header = i18n.item_table.vaal_souls_requirement,
        fields = {'skill_levels.vaal_souls_requirement'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                skill_levels = true,
            },
        }},
        order = 6000,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        arg = 'vaal',
        header = i18n.item_table.stored_uses,
        fields = {'skill_levels.vaal_stored_uses'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                skill_levels = true,
            },
        }},
        order = 6001,
        options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        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(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_description'}(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(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_secondary_description'}(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(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_tertiary_description'}(data['skill.radius_tertiary']))
        end,
        order = 7002,
    },
}

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

local function _item_table(args)
    --[[
    Creates a generic table for items.

    Examples
    --------
    = p.item_table{
        q_tables='vendor_rewards, quest_rewards, skill_gems',
        q_join='items._pageID=vendor_rewards._pageID, items._pageID=quest_rewards._pageID, items._pageID=skill_gems._pageID',
        q_where='(items.class="Active Skill Gems" OR items.class = "Support Skill Gems") AND (vendor_rewards.quest_id IS NOT NULL OR quest_rewards.quest_id IS NOT NULL)',
        vendor=1,
    }
    ]]

    m_item_util = m_item_util or require('Module:Item util')

    local t = os.clock()

    args.mode = args.mode or 'item'
    local modes = {
        skill = {
            data = data_map.skill_gem,
            header = i18n.item_table.skill_gem,
        },
        item = {
            data = data_map.generic_item,
            header = i18n.item_table.item,
        },
    }
    if modes[args.mode] == nil then
        error(i18n.errors.invalid_item_table_mode)
    end

    local results2 = {
        stats = {},
        skill_levels = {},
        pageIDs = {},
    }

    local row_infos = {}
    for _, row_info in ipairs(modes[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(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(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 = {}
    for i=1, math.huge do -- repeat until no more columns are found
        local prefix = string.format('stat_column%s_', i)
        if args[prefix .. 'stat1_id'] == nil then
            -- Each column requires at least one stat id
            break
        end
        local col_info = {
            header = args[prefix .. 'header'] or tostring(i),
            format = args[prefix .. 'format'],
            stat_format = args[prefix .. 'stat_format'] or 'separate',
            order = tonumber(args[prefix .. 'order']) or (10000000 + i),
            stats = {},
        }
        for j=1, math.huge do
            local stat_id = args[string.format('%sstat%s_id', prefix, j)]
            if stat_id == nil then
                break
            end
            table.insert(col_info.stats, {id=stat_id})
            query_stats[stat_id] = true
        end
        table.insert(stat_columns, col_info)
    end

    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 = (results2.stats[data['items._pageName']] or {})[stat_info.id]
                        if stat ~= nil then
                            stat_texts[#stat_texts+1] = m_util.html.format_value(args, 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 = (results2.stats[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, m_util.html.format_value(args, 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(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

    -- Namespace condition
    -- This is mainly to prevent items from user pages or other testing pages 
    -- from being returned in the query results.
    if args.namespaces ~= 'any' then
        local namespaces = m_util.cast.table(args.namespaces, {callback=m_util.cast.number})
        if #namespaces > 0 then
            namespaces = table.concat(namespaces, ',')
        else
            namespaces = m_item_util.get_item_namespaces{format = 'list'}
        end
        query.where = string.format('(%s) AND items._pageNamespace IN (%s)', query.where, namespaces)
    end

    local skill_levels = {}
    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 {}
            if rowinfo.options[index].skill_levels then
                skill_levels[#skill_levels+1] = field
            else
                fields[#fields+1] = field
                tables_assoc[m_util.string.split(field, '%.')[1]] = true
            end
        end
    end

    if #skill_levels > 0 then
        fields[#fields+1] = 'skill.max_level'
        tables_assoc.skill = true
    end

    -- Reformat the tables and fields so they can be retrieved correctly:
    local tables = {}
    for table_name,_ in pairs(tables_assoc) do
        tables[#tables+1] = table_name
    end
    local tbls = table.concat(tables,',') .. (query.tables or '')
    query.tables = m_util.string.split(tbls, ',')

    for index, field in ipairs(fields) do
        fields[index] = string.format('%s=%s', field, field)
    end
    query.fields = fields

    -- Take care of the minimum required joins, joins from templates
    -- must still be userdefined:
    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

    -- Needed to eliminate duplicates supplied via table joins:
    query.groupBy = 'items._pageID' .. (query.groupBy or '')

    -- Query results:
    local results = m_cargo.query(query.tables, query.fields, query)

    if #results == 0 and args.default ~= nil then
        return args.default
    end

    if #results > 0 then
        -- Create a list of found pageIDs for column specific queries:
        for _,v in ipairs(results) do
            results2.pageIDs[#results2.pageIDs+1] = v['items._pageID']
        end

        -- fetch skill level information
        if #skill_levels > 0 then
            skill_levels[#skill_levels+1] = 'skill_levels._pageName'
            skill_levels[#skill_levels+1] = 'skill_levels.level'
            local pages = {}
            for _, row in ipairs(results) do
                pages[#pages+1] = string.format('(skill_levels._pageID="%s" AND skill_levels.level IN (0, 1, %s))', row['items._pageID'], row['skill.max_level'])
            end
            local temp = m_cargo.query(
                {'skill_levels'},
                skill_levels,
                {
                    where=table.concat(pages, ' OR '),
                    groupBy='skill_levels._pageID, skill_levels.level',
                }
            )
            -- map to results
            for _, row in ipairs(temp) do
                if results2.skill_levels[row['skill_levels._pageName']] == nil then
                   results2.skill_levels[row['skill_levels._pageName']] = {}
                end
                -- TODO: convert to int?
                results2.skill_levels[row['skill_levels._pageName']][row['skill_levels.level']] = row
            end
        end

        if #stat_columns > 0 then
            local stat_results = m_cargo.query(
                {'items', 'item_stats'},
                {'item_stats._pageName', 'item_stats.id', 'item_stats.min', 'item_stats.max', 'item_stats.avg'},
                {
                    join = 'items._pageID=item_stats._pageID',
                    where = string.format(
                        'item_stats._pageID IN (%s) AND item_stats.id IN ("%s")',
                        table.concat(m_util.table.column(results, 'items._pageID'), ','),
                        table.concat(m_util.table.keys(query_stats), '","')
                    ),
                    groupBy = 'items._pageID, item_stats.id',
                }
            )
            for _, row in ipairs(stat_results) do
                local stat = {
                    min = tonumber(row['item_stats.min']),
                    max = tonumber(row['item_stats.max']),
                    avg = tonumber(row['item_stats.avg']),
                }

                if results2.stats[row['item_stats._pageName']] == nil then
                    results2.stats[row['item_stats._pageName']] = {[row['item_stats.id']] = stat}
                else
                    results2.stats[row['item_stats._pageName']][row['item_stats.id']] = stat
                end
            end
        end
    end


    --
    -- Display the table
    --

    local tbl = mw.html.create('table')
    tbl:attr('class', 'wikitable sortable item-table')
    if m_util.cast.boolean(args.responsive) then
        tbl:addClass('responsive-table')
    end

    -- Headers:
    local tr = tbl:tag('tr')
    tr
        :tag('th')
            :wikitext(modes[args.mode].header)
            :done()
    for _, row_info in ipairs(row_infos) do
        local th = tr:tag('th')

        if row_info.colspan then
            th:attr('colspan', row_info.colspan)
        end

        th
            :attr('data-sort-type', row_info.sort_type or 'number')
            :wikitext(row_info.header)
    end

    -- Rows:
    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'],
            width=row['items.size_x'],
            height=row['items.size_y'],
        }

        if args.no_html == nil then
            il_args.html = row['items.html']
        end

        if args.large then
            il_args.large = args.large
        end

        tr
            :tag('td')
                :wikitext(h.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] == nil or row[field] == '' then
                    local opts = rowinfo.options[index]
                    if opts.optional ~= true and opts.skill_levels ~= true then
                        display = false
                        break
                    else
                        row[field] = nil
                    end
                end
            end
            if display then
                rowinfo.display(tr, row, rowinfo.fields, results2)
            else
                tr:wikitext(m_util.html.td.na())
            end
        end
    end

    local cats = {}
    if #results == query.limit then
        cats[#cats+1] = i18n.categories.query_limit
    end

    if #results == 0 then
        cats[#cats+1] = i18n.categories.no_results
    end

    mw.logObject({os.clock() - t, query})

    return tostring(tbl) .. m_util.misc.add_category(cats, {ignore_blacklist=args.debug})
end


-------------------------------------------------------------------------------
-- Map item drops
-------------------------------------------------------------------------------

local function _map_item_drops(args)
    --[[
    Gets the area id from the map item and activates
    Template:Area_item_drops.

    Examples:
    = p.map_item_drops{page='Underground River Map (War for the Atlas)'}
    ]]

    local tables = {'maps'}
    local fields = {
        'maps.area_id',
    }
    local query = {
        -- Only need each page name once
        groupBy = 'maps._pageName',
    }
    if args.page then
        -- Join with _pageData in order to check for page redirect
        tables[#tables+1] = '_pageData'
        fields[#fields+1] = '_pageData._pageNameOrRedirect'
        query.where = string.format(
            '_pageData._pageName="%s"',
            args.page
        )
        query.join = 'maps._pageName = _pageData._pageNameOrRedirect'
    else
        query.where = string.format(
            'maps._pageName="%s"',
            tostring(mw.title.getCurrentTitle())
        )
    end
    query.where = query.where .. ' AND maps.area_id > ""' -- area_id must not be empty or null
    local results = m_cargo.query(tables, fields, query)
    local id = ''
    if #results > 0 then
        id = results[1]['maps.area_id']
    end
    return mw.getCurrentFrame():expandTemplate{ title = 'Area item drops', args = {area_id=id} }
end

-------------------------------------------------------------------------------
-- Prophecy description
-------------------------------------------------------------------------------

local function _prophecy_description(args)
    args.page = args.page or tostring(mw.title.getCurrentTitle())

    local results = m_cargo.query(
        {'prophecies'},
        {'prophecies.objective', 'prophecies.reward'},
        {
            where=string.format('prophecies._pageName="%s"', args.page),
            -- Only need each page name once
            groupBy='prophecies._pageName',
        }
    )

    results = results[1]

    local out = {}

    if results['prophecies.objective'] then
        out[#out+1] = string.format('<h2>%s</h2>', i18n.prophecy_description.objective)
        out[#out+1] = results['prophecies.objective']
    end

    if results['prophecies.reward'] then
        out[#out+1] = string.format('<h2>%s</h2>', i18n.prophecy_description.reward)
        out[#out+1] = results['prophecies.reward']
    end

    return table.concat(out, '\n')
end


local function _simple_item_list(args)
    --[[
    Creates a simple list of items.

    Examples
    --------
    = p.simple_item_list{
        q_tables='maps',
        q_join='items._pageID=maps._pageID',
        q_where='maps.tier=1 AND items.drop_enabled=1 AND items.rarity_id="normal"',
        no_html=1,
        link_from_name=1,
    }
    ]]

    local query = {}
    for key, value in pairs(args) do
        if string.sub(key, 0, 2) == 'q_' then
            query[string.sub(key, 3)] = value
        end
    end

    local fields = {
        'items._pageName',
        'items.name',
        'items.class',
    }

    if args.no_icon == nil then
        fields[#fields+1] = 'items.inventory_icon'
    end

    if args.no_html == nil then
        fields[#fields+1] = 'items.html'
    end

    local tables = m_util.cast.table(args.q_tables)
    table.insert(tables, 1, 'items')

    query.groupBy = query.groupBy or 'items._pageID'

    local results = m_cargo.query(
        tables,
        fields,
        query
    )

    local out = {}
    for _, row in ipairs(results) do
        local page
        if args.use_name_as_link ~= nil then
            page = row['items.name']
        else
            page = row['items._pageName']
        end

        local link = h.item_link{
            page=page,
            name=row['items.name'],
            inventory_icon=row['items.inventory_icon'] or '',
            html=row['items.html'] or '',
            skip_query=true
        }

        if args.format == nil then
            out[#out+1] = string.format('* %s', link)
        elseif args.format == 'none' then
            out[#out+1] = link
        elseif args.format == 'li' then
            out[#out+1] = string.format('<li>%s</li>', link)
        else
            error(string.format(i18n.errors.generic_argument_parameter, 'format', args.format))
        end
    end

    if args.format == nil then
        return table.concat(out, '\n')
    elseif args.format == 'none' then
        return table.concat(out, '\n')
    elseif args.format == 'li' then
        return table.concat(out)
    end
end

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

local p = {}

--
-- Template:Item table
--
p.item_table = m_util.misc.invoker_factory(_item_table)

--
-- Template:Map item drops
--
p.map_item_drops = m_util.misc.invoker_factory(_map_item_drops, {
    wrappers = cfg.wrappers.map_item_drops,
})

--
-- Template:Prophecy description
--
p.prophecy_description = m_util.misc.invoker_factory(_prophecy_description, {
    wrappers = cfg.wrappers.prophecy_description,
})

--
-- Template:Simple item list
--
p.simple_item_list = m_util.misc.invoker_factory(_simple_item_list, {
    wrappers = cfg.wrappers.simple_item_list,
})

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

function p.debug._tbl_data(tbl)
    keys = {}
    for _, data in ipairs(tbl) 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)
end

return p