Ir para o conteúdo

Módulo:Item table: mudanças entre as edições

De DropWiki PoE 2
Sem resumo de edição
Jhon (discussão | contribs)
Sem resumo de edição
 
(27 revisões intermediárias por 2 usuários não estão sendo mostradas)
Linha 8: Linha 8:


_pageName='{{#replace:{{PAGENAME}}|'|\'}}'
_pageName='{{#replace:{{PAGENAME}}|'|\'}}'


require('Module:No globals')
require('Module:No globals')
Linha 151: Linha 150:
         tr
         tr
             :tag('td')
             :tag('td')
                 :attr('data-sort-value', data[string.format('%s_range_maximum', args.field)] or '0')
                --: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'))
                --:attr('class', 'tc -' .. (data[string.format('%s_range_colour', args.field)] or 'default'))
                 :wikitext(data[string.format('%s_range_text', args.field)])
                --:wikitext(data[string.format('%s_range_text', args.field)])
                 :attr('data-sort-value', data[string.format('%s_range_maximum', args.field:gsub('%.', '_'))] or '0')
                 :attr('class', 'tc -' .. (data[string.format('%s_range_colour', args.field:gsub('%.', '_'))] or 'default'))
                 :wikitext(data[string.format('%s_range_text', args.field:gsub('%.', '_'))])
                 :done()
                 :done()
     end
     end
Linha 307: Linha 309:
         args = {'class'},
         args = {'class'},
         header = i18n.item_table.item_class,
         header = i18n.item_table.item_class,
         fields = {'class'},
         fields = {'items.class'},
         display = h.display_value_factory{
         display = h.display_value_factory{
             fmt_options = {
             fmt_options = {
Linha 321: Linha 323:
         args = {'rarity'},
         args = {'rarity'},
         header = i18n.item_table.rarity,
         header = i18n.item_table.rarity,
         fields = {'rarity'},
         fields = {'items.rarity'},
         display = h.display_value_factory{},
         display = h.display_value_factory{},
     },
     },
Linha 328: Linha 330:
         args = {'rarity_id'},
         args = {'rarity_id'},
         header = i18n.item_table.rarity_id,
         header = i18n.item_table.rarity_id,
         fields = {'rarity_id'},
         fields = {'items.rarity_id'},
         display = h.display_value_factory{},
         display = h.display_value_factory{},
     },
     },
Linha 335: Linha 337:
         args = {'metadata_id'},
         args = {'metadata_id'},
         header = i18n.item_table.metadata_id,
         header = i18n.item_table.metadata_id,
         fields = {'metadata_id'},
         fields = {'items.metadata_id'},
         display = h.display_value_factory{},
         display = h.display_value_factory{},
     },
     },
Linha 383: Linha 385:
             i18n.item_table.required_level
             i18n.item_table.required_level
         ),
         ),
         fields = h.range_fields_factory{fields={'required_level'}},
         fields = h.range_fields_factory{fields={'items.required_level'}},
         display = h.display_range_factory{field='required_level'},
         display = h.display_range_factory{field='items.required_level'},
     },
     },
     {
     {
Linha 397: Linha 399:
             i18n.item_table.required_str
             i18n.item_table.required_str
         ),
         ),
         fields = h.range_fields_factory{fields={'required_strength'}},
         fields = h.range_fields_factory{fields={'items.required_strength'}},
         display = h.display_range_factory{field='required_strength'},
         display = h.display_range_factory{field='items.required_strength'},
     },
     },
     {
     {
Linha 411: Linha 413:
             i18n.item_table.required_dex
             i18n.item_table.required_dex
         ),
         ),
         fields = h.range_fields_factory{fields={'equired_dexterity'}},
         fields = h.range_fields_factory{fields={'items.required_dexterity'}},
         display = h.display_range_factory{field='equired_dexterity'},
         display = h.display_range_factory{field='items.required_dexterity'},
     },
     },
     {
     {
Linha 425: Linha 427:
             i18n.item_table.required_int
             i18n.item_table.required_int
         ),
         ),
         fields = h.range_fields_factory{fields={'required_intelligence'}},
         fields = h.range_fields_factory{fields={'items.required_intelligence'}},
         display = h.display_range_factory{field='required_intelligence'},
         display = h.display_range_factory{field='items.required_intelligence'},
     },
     },
     -- Armour 15xx
     -- Armour 15xx
Linha 800: Linha 802:
         args = {'stat'},
         args = {'stat'},
         header = i18n.item_table.stats,
         header = i18n.item_table.stats,
         fields = {'stat_text'},
         fields = {'items.stat_text'},
         display = h.display_value_factory{
         display = h.display_value_factory{
             color = 'mod',
             color = 'mod',
Linha 810: Linha 812:
         args = {'description'},
         args = {'description'},
         header = i18n.item_table.description,
         header = i18n.item_table.description,
         fields = {'description'},
         fields = {'items.description'},
         display = h.display_value_factory{
         display = h.display_value_factory{
             color = 'mod',
             color = 'mod',
Linha 830: Linha 832:
         args = {'flavour_text'},
         args = {'flavour_text'},
         header = i18n.item_table.flavour_text,
         header = i18n.item_table.flavour_text,
         fields = {'flavour_text'},
         fields = {'items.flavour_text'},
         display = h.display_value_factory{
         display = h.display_value_factory{
             color = 'flavour',
             color = 'flavour',
Linha 840: Linha 842:
         args = {'help_text'},
         args = {'help_text'},
         header = i18n.item_table.help_text,
         header = i18n.item_table.help_text,
         fields = {'help_text'},
         fields = {'items.help_text'},
         display = h.display_value_factory{
         display = h.display_value_factory{
             color = 'help',
             color = 'help',
Linha 897: Linha 899:
         args = {'version', 'release_version'},
         args = {'version', 'release_version'},
         header = i18n.item_table.release_version,
         header = i18n.item_table.release_version,
         fields = {'release_version'},
         fields = {'items.release_version'},
         display = function(tr, data)
         display = function(tr, data)
             tr
             tr
Linha 914: Linha 916:
         args = {'drop', 'drop_level'},
         args = {'drop', 'drop_level'},
         header = i18n.item_table.drop_level,
         header = i18n.item_table.drop_level,
         fields = {'drop_level'},
         fields = {'items.drop_level'},
         display = h.display_value_factory{},
         display = h.display_value_factory{},
     },
     },
Linha 921: Linha 923:
         args = {'drop_level_maximum'},
         args = {'drop_level_maximum'},
         header = i18n.item_table.drop_level_maximum,
         header = i18n.item_table.drop_level_maximum,
         fields = {'drop_level_maximum'},
         fields = {'items.drop_level_maximum'},
         display = h.display_value_factory{},
         display = h.display_value_factory{},
     },
     },
Linha 928: Linha 930:
         args = {'version', 'removal_version'},
         args = {'version', 'removal_version'},
         header = i18n.item_table.removal_version,
         header = i18n.item_table.removal_version,
         fields = {'removal_version'},
         fields = {'items.removal_version'},
         display = function(tr, data)
         display = function(tr, data)
             tr
             tr
Linha 948: Linha 950:
         display = h.display_value_factory{},
         display = h.display_value_factory{},
         display = h.display_yesno_factory{
         display = h.display_yesno_factory{
             field = 'drop_enabled',
             field = 'items.drop_enabled',
         },
         },
         sort_type = 'text',
         sort_type = 'text',
Linha 956: Linha 958:
         args = {'drop', 'drop_areas'},
         args = {'drop', 'drop_areas'},
         header = i18n.item_table.drop_areas,
         header = i18n.item_table.drop_areas,
         fields = {'drop_areas_html'},
         fields = {'items.drop_areas_html'},
         display = h.display_value_factory{},
         display = h.display_value_factory{},
         sort_type = 'text',
         sort_type = 'text',
Linha 1 930: Linha 1 932:
         end
         end
     end
     end
 
   
     -- Parse stat arguments
     -- Parse stat arguments
     local stat_columns = {}
     local stat_columns = {}
Linha 2 032: Linha 2 034:
     local tables = {'items'}
     local tables = {'items'}
     local fields = {
     local fields = {
         '_pageID',
         'items._pageName=_pageName',
         '_pageName',
         'items._pageID=_pageID',
         'name',
         'items.name=items_name',
         'inventory_icon',
         'items.inventory_icon=items_inventory_icon',
         'html',
         'items.html=items_html',
         'size_x',
         'items.size_x=items_size_x',
         'size_y',
         'items.size_y=items_size_y',
        'items.required_level_range_maximum=items_required_level_range_maximum',
        'items.required_level_range_colour=items_required_level_range_colour',
        'items.required_level_range_text=items_required_level_range_text',
        'items.stat_text=items_stat_text',
     }
     }
     local query = {
     local query = {
         where = args.where,
         where = args.where,
         groupBy = table.concat({'_pageID', args.groupBy}, ', '),
         groupBy = table.concat({'items._pageID', args.groupBy}, ', '),
         having = args.having,
         having = args.having,
         orderBy = args.orderBy,
         orderBy = args.orderBy,
Linha 2 106: Linha 2 112:
     -- Query results
     -- Query results
     local results = m_cargo.query(tables, fields, query)
     local results = m_cargo.query(tables, fields, query)
    for _, row in ipairs(results) do
        for k, v in pairs(row) do
            if not row[k:gsub('%.', '_')] then
                row[k:gsub('%.', '_')] = v
            end
        end
    end
    for _, row in ipairs(results) do
        for k, v in pairs(row) do
            local with_dot = k:gsub('_', '.')
            if not row[with_dot] then
                row[with_dot] = v
            end
        end
    end


     if #results == 0 and args.default ~= nil then
     if #results == 0 and args.default ~= nil then
Linha 2 116: Linha 2 137:
             results2.pageIDs[#results2.pageIDs+1] = v['items._pageID']
             results2.pageIDs[#results2.pageIDs+1] = v['items._pageID']
         end
         end
         -- fetch skill level information
         -- fetch skill level information
         if #skill_levels > 0 then
         if #skill_levels > 0 then
Linha 2 233: Linha 2 253:
             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] == nil then
                    row[field] = row[field:gsub('%.', '_')]
                end
                 if row[field] == nil or row[field] == '' then
                 if row[field] == nil or row[field] == '' then
                     local options = rowinfo.field_options[index]
                     local options = rowinfo.field_options[index]
Linha 2 280: Linha 2 303:
     local tables = {'maps'}
     local tables = {'maps'}
     local fields = {
     local fields = {
         'area_id',
         'maps.area_id',
     }
     }
     local query = {
     local query = {
Linha 2 368: Linha 2 391:


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


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


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



Edição atual tal como às 00h40min de 30 de março de 2025


The item module provides functionality for creating item tables.

Implemented templates

This module implements the following templates:


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

_pageName='{{#replace:{{PAGENAME}}|'|\'}}'

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.range_fields_factory(args)
    -- Returns a function that gets the range fields for the given keys
    local suffixes = {'maximum', 'text', 'colour'}
    if args.full then
        suffixes[#suffixes+1] = 'minimum'
    end
    return function ()
        local fields = {}
        for _, field in ipairs(args.fields) do
            for _, suffix in ipairs(suffixes) do
                fields[#fields+1] = string.format('%s_range_%s', field, suffix)
            end
        end
        return fields
    end
end

function h.display_value_factory(args)
    args.fmt_options = args.fmt_options or {}
    return function(tr, data, fields, data2)
        local values = {}
        local fmt_values = {}
        for index, field in ipairs(fields) do
            local value = {
                min=data[field],
                max=data[field],
                base=data[field],
            }
            local sdata = data2 and data2.skill_levels[data['items._pageName']]
            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 options = args.fmt_options[index] or {}
                -- global color is set, no overrides
                if args.color ~= nil then
                    options.color = false
                end
                fmt_values[#fmt_values+1] = m_util.html.format_value(nil, value, options)
            end
        end
        if #values == 0 then
            tr:node(m_util.html.table_cell('na'))
        else
            local td = tr:tag('td')
            td
                :attr('data-sort-value', table.concat(values, ', '))
                :wikitext(table.concat(fmt_values, ', '))
            if args.color then
                td:attr('class', 'tc -' .. args.color)
            end
        end
    end
end

function h.display_range_factory(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)])
                :attr('data-sort-value', data[string.format('%s_range_maximum', args.field:gsub('%.', '_'))] or '0')
                :attr('class', 'tc -' .. (data[string.format('%s_range_colour', args.field:gsub('%.', '_'))] or 'default'))
                :wikitext(data[string.format('%s_range_text', args.field:gsub('%.', '_'))])
                :done()
    end
end

function h.display_range_composite_factory(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.display_descriptor_value_factory(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.display_atlas_tier_factory(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

function h.display_yesno_factory(args)
    -- args:
    --  field
    --  condition
    args = args or {}
    args.condition = args.condition or function (value)
        return m_util.cast.boolean(value, {cast_nil=false})
    end
    return function (tr, data)
        local type = args.condition(data[args.field]) and 'yes' or 'no'
        tr:node(m_util.html.table_cell(type))
    end
end

function h.value_greater_than_zero(value)
    value = m_util.cast.number(value, {default=0})
    if value > 0 then
        return true
    end
    return false
end

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

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

local data_map = {}

-- for sort type see:
-- https://meta.wikimedia.org/wiki/Help:Sorting
data_map.generic_item = {
    {
        order = 1000,
        args = {'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,
        sort_type = 'text',
    },
    {
        order = 1001,
        args = {'class'},
        header = i18n.item_table.item_class,
        fields = {'items.class'},
        display = h.display_value_factory{
            fmt_options = {
                [1] = {
                    fmt = '[[%s]]',
                },
            },
        },
        sort_type = 'text',
    },
    {
        order = 1002,
        args = {'rarity'},
        header = i18n.item_table.rarity,
        fields = {'items.rarity'},
        display = h.display_value_factory{},
    },
    {
        order = 1003,
        args = {'rarity_id'},
        header = i18n.item_table.rarity_id,
        fields = {'items.rarity_id'},
        display = h.display_value_factory{},
    },
    {
        order = 1004,
        args = {'metadata_id'},
        header = i18n.item_table.metadata_id,
        fields = {'items.metadata_id'},
        display = h.display_value_factory{},
    },
    {
        order = 1005,
        args = {'size'},
        header = i18n.item_table.inventory_size,
        fields = {'items.size_x', 'items.size_y'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['items.size_x'] * data['items.size_y'])
                    :wikitext(string.format('%s×%s', data['items.size_x'], data['items.size_y']))
        end,
    },
    {
        order = 1100,
        args = {'essence'},
        header = i18n.item_table.essence_level,
        fields = {'essences.level'},
        display = h.display_value_factory{},
    },
    {
        order = 1300,
        args = {'stack_size'},
        header = i18n.item_table.stack_size,
        fields = {'stackables.stack_size'},
        display = h.display_value_factory{},
    },
    {
        order = 1301,
        args = {'stack_size_currency_tab'},
        header = i18n.item_table.stack_size_currency_tab,
        fields = {'stackables.stack_size_currency_tab'},
        display = h.display_value_factory{},
    },
    -- Requirements
    {
        order = 1400,
        args = {'level'},
        header = m_util.html.abbr(
            string.format(
                '[[%s|link=|alt=%s]]',
                i18n.item_table.level_icon,
                i18n.item_table.required_level
            ),
            i18n.item_table.required_level
        ),
        fields = h.range_fields_factory{fields={'items.required_level'}},
        display = h.display_range_factory{field='items.required_level'},
    },
    {
        order = 1401,
        args = {'str'},
        header = m_util.html.abbr(
            string.format(
                '[[%s|link=|alt=%s]]',
                i18n.item_table.str_icon,
                i18n.item_table.required_str
            ),
            i18n.item_table.required_str
        ),
        fields = h.range_fields_factory{fields={'items.required_strength'}},
        display = h.display_range_factory{field='items.required_strength'},
    },
    {
        order = 1402,
        args = {'dex'},
        header = m_util.html.abbr(
            string.format(
                '[[%s|link=|alt=%s]]',
                i18n.item_table.dex_icon,
                i18n.item_table.required_dex
            ),
            i18n.item_table.required_dex
        ),
        fields = h.range_fields_factory{fields={'items.required_dexterity'}},
        display = h.display_range_factory{field='items.required_dexterity'},
    },
    {
        order = 1403,
        args = {'int'},
        header = m_util.html.abbr(
            string.format(
                '[[%s|link=|alt=%s]]',
                i18n.item_table.int_icon,
                i18n.item_table.required_int
            ),
            i18n.item_table.required_int
        ),
        fields = h.range_fields_factory{fields={'items.required_intelligence'}},
        display = h.display_range_factory{field='items.required_intelligence'},
    },
    -- Armour 15xx
    {
        order = 1500,
        args = {'ar'},
        header = i18n.item_table.armour,
        fields = h.range_fields_factory{fields={'armours.armour'}},
        display = h.display_range_factory{field='armours.armour'},
    },
    {
        order = 1501,
        args = {'ev'},
        header =i18n.item_table.evasion,
        fields = h.range_fields_factory{fields={'armours.evasion'}},
        display = h.display_range_factory{field='armours.evasion'},
    },
    {
        order = 1502,
        args = {'es'},
        header = i18n.item_table.energy_shield,
        fields = h.range_fields_factory{fields={'armours.energy_shield'}},
        display = h.display_range_factory{field='armours.energy_shield'},
    },
    {
        order = 1503,
        args = {'wd'},
        header = i18n.item_table.ward,
        fields = h.range_fields_factory{fields={'armours.ward'}},
        display = h.display_range_factory{field='armours.ward'},
    },
    {
        order = 1504,
        args = {'block'},
        header = i18n.item_table.block,
        fields = h.range_fields_factory{fields={'shields.block'}},
        display = h.display_range_factory{field='shields.block'},
    },
    -- Weapons 16xx
    {
        order = 1600,
        args = {'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 = 1601,
        args = {'weapon', 'aps'},
        header = i18n.item_table.attacks_per_second,
        fields = h.range_fields_factory{fields={'weapons.attack_speed'}},
        display = h.display_range_factory{field='weapons.attack_speed'},
    },
    {
        order = 1602,
        args = {'weapon', 'crit'},
        header = i18n.item_table.local_critical_strike_chance,
        fields = h.range_fields_factory{fields={'weapons.critical_strike_chance'}},
        display = h.display_range_factory{field='weapons.critical_strike_chance'},
    },
    {
        order = 1603,
        args = {'physical_dps'},
        header = i18n.item_table.physical_dps,
        fields = h.range_fields_factory{fields={'weapons.physical_dps'}},
        display = h.display_range_factory{field='weapons.physical_dps'},
    },
    {
        order = 1604,
        args = {'lightning_dps'},
        header = i18n.item_table.lightning_dps,
        fields = h.range_fields_factory{fields={'weapons.lightning_dps'}},
        display = h.display_range_factory{field='weapons.lightning_dps'},
    },
    {
        order = 1605,
        args = {'cold_dps'},
        header = i18n.item_table.cold_dps,
        fields = h.range_fields_factory{fields={'weapons.cold_dps'}},
        display = h.display_range_factory{field='weapons.cold_dps'},
    },
    {
        order = 1606,
        args = {'fire_dps'},
        header = i18n.item_table.fire_dps,
        fields = h.range_fields_factory{fields={'weapons.fire_dps'}},
        display = h.display_range_factory{field='weapons.fire_dps'},
    },
    {
        order = 1607,
        args = {'chaos_dps'},
        header = i18n.item_table.chaos_dps,
        fields = h.range_fields_factory{fields={'weapons.chaos_dps'}},
        display = h.display_range_factory{field='weapons.chaos_dps'},
    },
    {
        order = 1608,
        args = {'elemental_dps'},
        header = i18n.item_table.elemental_dps,
        fields = h.range_fields_factory{fields={'weapons.elemental_dps'}},
        display = h.display_range_factory{field='weapons.elemental_dps'},
    },
    {
        order = 1609,
        args = {'poison_dps'},
        header = i18n.item_table.poison_dps,
        fields = h.range_fields_factory{fields={'weapons.poison_dps'}},
        display = h.display_range_factory{field='weapons.poison_dps'},
    },
    {
        order = 1610,
        args = {'dps'},
        header = i18n.item_table.dps,
        fields = h.range_fields_factory{fields={'weapons.dps'}},
        display = h.display_range_factory{field='weapons.dps'},
    },
    -- Flasks 17xx
    {
        order = 1700,
        args = {'flask_life'},
        header = i18n.item_table.flask_life,
        fields = h.range_fields_factory{fields={'flasks.life'}},
        display = h.display_range_factory{field='flasks.life'},
    },
    {
        order = 1701,
        args = {'flask_life_per_second'},
        header = i18n.item_table.flask_life_per_second,
        fields = h.range_fields_factory{fields={'flasks.life', 'flasks.duration'}, full=true},
        display = h.display_range_composite_factory{field1='flasks.life', field2='flasks.duration'},
    },
    {
        order = 1702,
        args = {'flask_life_per_charge'},
        header = i18n.item_table.flask_life_per_charge,
        fields = h.range_fields_factory{fields={'flasks.life', 'flasks.charges_per_use'}, full=true},
        display = h.display_range_composite_factory{field1='flasks.life', field2='flasks.charges_per_use'},
    },
    {
        order = 1703,
        args = {'flask_mana'},
        header = i18n.item_table.flask_mana,
        fields = h.range_fields_factory{fields={'flasks.mana'}},
        display = h.display_range_factory{field='flasks.mana'},
    },
    {
        order = 1704,
        args = {'flask_mana_per_second'},
        header = i18n.item_table.flask_mana_per_second,
        fields = h.range_fields_factory{fields={'flasks.mana', 'flasks.duration'}, full=true},
        display = h.display_range_composite_factory{field1='flasks.mana', field2='flasks.duration'},
    },
    {
        order = 1705,
        args = {'flask_mana_per_charge'},
        header = i18n.item_table.flask_mana_per_charge,
        fields = h.range_fields_factory{fields={'flasks.mana', 'flasks.charges_per_use'}, full=true},
        display = h.display_range_composite_factory{field1='flasks.mana', field2='flasks.charges_per_use'},
    },
    {
        order = 1706,
        args = {'flask'},
        header = i18n.item_table.flask_duration,
        fields = h.range_fields_factory{fields={'flasks.duration'}},
        display = h.display_range_factory{field='flasks.duration'},
    },
    {
        order = 1707,
        args = {'flask'},
        header = i18n.item_table.flask_charges_per_use,
        fields = h.range_fields_factory{fields={'flasks.charges_per_use'}},
        display = h.display_range_factory{field='flasks.charges_per_use'},
    },
    {
        order = 1708,
        args = {'flask'},
        header = i18n.item_table.flask_maximum_charges,
        fields = h.range_fields_factory{fields={'flasks.charges_max'}},
        display = h.display_range_factory{field='flasks.charges_max'},
    },
    -- Jewels 18xx
    {
        order = 1800,
        args = {'jewel_limit'},
        header = i18n.item_table.jewel_limit,
        fields = {'jewels.jewel_limit'},
        display = h.display_value_factory{},
    },
    {
        order = 1801,
        args = {'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,
    },
    -- Maps 19xx
    {
        order = 1900,
        args = {'map_tier'},
        header = i18n.item_table.map_tier,
        fields = {'maps.tier'},
        display = h.display_value_factory{},
    },
    {
        order = 1901,
        args = {'map_level'},
        header = i18n.item_table.map_level,
        fields = {'maps.area_level'},
        display = h.display_value_factory{},
    },
    {
        order = 1902,
        args = {'map_guild_character'},
        header = i18n.item_table.map_guild_character,
        fields = {'maps.guild_character'},
        display = h.display_value_factory{},
        sort_type = 'text',
    },
    {
        order = 1903,
        args = {'map_series'},
        header = i18n.item_table.map_series,
        fields = {'maps.series'},
        display = h.display_value_factory{},
    },
    {
        order = 1904,
        args = {'atlas_tier'},
        header = i18n.item_table.atlas_tier,
        fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
        display = h.display_atlas_tier_factory{is_level=false},
        colspan = 5,
    },
    {
        order = 1905,
        args = {'atlas_level'},
        header = i18n.item_table.atlas_level,
        fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
        display = h.display_atlas_tier_factory{is_level=true},
        colspan = 5,
    },
    -- Map fragments 20xx
    {
        order = 2000,
        args = {'map_fragment', 'map_fragment_limit'},
        header = i18n.item_table.map_fragment_limit,
        fields = {'map_fragments.map_fragment_limit'},
        display = h.display_value_factory{},
    },
    -- Hideout decorations 21xx
    {
        order = 2100,
        args = {'doodad', 'variation_count'},
        header = i18n.item_table.variation_count,
        fields = {'hideout_doodads.variation_count'},
        display = h.display_value_factory{
            color = 'mod',
        },
    },
    -- Corpse items 22xx
    {
        order = 2200,
        args = {'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 = 2201,
        args = {'corpse', 'monster_abilities'},
        header = i18n.item_table.monster_abilities,
        fields = {'corpse_items.monster_abilities'},
        display = h.display_value_factory{
            color = 'mod',
        },
        sort_type = 'text',
    },
    -- Seed data 91xx,
    {
        order = 9100,
        args = {'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 = 9101,
        args = {'seed', 'seed_tier'},
        header = i18n.item_table.seed_tier,
        fields = {'harvest_seeds.tier'},
        display = h.display_value_factory{},
    },
    {
        order = 9102,
        args = {'seed', 'seed_growth_cycles'},
        header = i18n.item_table.seed_growth_cycles,
        fields = {'harvest_seeds.growth_cycles'},
        display = h.display_value_factory{},
    },
    {
        order = 9110,
        args = {'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.display_value_factory{
            color = 'primal',
        },
    },
    {
        order = 9110,
        args = {'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.display_value_factory{
            color = 'vivid',
        },
    },
    {
        order = 9110,
        args = {'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.display_value_factory{
            color = 'wild',
        },
    },
    {
        order = 9113,
        args = {'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.display_value_factory{},
    },
    {
        order = 9114,
        args = {'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.display_value_factory{},
    },
    {
        order = 12000,
        args = {'buff'},
        header = i18n.item_table.buff_effects,
        fields = {'item_buffs.stat_text'},
        display = h.display_value_factory{
            color = 'mod',
        },
        sort_type = 'text',
    },
    {
        order = 12001,
        args = {'stat'},
        header = i18n.item_table.stats,
        fields = {'items.stat_text'},
        display = h.display_value_factory{
            color = 'mod',
        },
        sort_type = 'text',
    },
    {
        order = 12002,
        args = {'description'},
        header = i18n.item_table.description,
        fields = {'items.description'},
        display = h.display_value_factory{
            color = 'mod',
        },
        sort_type = 'text',
    },
    {
        order = 12003,
        args = {'seed', 'seed_effect'},
        header = i18n.item_table.seed_effects,
        fields = {'harvest_seeds.effect'},
        display = h.display_value_factory{
            color = 'crafted',
        },
        sort_type = 'text',
    },
    {
        order = 12100,
        args = {'flavour_text'},
        header = i18n.item_table.flavour_text,
        fields = {'items.flavour_text'},
        display = h.display_value_factory{
            color = 'flavour',
        },
        sort_type = 'text',
    },
    {
        order = 12200,
        args = {'help_text'},
        header = i18n.item_table.help_text,
        fields = {'items.help_text'},
        display = h.display_value_factory{
            color = 'help',
        },
        sort_type = 'text',
    },
    {
        order = 12300,
        args = {'buff_icon'},
        header = i18n.item_table.buff_icon,
        fields = {'item_buffs.icon'},
        display = h.display_value_factory{
            fmt_options = {
                [1] = {
                    fmt = '[[%s]]',
                },
            },
        },
        sort_type = 'text',
    },
    {
        order = 13000,
        args = {'prophecy', 'objective'},
        header = i18n.item_table.objective,
        fields = {'prophecies.objective'},
        display = h.display_value_factory{},
    },
    {
        order = 13001,
        args = {'prophecy', 'reward'},
        header = i18n.item_table.reward,
        fields = {'prophecies.reward'},
        display = h.display_value_factory{},
    },
    {
        order = 13002,
        args = {'prophecy', 'seal_cost'},
        header = i18n.item_table.seal_cost,
        fields = {'prophecies.seal_cost'},
        display = h.display_value_factory{
            color = 'currency',
        },
    },
    {
        order = 13003,
        args = {'prediction_text'},
        header = i18n.item_table.prediction_text,
        fields = {'prophecies.prediction_text'},
        display = h.display_value_factory{
            color = 'value',
        },
        sort_type = 'text',
    },
    {
        order = 14000,
        args = {'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,
        args = {'drop', 'drop_level'},
        header = i18n.item_table.drop_level,
        fields = {'items.drop_level'},
        display = h.display_value_factory{},
    },
    {
        order = 15001,
        args = {'drop_level_maximum'},
        header = i18n.item_table.drop_level_maximum,
        fields = {'items.drop_level_maximum'},
        display = h.display_value_factory{},
    },
    {
        order = 15002,
        args = {'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 = 15003,
        args = {'drop', 'drop_enabled'},
        header = i18n.item_table.drop_enabled,
        fields = {'items.drop_enabled'},
        display = h.display_value_factory{},
        display = h.display_yesno_factory{
            field = 'items.drop_enabled',
        },
        sort_type = 'text',
    },
    {
        order = 15004,
        args = {'drop', 'drop_areas'},
        header = i18n.item_table.drop_areas,
        fields = {'items.drop_areas_html'},
        display = h.display_value_factory{},
        sort_type = 'text',
    },
    {
        order = 15005,
        args = {'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,
        sort_type = 'text',
    },
    {
        order = 15006,
        args = {'drop', 'drop_text'},
        header = i18n.item_table.drop_text,
        fields = {'items.drop_text'},
        display = h.display_value_factory{},
        sort_type = 'text',
    },
    {
        order = 16000,
        args = {'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:node(m_util.html.table_cell('na'))
            else
                tr
                    :tag('td')
                        :attr('style', 'text-align:left')
                        :wikitext(value)
            end
        end,
        sort_type = 'text',
    },
    {
        order = 17000,
        args = {'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:node(m_util.html.table_cell('na'))
            else
                tr
                    :tag('td')
                        :attr('style', 'text-align:left')
                        :wikitext(value)
            end
        end,
        sort_type = 'text',
    },
    {
        order = 18000,
        args = {'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,
        sort_type = 'text',
    },
    {
        order = 18001,
        args = {'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,
        sort_type = 'text',
    },
    {
        order = 19000,
        args = {'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,
        sort_type = 'text',
    },
    {
        order = 19001,
        args = {'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
            tr
                :tag('td')
                    :attr('data-sort-value', #tbl)
                    :wikitext(#tbl)
        end,
    },
    {
        order = 20000,
        args = {'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,
        sort_type = 'text',
    },
    {
        order = 21000,
        args = {'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,
        sort_type = 'text',
    },
    {
        order = 23000,
        args = {'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,
        sort_type = 'text',
    },
}

data_map.skill_gem = {
    {
        order = 1000,
        args = {'gem_tier'},
        header = i18n.item_table.tier,
        fields = {'skill_gems.gem_tier'},
        display = h.display_value_factory{},
    },
    {
        order = 1001,
        args = {'skill_icon'},
        header = i18n.item_table.skill_icon,
        fields = {'skill.skill_icon'},
        display = h.display_value_factory{
            fmt_options = {
                [1] = {
                    fmt = '[[%s]]',
                },
            },
        },
        sort_type = 'text',
    },
    {
        order = 2000,
        args = {'stat', 'stat_text'},
        header = i18n.item_table.stats,
        fields = {'skill.stat_text'},
        display = h.display_value_factory{},
        sort_type = 'text',
    },
    {
        order = 2001,
        args = {'quality', 'quality_stat_text'},
        header = i18n.item_table.quality_stats,
        fields = {'skill.quality_stat_text'},
        display = h.display_value_factory{},
        sort_type = 'text',
    },
    {
        order = 2100,
        args = {'description'},
        header = i18n.item_table.description,
        fields = {'skill.description'},
        display = h.display_value_factory{},
        sort_type = 'text',
    },
    {
        order = 3000,
        args = {'level'},
        header = m_util.html.abbr(
            string.format(
                '[[%s|link=|alt=%s]]',
                i18n.item_table.level_icon,
                i18n.item_table.gem_level_requirement
            ),
            i18n.item_table.gem_level_requirement
        ),
        fields = h.range_fields_factory{fields={'items.required_level'}},
        display = h.display_range_factory{field='items.required_level'},
    },
    {
        order = 3001,
        args = {'str'},
        header = m_util.html.abbr(
            string.format(
                '[[%s|link=|alt=%s]]',
                i18n.item_table.str_icon,
                i18n.item_table.str_gem
            ),
            i18n.item_table.str_gem
        ),
        fields = {'skill_gems.strength_percent'},
        display = h.display_yesno_factory{
            field = 'skill_gems.strength_percent',
            condition = h.value_greater_than_zero,
        },
        sort_type = 'text',
    },
    {
        order = 3002,
        args = {'dex'},
        header = m_util.html.abbr(
            string.format(
                '[[%s|link=|alt=%s]]',
                i18n.item_table.dex_icon,
                i18n.item_table.dex_gem
            ),
            i18n.item_table.dex_gem
        ),
        fields = {'skill_gems.dexterity_percent'},
        display = h.display_yesno_factory{
            field = 'skill_gems.dexterity_percent',
            condition = h.value_greater_than_zero,
        },
        sort_type = 'text',
    },
    {
        order = 3003,
        args = {'int'},
        header = m_util.html.abbr(
            string.format(
                '[[%s|link=|alt=%s]]',
                i18n.item_table.int_icon,
                i18n.item_table.int_gem
            ),
            i18n.item_table.int_gem
        ),
        fields = {'skill_gems.intelligence_percent'},
        display = h.display_yesno_factory{
            field = 'skill_gems.intelligence_percent',
            condition = h.value_greater_than_zero,
        },
        sort_type = 'text',
    },
    {
        order = 4000,
        args = {'crit'},
        header = i18n.item_table.skill_critical_strike_chance,
        fields = {'skill_levels.critical_strike_chance'},
        display = h.display_value_factory{
            fmt_options = {
                [1] = {
                    fmt = '%s%%',
                },
            },
        },
        field_options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        order = 4001,
        args = {'cast_time'},
        header = i18n.item_table.cast_time,
        fields = {'skill.cast_time'},
        display = h.display_value_factory{},
    },
    {
        order = 4002,
        args = {'aspd', 'attack_speed', 'attack_speed_multiplier'},
        header = i18n.item_table.attack_speed_multiplier,
        fields = {'skill_levels.attack_speed_multiplier'},
        display = h.display_value_factory{
            fmt_options = {
                [1] = {
                    fmt = '%s%%',
                },
            },
        },
        field_options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        order = 4003,
        args = {'dmgeff'},
        header = i18n.item_table.damage_effectiveness,
        fields = {'skill_levels.damage_effectiveness'},
        display = h.display_value_factory{
            fmt_options = {
                [1] = {
                    fmt = '%s%%',
                },
            },
        },
        field_options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        order = 5000,
        args = {'mcm', 'cost_multiplier'},
        header = i18n.item_table.cost_multiplier,
        fields = {'skill_levels.cost_multiplier'},
        display = h.display_value_factory{
            fmt_options = {
                [1] = {
                    fmt = '%s%%',
                },
            },
        },
        field_options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        order = 5001,
        args = {'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.display_value_factory{
                fmt_options = {
                    [1] = {
                        fmt = '%d' .. appendix,
                    },
                },
            }(tr, data, {cost_field}, data2)
        end,
        -- Need one set of options per field.
        field_options = {
            [1] = {
                skill_levels = true,
            },
            [2] = {
                skill_levels = true,
            },
            [3] = {
                skill_levels = true,
            },
        },
    },
    {
        order = 6000,
        args = {'vaal'},
        header = i18n.item_table.vaal_souls_requirement,
        fields = {'skill_levels.vaal_souls_requirement'},
        display = h.display_value_factory{},
        field_options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        order = 6001,
        args = {'vaal'},
        header = i18n.item_table.stored_uses,
        fields = {'skill_levels.vaal_stored_uses'},
        display = h.display_value_factory{},
        field_options = {
            [1] = {
                skill_levels = true,
            },
        },
    },
    {
        order = 7000,
        args = {'radius'},
        header = i18n.item_table.primary_radius,
        fields = {'skill.radius', 'skill.radius_description'},
        field_options = {
            [2] = {
                optional = true,
            },
        },
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill.radius'])
                    :wikitext(h.display_descriptor_value_factory{tbl=data, key='skill.radius_description'}(data['skill.radius']))
        end,
    },
    {
        order = 7001,
        args = {'radius'},
        header = i18n.item_table.secondary_radius,
        fields = {'skill.radius_secondary', 'skill.radius_secondary_description'},
        field_options = {
            [2] = {
                optional = true,
            },
        },
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill.radius_secondary'])
                    :wikitext(h.display_descriptor_value_factory{tbl=data, key='skill.radius_secondary_description'}(data['skill.radius_secondary']))
        end,
    },
    {
        order = 7002,
        args = {'radius'},
        header = i18n.item_table.tertiary_radius,
        fields = {'skill.radius_tertiary', 'skill.radius_tertiary_description'},
        field_options = {
            [2] = {
                optional = true,
            },
        },
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['skill.radius_tertiary'])
                   :wikitext(h.display_descriptor_value_factory{tbl=data, key='skill.radius_tertiary_description'}(data['skill.radius_tertiary']))
        end,
    },
}

-- ----------------------------------------------------------------------------
-- 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

    -- Handle deprecated "q_" args
    local query_params = {
        'tables',
        'join',
        'where',
        'groupBy',
        'having',
        'orderBy',
        'limit',
        'offset',
    }
    for _, v in ipairs(query_params) do
        args[v] = args[v] or args['q_' .. v]
    end

    -- A where clause is required; there are far too many items to list in one table
    if args.where == nil then
        error(string.format(i18n.errors.generic_required_parameter, 'where'))
    end

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

    local row_infos = {}
    for _, row_info in ipairs(modes[args.mode].data) do
        local enabled = false
        if type(row_info.args) == 'table' then
            for _, a in ipairs(row_info.args) do
                if m_util.cast.boolean(args[a]) then
                    enabled = true
                    break
                end
            end
        else
            enabled = true
        end
        if enabled then
            row_info.field_options = row_info.field_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 = {
            header = col_info.header,
            fields = {},
            display = function (tr, data)
                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, {color=false})
                            vmax = vmax + stat.max
                        end
                    end

                    if num_stats ~= #stat_texts then
                        tr:node(m_util.html.table_cell('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)

    -- Build Cargo query
    local tables = {'items'}
    local fields = {
        'items._pageName=_pageName',
        'items._pageID=_pageID',
        'items.name=items_name',
        'items.inventory_icon=items_inventory_icon',
        'items.html=items_html',
        'items.size_x=items_size_x',
        'items.size_y=items_size_y',
        'items.required_level_range_maximum=items_required_level_range_maximum',
        'items.required_level_range_colour=items_required_level_range_colour',
        'items.required_level_range_text=items_required_level_range_text',
        'items.stat_text=items_stat_text',
    }
    local query = {
        where = args.where,
        groupBy = table.concat({'items._pageID', args.groupBy}, ', '),
        having = args.having,
        orderBy = args.orderBy,
        limit = args.limit,
        offset = args.offset,
    }

    -- 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

    -- Minimum required tables and fields, based on display options
    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.field_options[index] = rowinfo.field_options[index] or {}
            if rowinfo.field_options[index].skill_levels then
                skill_levels[#skill_levels+1] = field
            else
                fields[#fields+1] = field
                tables[#tables+1] = m_util.string.split(field, '.', true)[1]
            end
        end
    end
    if #skill_levels > 0 then
        fields[#fields+1] = 'skill.max_level'
        tables[#tables+1] = 'skill'
    end
    tables = m_util.table.remove_duplicates(tables)

    -- Minimum required joins, based on display options
    local joins = {}
    for _, table_name in ipairs(tables) do
        if table_name ~= 'items' then
            joins[#joins+1] = string.format('items._pageID=%s._pageID', table_name)
        end
    end

    -- Append additional tables
    args.tables = m_util.cast.table(args.tables)
    if type(args.tables) == 'table' and #args.tables > 0 then
        tables = m_util.table.merge(tables, args.tables)
    end

    -- Make join clause
    if #joins > 0 or args.join then
        -- m_util.table.merge rebuilds the table, which removes empty values
        query.join = table.concat(m_util.table.merge(joins, {args.join}), ', ')
    end

    -- Query results
    local results = m_cargo.query(tables, fields, query)
    for _, row in ipairs(results) do
        for k, v in pairs(row) do
            if not row[k:gsub('%.', '_')] then
                row[k:gsub('%.', '_')] = v
            end
        end
    end
    for _, row in ipairs(results) do
        for k, v in pairs(row) do
            local with_dot = k:gsub('_', '.')
            if not row[with_dot] then
                row[with_dot] = v
            end
        end
    end

    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 then
                    row[field] = row[field:gsub('%.', '_')]
                end
                if row[field] == nil or row[field] == '' then
                    local options = rowinfo.field_options[index]
                    if options.optional ~= true and options.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:node(m_util.html.table_cell('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, {tables=tables, fields=fields, query=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=_pageName',
        'items._pageID=_pageID',
        'items.name=items_name',
        'items.class=item_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