Module:Item table: Difference between revisions

From Path of Exile 2 Wiki
Jump to navigation Jump to search
(work around an upcoming bug in cargo)
(Minor refactoring in preparation to drop the "q_" from the query arguments. The error message when `where` param is missing is now more informative.)
 
(16 intermediate revisions by 2 users not shown)
Line 4: Line 4:
--  
--  
-- This module implements Template:Item table and other templates that query
-- This module implements Template:Item table and other templates that query
-- and display lists of items.
-- and display tables or lists of items.
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- Todo list
-- ---------
-- * Handle table columns that can have multiple cargo rows, preferably
--  in one or two queries. Not per column AND row.
-- * Handle template include size? Remove item link when getting close
--  to the limit?
-- * Add a wrapper around f_item_link to be able to disable all
--  hoverboxes, to avoid running into template expansion size issues.


require('Module:No globals')
require('Module:No globals')
local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_cargo = require('Module:Cargo')
local m_game = mw.loadData('Module:Game')


-- Should we use the sandbox version of our submodules?
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Item table')
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
-- Lazy loading
Line 93: Line 83:
end
end


function h.na_or_val(tr, value, func)
function h.range_fields_factory(args)
    if value == nil or value == '' then
     -- Returns a function that gets the range fields for the given keys
        tr:wikitext(m_util.html.td.na())
     else
        local raw_value = value
        if func ~= nil then
            value = func(value)
        end
        tr
            :tag('td')
                :attr('data-sort-value', raw_value)
                :wikitext(value)
                :done()
    end
end
 
h.tbl = {}
 
function h.tbl.range_fields(args)
     local suffixes = {'maximum', 'text', 'colour'}
     local suffixes = {'maximum', 'text', 'colour'}
     if args.full then
     if args.full then
         suffixes[#suffixes+1] = 'minimum'
         suffixes[#suffixes+1] = 'minimum'
     end
     end
       
     return function ()
     return function()
         local fields = {}
         local fields = {}
         local inner = function (field)
         for _, field in ipairs(args.fields) do
             for _, partial_field in ipairs(suffixes) do
             for _, suffix in ipairs(suffixes) do
                 fields[#fields+1] = string.format('%s_range_%s', field, partial_field)
                 fields[#fields+1] = string.format('%s_range_%s', field, suffix)
             end
             end
        end
       
        if type(args.field) == 'table' then
            for _, field in ipairs(args.field) do
                inner(field)
            end
        else
            inner(args.field)
         end
         end
         return fields
         return fields
Line 136: Line 100:
end
end


h.tbl.display = {}
function h.display_value_factory(args)
function h.tbl.display.na_or_val(tr, value, data)
     args.fmt_options = args.fmt_options or {}
    return h.na_or_val(tr, value)
     return function(tr, data, fields, data2)
end
         local values = {}
 
         local fmt_values = {}
function h.tbl.display.seconds(tr, value, data)
         for index, field in ipairs(fields) do
    return h.na_or_val(tr, value, function(value)
             local value = {
        return string.format('%ss', value)
    end)
end
 
function h.tbl.display.percent(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('%s%%', value)
    end)
end
 
function h.tbl.display.wikilink(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('[[%s]]', value)
    end)
end
 
h.tbl.display.factory = {}
function h.tbl.display.factory.value(args)
     args.options = args.options or {}
 
     return function(tr, data, fields, data2)
         local values = {}
         local fmt_values = {}
        local sdata = data2.skill_levels[data['items._pageName']]
 
         for index, field in ipairs(fields) do
             local value = {
                 min=data[field],
                 min=data[field],
                 max=data[field],
                 max=data[field],
                 base=data[field],
                 base=data[field],
             }
             }
            local sdata = data2 and data2.skill_levels[data['items._pageName']]
             if sdata then --For skill data
             if sdata then --For skill data
                 -- Use the fixed value, or the first-level value.
                 -- Use the fixed value, or the first-level value.
Line 182: Line 120:
             if value.min then
             if value.min then
                 values[#values+1] = value.max
                 values[#values+1] = value.max
                 local opts = args.options[index] or {}
                 local options = args.fmt_options[index] or {}
                 -- global colour is set, no overrides
                 -- global color is set, no overrides
                 if args.colour ~= nil then
                 if args.color ~= nil then
                     opts.no_color = true
                     options.color = false
                 end
                 end
                 fmt_values[#fmt_values+1] = m_util.html.format_value(nil, value, opts)
                 fmt_values[#fmt_values+1] = m_util.html.format_value(nil, value, options)
             end
             end
         end
         end
         if #values == 0 then
         if #values == 0 then
             tr:wikitext(m_util.html.td.na())
             tr:node(m_util.html.table_cell('na'))
         else
         else
             local td = tr:tag('td')
             local td = tr:tag('td')
             td:attr('data-sort-value', table.concat(values, ', '))
             td
            td:wikitext(table.concat(fmt_values, ', '))
                :attr('data-sort-value', table.concat(values, ', '))
             if args.colour then
                :wikitext(table.concat(fmt_values, ', '))
                 td:attr('class', 'tc -' .. args.colour)
             if args.color then
                 td:attr('class', 'tc -' .. args.color)
             end
             end
         end
         end
Line 204: Line 142:
end
end


function h.tbl.display.factory.range(args)
function h.display_range_factory(args)
     -- args: table
     -- args: table
     --  property
     --  property
Line 217: Line 155:
end
end


function h.tbl.display.factory.range_composite(args)
function h.display_range_composite_factory(args)
     -- division by default
     -- division by default
     if args.func == nil then
     if args.func == nil then
Line 268: Line 206:
end
end


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


function h.tbl.display.factory.atlas_tier(args)
function h.display_atlas_tier_factory(args)
     args = args or {}
     args = args or {}
     return function (tr, data)
     return function (tr, data)
Line 308: Line 245:
end
end


-- ----------------------------------------------------------------------------
function h.display_yesno_factory(args)
-- Data mappings
    -- 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


local data_map = {}
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


-- for sort type see:
function h.na_or_val(tr, value)
-- https://meta.wikimedia.org/wiki/Help:Sorting
    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 = {
data_map.generic_item = {
     {
     {
         arg = 'base_item',
         order = 1000,
        args = {'base_item'},
         header = i18n.item_table.base_item,
         header = i18n.item_table.base_item,
         fields = {'items.base_item', 'items.base_item_page'},
         fields = {'items.base_item', 'items.base_item_page'},
Line 327: Line 298:
                     :wikitext(string.format('[[%s|%s]]', data['items.base_item_page'], data['items.base_item']))
                     :wikitext(string.format('[[%s|%s]]', data['items.base_item_page'], data['items.base_item']))
         end,
         end,
        order = 1000,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'class',
         order = 1001,
        args = {'class'},
         header = i18n.item_table.item_class,
         header = i18n.item_table.item_class,
         fields = {'items.class'},
         fields = {'items.class'},
         display = h.tbl.display.factory.value{options = {
         display = h.display_value_factory{
            [1] = {
            fmt_options = {
                fmt='[[%s]]',
                [1] = {
                    fmt = '[[%s]]',
                },
             },
             },
         }},
         },
        order = 1001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'rarity',
         order = 1002,
        args = {'rarity'},
         header = i18n.item_table.rarity,
         header = i18n.item_table.rarity,
         fields = {'items.rarity'},
         fields = {'items.rarity'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 1002,
     },
     },
     {
     {
         arg = 'rarity_id',
         order = 1003,
        args = {'rarity_id'},
         header = i18n.item_table.rarity_id,
         header = i18n.item_table.rarity_id,
         fields = {'items.rarity_id'},
         fields = {'items.rarity_id'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 1003,
     },
     },
     {
     {
         arg = 'metadata_id',
         order = 1004,
        args = {'metadata_id'},
         header = i18n.item_table.metadata_id,
         header = i18n.item_table.metadata_id,
         fields = {'items.metadata_id'},
         fields = {'items.metadata_id'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 1004,
     },
     },
     {
     {
         arg = 'essence',
         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,
         header = i18n.item_table.essence_level,
         fields = {'essences.level'},
         fields = {'essences.level'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
         order = 2000,
    },
    {
        order = 1300,
        args = {'stack_size'},
        header = i18n.item_table.stack_size,
        fields = {'stackables.stack_size'},
         display = h.display_value_factory{},
     },
     },
     {
     {
         arg = {'drop', 'drop_level'},
         order = 1301,
         header = i18n.item_table.drop_level,
        args = {'stack_size_currency_tab'},
         fields = {'items.drop_level'},
         header = i18n.item_table.stack_size_currency_tab,
         display = h.tbl.display.factory.value{},
         fields = {'stackables.stack_size_currency_tab'},
        order = 3000,
         display = h.display_value_factory{},
     },
     },
    -- Requirements
     {
     {
         arg = {'drop_level_maximum'},
         order = 1400,
         header = i18n.item_table.drop_level_maximum,
        args = {'level'},
         fields = {'items.drop_level_maximum'},
         header = m_util.html.abbr(
         display = h.tbl.display.factory.value{},
            string.format(
        order = 3001,
                '[[%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'},
     },
     },
     {
     {
         arg = 'stack_size',
         order = 1401,
         header = i18n.item_table.stack_size,
        args = {'str'},
         fields = {'stackables.stack_size'},
         header = m_util.html.abbr(
         display = h.tbl.display.factory.value{},
            string.format(
        order = 4000,
                '[[%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'},
     },
     },
     {
     {
         arg = 'stack_size_currency_tab',
         order = 1402,
         header = i18n.item_table.stack_size_currency_tab,
        args = {'dex'},
         fields = {'stackables.stack_size_currency_tab'},
         header = m_util.html.abbr(
         display = h.tbl.display.factory.value{},
            string.format(
        order = 4001,
                '[[%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'},
     },
     },
     {
     {
         arg = 'level',
         order = 1403,
         header = m_game.level_requirement.icon,
        args = {'int'},
         fields = h.tbl.range_fields{field='items.required_level'},
         header = m_util.html.abbr(
         display = h.tbl.display.factory.range{field='items.required_level'},
            string.format(
        order = 5000,
                '[[%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
     {
     {
         arg = 'ar',
         order = 1500,
        args = {'ar'},
         header = i18n.item_table.armour,
         header = i18n.item_table.armour,
         fields = h.tbl.range_fields{field='armours.armour'},
         fields = h.range_fields_factory{fields={'armours.armour'}},
         display = h.tbl.display.factory.range{field='armours.armour'},
         display = h.display_range_factory{field='armours.armour'},
        order = 6000,
     },
     },
     {
     {
         arg = 'ev',
         order = 1501,
        args = {'ev'},
         header =i18n.item_table.evasion,
         header =i18n.item_table.evasion,
         fields = h.tbl.range_fields{field='armours.evasion'},
         fields = h.range_fields_factory{fields={'armours.evasion'}},
         display = h.tbl.display.factory.range{field='armours.evasion'},
         display = h.display_range_factory{field='armours.evasion'},
        order = 6001,
     },
     },
     {
     {
         arg = 'es',
         order = 1502,
        args = {'es'},
         header = i18n.item_table.energy_shield,
         header = i18n.item_table.energy_shield,
         fields = h.tbl.range_fields{field='armours.energy_shield'},
         fields = h.range_fields_factory{fields={'armours.energy_shield'}},
         display = h.tbl.display.factory.range{field='armours.energy_shield'},
         display = h.display_range_factory{field='armours.energy_shield'},
        order = 6002,
     },
     },
     {
     {
         arg = 'wd',
         order = 1503,
        args = {'wd'},
         header = i18n.item_table.ward,
         header = i18n.item_table.ward,
         fields = h.tbl.range_fields{field='armours.ward'},
         fields = h.range_fields_factory{fields={'armours.ward'}},
         display = h.tbl.display.factory.range{field='armours.ward'},
         display = h.display_range_factory{field='armours.ward'},
        order = 6003,
     },
     },
     {
     {
         arg = 'block',
         order = 1504,
        args = {'block'},
         header = i18n.item_table.block,
         header = i18n.item_table.block,
         fields = h.tbl.range_fields{field='shields.block'},
         fields = h.range_fields_factory{fields={'shields.block'}},
         display = h.tbl.display.factory.range{field='shields.block'},
         display = h.display_range_factory{field='shields.block'},
        order = 6004,
    },
    --[[{
        arg = 'physical_damage_min',
        header = m_util.html.abbr('Min', 'Local minimum weapon damage'),
        fields = h.tbl.range_fields('minimum physical damage'),
        display = h.tbl.display.factory.range{field='minimum physical damage'},
        order = 7000,
     },
     },
    -- Weapons 16xx
     {
     {
        arg = 'physical_damage_max',
         order = 1600,
        header = m_util.html.abbr('Max', 'Local maximum weapon damage'),
         args = {'weapon', 'damage'},
        fields = h.tbl.range_fields('maximum physical damage'),
        display = h.tbl.display.factory.range{field='maximum physical damage'},
         order = 7001,
 
    },]]--
    {
         arg = {'weapon', 'damage'},
         header = i18n.item_table.damage,
         header = i18n.item_table.damage,
         fields = {'weapons.damage_html', 'weapons.damage_avg'},
         fields = {'weapons.damage_html', 'weapons.damage_avg'},
Line 465: Line 473:
                     :wikitext(data['weapons.damage_html'])
                     :wikitext(data['weapons.damage_html'])
         end,
         end,
        order = 8000,
     },
     },
     {
     {
         arg = {'weapon', 'aps'},
         order = 1601,
        args = {'weapon', 'aps'},
         header = i18n.item_table.attacks_per_second,
         header = i18n.item_table.attacks_per_second,
         fields = h.tbl.range_fields{field='weapons.attack_speed'},
         fields = h.range_fields_factory{fields={'weapons.attack_speed'}},
         display = h.tbl.display.factory.range{field='weapons.attack_speed'},
         display = h.display_range_factory{field='weapons.attack_speed'},
        order = 8001,
     },
     },
     {
     {
         arg = {'weapon', 'crit'},
         order = 1602,
        args = {'weapon', 'crit'},
         header = i18n.item_table.local_critical_strike_chance,
         header = i18n.item_table.local_critical_strike_chance,
         fields = h.tbl.range_fields{field='weapons.critical_strike_chance'},
         fields = h.range_fields_factory{fields={'weapons.critical_strike_chance'}},
         display = h.tbl.display.factory.range{field='weapons.critical_strike_chance'},
         display = h.display_range_factory{field='weapons.critical_strike_chance'},
        order = 8002,
     },
     },
     {
     {
         arg = {'physical_dps'},
         order = 1603,
        args = {'physical_dps'},
         header = i18n.item_table.physical_dps,
         header = i18n.item_table.physical_dps,
         fields = h.tbl.range_fields{field='weapons.physical_dps'},
         fields = h.range_fields_factory{fields={'weapons.physical_dps'}},
         display = h.tbl.display.factory.range{field='weapons.physical_dps'},
         display = h.display_range_factory{field='weapons.physical_dps'},
        order = 8100,
     },
     },
     {
     {
         arg = {'lightning_dps'},
         order = 1604,
        args = {'lightning_dps'},
         header = i18n.item_table.lightning_dps,
         header = i18n.item_table.lightning_dps,
         fields = h.tbl.range_fields{field='weapons.lightning_dps'},
         fields = h.range_fields_factory{fields={'weapons.lightning_dps'}},
         display = h.tbl.display.factory.range{field='weapons.lightning_dps'},
         display = h.display_range_factory{field='weapons.lightning_dps'},
        order = 8101,
     },
     },
     {
     {
         arg = {'cold_dps'},
         order = 1605,
        args = {'cold_dps'},
         header = i18n.item_table.cold_dps,
         header = i18n.item_table.cold_dps,
         fields = h.tbl.range_fields{field='weapons.cold_dps'},
         fields = h.range_fields_factory{fields={'weapons.cold_dps'}},
         display = h.tbl.display.factory.range{field='weapons.cold_dps'},
         display = h.display_range_factory{field='weapons.cold_dps'},
        order = 8102,
     },
     },
     {
     {
         arg = {'fire_dps'},
         order = 1606,
        args = {'fire_dps'},
         header = i18n.item_table.fire_dps,
         header = i18n.item_table.fire_dps,
         fields = h.tbl.range_fields{field='weapons.fire_dps'},
         fields = h.range_fields_factory{fields={'weapons.fire_dps'}},
         display = h.tbl.display.factory.range{field='weapons.fire_dps'},
         display = h.display_range_factory{field='weapons.fire_dps'},
        order = 8103,
     },
     },
     {
     {
         arg = {'chaos_dps'},
         order = 1607,
        args = {'chaos_dps'},
         header = i18n.item_table.chaos_dps,
         header = i18n.item_table.chaos_dps,
         fields = h.tbl.range_fields{field='weapons.chaos_dps'},
         fields = h.range_fields_factory{fields={'weapons.chaos_dps'}},
         display = h.tbl.display.factory.range{field='weapons.chaos_dps'},
         display = h.display_range_factory{field='weapons.chaos_dps'},
        order = 8104,
     },
     },
     {
     {
         arg = {'elemental_dps'},
         order = 1608,
        args = {'elemental_dps'},
         header = i18n.item_table.elemental_dps,
         header = i18n.item_table.elemental_dps,
         fields = h.tbl.range_fields{field='weapons.elemental_dps'},
         fields = h.range_fields_factory{fields={'weapons.elemental_dps'}},
         display = h.tbl.display.factory.range{field='weapons.elemental_dps'},
         display = h.display_range_factory{field='weapons.elemental_dps'},
        order = 8105,
     },
     },
     {
     {
         arg = {'poison_dps'},
         order = 1609,
        args = {'poison_dps'},
         header = i18n.item_table.poison_dps,
         header = i18n.item_table.poison_dps,
         fields = h.tbl.range_fields{field='weapons.poison_dps'},
         fields = h.range_fields_factory{fields={'weapons.poison_dps'}},
         display = h.tbl.display.factory.range{field='weapons.poison_dps'},
         display = h.display_range_factory{field='weapons.poison_dps'},
        order = 8106,
     },
     },
     {
     {
         arg = {'dps'},
         order = 1610,
        args = {'dps'},
         header = i18n.item_table.dps,
         header = i18n.item_table.dps,
         fields = h.tbl.range_fields{field='weapons.dps'},
         fields = h.range_fields_factory{fields={'weapons.dps'}},
         display = h.tbl.display.factory.range{field='weapons.dps'},
         display = h.display_range_factory{field='weapons.dps'},
        order = 8107,
     },
     },
    -- Flasks 17xx
     {
     {
         arg = 'flask_life',
         order = 1700,
        args = {'flask_life'},
         header = i18n.item_table.flask_life,
         header = i18n.item_table.flask_life,
         fields = h.tbl.range_fields{field='flasks.life'},
         fields = h.range_fields_factory{fields={'flasks.life'}},
         display = h.tbl.display.factory.range{field='flasks.life'},
         display = h.display_range_factory{field='flasks.life'},
        order = 9000,
     },
     },
     {
     {
         arg = 'flask_life_per_second',
         order = 1701,
        args = {'flask_life_per_second'},
         header = i18n.item_table.flask_life_per_second,
         header = i18n.item_table.flask_life_per_second,
         fields = h.tbl.range_fields{field={'flasks.life', 'flasks.duration'}, full=true},
         fields = h.range_fields_factory{fields={'flasks.life', 'flasks.duration'}, full=true},
         display = h.tbl.display.factory.range_composite{field1='flasks.life', field2='flasks.duration'},
         display = h.display_range_composite_factory{field1='flasks.life', field2='flasks.duration'},
        order = 9001,
     },
     },
     {
     {
         arg = 'flask_life_per_charge',
         order = 1702,
        args = {'flask_life_per_charge'},
         header = i18n.item_table.flask_life_per_charge,
         header = i18n.item_table.flask_life_per_charge,
         fields = h.tbl.range_fields{field={'flasks.life', 'flasks.charges_per_use'}, full=true},
         fields = h.range_fields_factory{fields={'flasks.life', 'flasks.charges_per_use'}, full=true},
         display = h.tbl.display.factory.range_composite{field1='flasks.life', field2='flasks.charges_per_use'},
         display = h.display_range_composite_factory{field1='flasks.life', field2='flasks.charges_per_use'},
        order = 9002,
     },
     },
     {
     {
         arg = 'flask_mana',
         order = 1703,
        args = {'flask_mana'},
         header = i18n.item_table.flask_mana,
         header = i18n.item_table.flask_mana,
         fields = h.tbl.range_fields{field='flasks.mana'},
         fields = h.range_fields_factory{fields={'flasks.mana'}},
         display = h.tbl.display.factory.range{field='flasks.mana'},
         display = h.display_range_factory{field='flasks.mana'},
        order = 9010,
     },
     },
     {
     {
         arg = 'flask_mana_per_second',
         order = 1704,
        args = {'flask_mana_per_second'},
         header = i18n.item_table.flask_mana_per_second,
         header = i18n.item_table.flask_mana_per_second,
         fields = h.tbl.range_fields{field={'flasks.mana', 'flasks.duration'}, full=true},
         fields = h.range_fields_factory{fields={'flasks.mana', 'flasks.duration'}, full=true},
         display = h.tbl.display.factory.range_composite{field1='flasks.mana', field2='flasks.duration'},
         display = h.display_range_composite_factory{field1='flasks.mana', field2='flasks.duration'},
        order = 9011,
     },
     },
     {
     {
         arg = 'flask_mana_per_charge',
         order = 1705,
        args = {'flask_mana_per_charge'},
         header = i18n.item_table.flask_mana_per_charge,
         header = i18n.item_table.flask_mana_per_charge,
         fields = h.tbl.range_fields{field={'flasks.mana', 'flasks.charges_per_use'}, full=true},
         fields = h.range_fields_factory{fields={'flasks.mana', 'flasks.charges_per_use'}, full=true},
         display = h.tbl.display.factory.range_composite{field1='flasks.mana', field2='flasks.charges_per_use'},
         display = h.display_range_composite_factory{field1='flasks.mana', field2='flasks.charges_per_use'},
        order = 9012,
     },
     },
     {
     {
         arg = 'flask',
         order = 1706,
        args = {'flask'},
         header = i18n.item_table.flask_duration,
         header = i18n.item_table.flask_duration,
         fields = h.tbl.range_fields{field='flasks.duration'},
         fields = h.range_fields_factory{fields={'flasks.duration'}},
         display = h.tbl.display.factory.range{field='flasks.duration'},
         display = h.display_range_factory{field='flasks.duration'},
        order = 9020,
     },
     },
     {
     {
         arg = 'flask',
         order = 1707,
        args = {'flask'},
         header = i18n.item_table.flask_charges_per_use,
         header = i18n.item_table.flask_charges_per_use,
         fields = h.tbl.range_fields{field='flasks.charges_per_use'},
         fields = h.range_fields_factory{fields={'flasks.charges_per_use'}},
         display = h.tbl.display.factory.range{field='flasks.charges_per_use'},
         display = h.display_range_factory{field='flasks.charges_per_use'},
        order = 9030,
     },
     },
     {
     {
         arg = 'flask',
         order = 1708,
        args = {'flask'},
         header = i18n.item_table.flask_maximum_charges,
         header = i18n.item_table.flask_maximum_charges,
         fields = h.tbl.range_fields{field='flasks.charges_max'},
         fields = h.range_fields_factory{fields={'flasks.charges_max'}},
         display = h.tbl.display.factory.range{field='flasks.charges_max'},
         display = h.display_range_factory{field='flasks.charges_max'},
         order = 9031,
    },
    -- Jewels 18xx
    {
         order = 1800,
        args = {'jewel_limit'},
        header = i18n.item_table.jewel_limit,
        fields = {'jewels.jewel_limit'},
        display = h.display_value_factory{},
     },
     },
    -- Seed data 91xx,
     {
     {
         arg = {'seed', 'seed_type'},
         order = 1801,
         header = i18n.item_table.seed_type,
        args = {'jewel_radius'},
         fields = {'harvest_seeds.type', 'harvest_seeds.type_id'},
         header = i18n.item_table.jewel_radius,
         fields = {'jewels.radius_html'},
         display = function (tr, data)
         display = function (tr, data)
             tr
             tr
                 :tag('td')
                 :tag('td')
                    :attr('table-sort-value', data['harvest_seeds.type'])
                     :wikitext(data['jewels.radius_html'])
                    :attr('class', 'tc -' .. data['harvest_seeds.type_id'])
                     :wikitext(data['harvest_seeds.type'])
         end,
         end,
        order = 9100,
     },
     },
    -- Maps 19xx
     {
     {
         arg = {'seed', 'seed_tier'},
         order = 1900,
         header = i18n.item_table.seed_tier,
        args = {'map_tier'},
         fields = {'harvest_seeds.tier'},
         header = i18n.item_table.map_tier,
         display = h.tbl.display.factory.value{},
         fields = {'maps.tier'},
        order = 9101,
         display = h.display_value_factory{},
     },
     },
     {
     {
         arg = {'seed', 'seed_growth_cycles'},
         order = 1901,
         header = i18n.item_table.seed_growth_cycles,
        args = {'map_level'},
         fields = {'harvest_seeds.growth_cycles'},
         header = i18n.item_table.map_level,
         display = h.tbl.display.factory.value{},
         fields = {'maps.area_level'},
        order = 9102,
         display = h.display_value_factory{},
     },
     },
     {
     {
         arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_primal_lifeforce_percentage'},
         order = 1902,
         header = i18n.item_table.seed_consumed_primal_lifeforce_percentage,
        args = {'map_guild_character'},
         fields = {'harvest_seeds.consumed_primal_lifeforce_percentage'},
         header = i18n.item_table.map_guild_character,
         display = h.tbl.display.factory.value{color='primal'},
         fields = {'maps.guild_character'},
         order = 9110,
         display = h.display_value_factory{},
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_vivid_lifeforce_percentage'},
         order = 1903,
         header = i18n.item_table.seed_consumed_vivid_lifeforce_percentage,
        args = {'map_series'},
         fields = {'harvest_seeds.consumed_vivid_lifeforce_percentage'},
         header = i18n.item_table.map_series,
         display = h.tbl.display.factory.value{color='vivid'},
         fields = {'maps.series'},
        order = 9110,
         display = h.display_value_factory{},
     },
     },
     {
     {
         arg = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_wild_lifeforce_percentage'},
         order = 1904,
         header = i18n.item_table.seed_consumed_wild_lifeforce_percentage,
        args = {'atlas_tier'},
         fields = {'harvest_seeds.consumed_wild_lifeforce_percentage'},
         header = i18n.item_table.atlas_tier,
         display = h.tbl.display.factory.value{color='wild'},
         fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
         order = 9110,
         display = h.display_atlas_tier_factory{is_level=false},
         colspan = 5,
     },
     },
     {
     {
         arg = {'seed', 'seed_required_nearby_seeds', 'seed_required_nearby_seed_amount'},
         order = 1905,
         header = i18n.item_table.seed_required_nearby_seed_amount,
        args = {'atlas_level'},
         fields = {'harvest_seeds.required_nearby_seed_amount'},
         header = i18n.item_table.atlas_level,
         display = h.tbl.display.factory.value{},
         fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
         order = 9113,
         display = h.display_atlas_tier_factory{is_level=true},
         colspan = 5,
     },
     },
    -- Map fragments 20xx
     {
     {
         arg = {'seed', 'seed_required_nearby_seeds', 'seed_required_nearby_seed_tier'},
         order = 2000,
         header = i18n.item_table.seed_required_nearby_seed_tier,
        args = {'map_fragment', 'map_fragment_limit'},
         fields = {'harvest_seeds.required_nearby_seed_tier'},
         header = i18n.item_table.map_fragment_limit,
         display = h.tbl.display.factory.value{},
         fields = {'map_fragments.map_fragment_limit'},
        order = 9114,
         display = h.display_value_factory{},
     },
     },
     -- 9120 show crafting options?
     -- Hideout decorations 21xx
     {
     {
         arg = 'jewel_limit',
         order = 2100,
         header = i18n.item_table.jewel_limit,
        args = {'doodad', 'variation_count'},
         fields = {'jewels.jewel_limit'},
         header = i18n.item_table.variation_count,
         display = h.tbl.display.factory.value{},
         fields = {'hideout_doodads.variation_count'},
         order = 10000,
         display = h.display_value_factory{
            color = 'mod',
         },
     },
     },
    -- Corpse items 22xx
     {
     {
         arg = 'jewel_radius',
         order = 2200,
         header = i18n.item_table.jewel_radius,
        args = {'corpse', 'monster_category'},
         fields = {'jewels.radius_html'},
         header = i18n.item_table.monster_category,
         fields = {'corpse_items.monster_category', 'corpse_items.monster_category_html'},
         display = function (tr, data)
         display = function (tr, data)
             tr
             tr
                 :tag('td')
                 :tag('td')
                     :wikitext(data['jewels.radius_html'])
                    :attr('data-sort-value', m_game.constants.monster.categories[data['corpse_items.monster_category']].id)
                     :wikitext(data['corpse_items.monster_category_html'])
         end,
         end,
        order = 10001,
     },
     },
     {
     {
         arg = 'map_tier',
         order = 2201,
         header = i18n.item_table.map_tier,
        args = {'corpse', 'monster_abilities'},
         fields = {'maps.tier'},
         header = i18n.item_table.monster_abilities,
         display = h.tbl.display.factory.value{},
         fields = {'corpse_items.monster_abilities'},
         order = 11000,
         display = h.display_value_factory{
            color = 'mod',
        },
         sort_type = 'text',
     },
     },
    -- Tinctures 23xx
     {
     {
         arg = 'map_level',
         order = 2300,
         header = i18n.item_table.map_level,
        args = {'tincture'},
         fields = {'maps.area_level'},
         header = i18n.item_table.tincture_debuff_interval,
         display = h.tbl.display.factory.value{},
         fields = h.range_fields_factory{fields={'tinctures.debuff_interval'}},
        order = 11010,
         display = h.display_range_factory{field='tinctures.debuff_interval'},
     },
     },
     {
     {
         arg = 'map_guild_character',
         order = 2301,
         header = i18n.item_table.map_guild_character,
        args = {'tincture'},
         fields = {'maps.guild_character'},
         header = i18n.item_table.tincture_cooldown,
         display = h.tbl.display.factory.value{},
         fields = h.range_fields_factory{fields={'tinctures.cooldown'}},
        order = 11020,
         display = h.display_range_factory{field='tinctures.cooldown'},
        sort_type = 'text',
     },
     },
    -- Seed data 91xx,
     {
     {
         arg = 'map_series',
         order = 9100,
         header = i18n.item_table.map_series,
        args = {'seed', 'seed_type'},
         fields = {'maps.series'},
         header = i18n.item_table.seed_type,
         display = h.tbl.display.factory.value{},
         fields = {'harvest_seeds.type', 'harvest_seeds.type_id'},
         order = 11030,
         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,
     },
     },
     {
     {
         arg = 'atlas_tier',
         order = 9101,
         header = i18n.item_table.atlas_tier,
        args = {'seed', 'seed_tier'},
        colspan = 5,
         header = i18n.item_table.seed_tier,
         fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
         fields = {'harvest_seeds.tier'},
         display = h.tbl.display.factory.atlas_tier{is_level=false},
         display = h.display_value_factory{},
        order = 11050,
     },
     },
     {
     {
         arg = 'atlas_level',
         order = 9102,
         header = i18n.item_table.atlas_level,
        args = {'seed', 'seed_growth_cycles'},
        colspan = 5,
         header = i18n.item_table.seed_growth_cycles,
         fields = {'atlas_maps.map_tier0', 'atlas_maps.map_tier1', 'atlas_maps.map_tier2', 'atlas_maps.map_tier3', 'atlas_maps.map_tier4'},
         fields = {'harvest_seeds.growth_cycles'},
         display = h.tbl.display.factory.atlas_tier{is_level=true},
         display = h.display_value_factory{},
        order = 11060,
     },
     },
     {
     {
         arg = {'doodad', 'variation_count'},
         order = 9110,
         header = i18n.item_table.variation_count,
        args = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_primal_lifeforce_percentage'},
         fields = {'hideout_doodads.variation_count'},
         header = i18n.item_table.seed_consumed_primal_lifeforce_percentage,
         display = h.tbl.display.factory.value{colour='mod'},
         fields = {'harvest_seeds.consumed_primal_lifeforce_percentage'},
         order = 11100,
         display = h.display_value_factory{
            color = 'primal',
         },
     },
     },
     {
     {
         arg = 'buff',
         order = 9110,
         header = i18n.item_table.buff_effects,
        args = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_vivid_lifeforce_percentage'},
         fields = {'item_buffs.stat_text'},
         header = i18n.item_table.seed_consumed_vivid_lifeforce_percentage,
         display = h.tbl.display.factory.value{colour='mod'},
         fields = {'harvest_seeds.consumed_vivid_lifeforce_percentage'},
         order = 12000,
         display = h.display_value_factory{
        sort_type = 'text',
            color = 'vivid',
         },
     },
     },
     {
     {
         arg = 'stat',
         order = 9110,
         header = i18n.item_table.stats,
        args = {'seed', 'seed_cosumed_lifeforce', 'seed_consumed_lifeforce_percentage', 'seed_consumed_wild_lifeforce_percentage'},
         fields = {'items.stat_text'},
         header = i18n.item_table.seed_consumed_wild_lifeforce_percentage,
         display = h.tbl.display.factory.value{colour='mod'},
         fields = {'harvest_seeds.consumed_wild_lifeforce_percentage'},
         order = 12001,
         display = h.display_value_factory{
        sort_type = 'text',
            color = 'wild',
         },
     },
     },
     {
     {
         arg = 'description',
         order = 9113,
         header = i18n.item_table.effects,
        args = {'seed', 'seed_required_nearby_seeds', 'seed_required_nearby_seed_amount'},
         fields = {'items.description'},
         header = i18n.item_table.seed_required_nearby_seed_amount,
         display = h.tbl.display.factory.value{colour='mod'},
         fields = {'harvest_seeds.required_nearby_seed_amount'},
        order = 12002,
         display = h.display_value_factory{},
        sort_type = 'text',
     },
     },
     {
     {
         arg = {'seed', 'seed_effect'},
         order = 9114,
         header = i18n.item_table.effects,
        args = {'seed', 'seed_required_nearby_seeds', 'seed_required_nearby_seed_tier'},
         fields = {'harvest_seeds.effect'},
         header = i18n.item_table.seed_required_nearby_seed_tier,
         display = h.tbl.display.factory.value{colour='crafted'},
         fields = {'harvest_seeds.required_nearby_seed_tier'},
        order = 12003,
         display = h.display_value_factory{},
        sort_type = 'text',
     },
     },
     {
     {
         arg = 'flavour_text',
         order = 12000,
         header = i18n.item_table.flavour_text,
        args = {'buff'},
         fields = {'items.flavour_text'},
         header = i18n.item_table.buff_effects,
         display = h.tbl.display.factory.value{colour='flavour'},
         fields = {'item_buffs.stat_text'},
         order = 12006,
         display = h.display_value_factory{
            color = 'mod',
         },
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'help_text',
         order = 12001,
         header = i18n.item_table.help_text,
        args = {'stat'},
         fields = {'items.help_text'},
         header = i18n.item_table.stats,
         display = h.tbl.display.factory.value{colour='help'},
         fields = {'items.stat_text'},
         order = 12008,
         display = h.display_value_factory{
            color = 'mod',
         },
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'prophecy', 'objective'},
         order = 12002,
         header = i18n.item_table.objective,
        args = {'description'},
         fields = {'prophecies.objective'},
         header = i18n.item_table.description,
         display = h.tbl.display.factory.value{},
         fields = {'items.description'},
         order = 13002,
         display = h.display_value_factory{
            color = 'mod',
        },
         sort_type = 'text',
     },
     },
         {
    {
         arg = {'prophecy', 'reward'},
         order = 12003,
         header = i18n.item_table.reward,
         args = {'seed', 'seed_effect'},
         fields = {'prophecies.reward'},
         header = i18n.item_table.seed_effects,
         display = h.tbl.display.factory.value{},
         fields = {'harvest_seeds.effect'},
         order = 13001,
         display = h.display_value_factory{
            color = 'crafted',
        },
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'prophecy', 'seal_cost'},
         order = 12100,
         header = i18n.item_table.seal_cost,
        args = {'flavour_text'},
         fields = {'prophecies.seal_cost'},
         header = i18n.item_table.flavour_text,
         display = h.tbl.display.factory.value{colour='currency'},
         fields = {'items.flavour_text'},
         order = 13002,
         display = h.display_value_factory{
            color = 'flavour',
        },
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'prediction_text'},
         order = 12200,
         header = i18n.item_table.prediction_text,
        args = {'help_text'},
         fields = {'prophecies.prediction_text'},
         header = i18n.item_table.help_text,
         display = h.tbl.display.factory.value{colour='value'},
         fields = {'items.help_text'},
         order = 12004,
         display = h.display_value_factory{
            color = 'help',
         },
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'buff_icon',
         order = 12300,
        args = {'buff_icon'},
         header = i18n.item_table.buff_icon,
         header = i18n.item_table.buff_icon,
         fields = {'item_buffs.icon'},
         fields = {'item_buffs.icon'},
         display = h.tbl.display.factory.value{options = {
         display = h.display_value_factory{
            [1] = {
            fmt_options = {
                fmt='[[%s]]',
                [1] = {
                    fmt = '[[%s]]',
                },
             },
             },
         }},
         },
         order = 14000,
        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',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'version', 'release_version'},
         order = 14000,
        args = {'version', 'release_version'},
         header = i18n.item_table.release_version,
         header = i18n.item_table.release_version,
         fields = {'items.release_version'},
         fields = {'items.release_version'},
Line 838: Line 921:
                     )
                     )
         end,
         end,
    },
    {
         order = 15000,
         order = 15000,
        args = {'drop', 'drop_level'},
        header = i18n.item_table.drop_level,
        fields = {'items.drop_level'},
        display = h.display_value_factory{},
     },
     },
     {
     {
         arg = {'version', 'removal_version'},
         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,
         header = i18n.item_table.removal_version,
         fields = {'items.removal_version'},
         fields = {'items.removal_version'},
Line 855: Line 952:
                     )
                     )
         end,
         end,
        order = 15001,
     },
     },
     {
     {
         arg = {'drop', 'drop_enabled'},
         order = 15003,
        args = {'drop', 'drop_enabled'},
         header = i18n.item_table.drop_enabled,
         header = i18n.item_table.drop_enabled,
         fields = {'items.drop_enabled'},
         fields = {'items.drop_enabled'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
         order = 15002,
         display = h.display_yesno_factory{
    },
             field = 'items.drop_enabled',
    {
         },
        arg = {'drop', 'drop_leagues'},
        header = i18n.item_table.drop_leagues,
        fields = {'items.drop_leagues'},
        display = function(tr, data)
             local s = m_util.string.split(data['items.drop_leagues'], ',')
            for i, v in ipairs(s) do
                s[i] = string.format(i18n.item_table.drop_leagues_link, v, v)
            end
            tr
                :tag('td')
                    :wikitext(table.concat(s, '<br>'))
        end,
         order = 15003,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'drop', 'drop_areas'},
         order = 15004,
        args = {'drop', 'drop_areas'},
         header = i18n.item_table.drop_areas,
         header = i18n.item_table.drop_areas,
         fields = {'items.drop_areas_html'},
         fields = {'items.drop_areas_html'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 15004,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'drop', 'drop_monsters'},
         order = 15005,
        args = {'drop', 'drop_monsters'},
         header = i18n.item_table.drop_monsters,
         header = i18n.item_table.drop_monsters,
         fields = {'items.drop_monsters'},
         fields = {'items.drop_monsters'},
Line 931: Line 1,016:
             h.na_or_val(tr, table.concat(tbl, '<br>'))
             h.na_or_val(tr, table.concat(tbl, '<br>'))
         end,
         end,
        order = 15005,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'drop', 'drop_text'},
         order = 15006,
        args = {'drop', 'drop_text'},
         header = i18n.item_table.drop_text,
         header = i18n.item_table.drop_text,
         fields = {'items.drop_text'},
         fields = {'items.drop_text'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 15006,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'quest'},
         order = 16000,
        args = {'quest'},
         header = i18n.item_table.quest_rewards,
         header = i18n.item_table.quest_rewards,
         fields = {'items._pageName'},
         fields = {'items._pageName'},
Line 989: Line 1,074:
             local value = table.concat(tbl, '<br>')
             local value = table.concat(tbl, '<br>')
             if value == nil or value == '' then
             if value == nil or value == '' then
                 tr:wikitext(m_util.html.td.na())
                 tr:node(m_util.html.table_cell('na'))
             else
             else
                 tr
                 tr
Line 997: Line 1,082:
             end
             end
         end,
         end,
        order = 16001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'vendor'},
         order = 17000,
        args = {'vendor'},
         header = i18n.item_table.vendor_rewards,
         header = i18n.item_table.vendor_rewards,
         fields = {'items._pageName'},
         fields = {'items._pageName'},
Line 1,049: Line 1,134:
             local value = table.concat(tbl, '<br>')
             local value = table.concat(tbl, '<br>')
             if value == nil or value == '' then
             if value == nil or value == '' then
                 tr:wikitext(m_util.html.td.na())
                 tr:node(m_util.html.table_cell('na'))
             else
             else
                 tr
                 tr
Line 1,057: Line 1,142:
             end
             end
         end,
         end,
        order = 17001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'price', 'purchase_cost'},
         order = 18000,
        args = {'price', 'purchase_cost'},
         header = i18n.item_table.purchase_costs,
         header = i18n.item_table.purchase_costs,
         fields = {'item_purchase_costs.name', 'item_purchase_costs.amount'},
         fields = {'item_purchase_costs.name', 'item_purchase_costs.amount'},
Line 1,077: Line 1,162:
             h.na_or_val(tr, table.concat(tbl, '<br>'))
             h.na_or_val(tr, table.concat(tbl, '<br>'))
         end,
         end,
        order = 18001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'price', 'sell_price'},
         order = 18001,
        args = {'price', 'sell_price'},
         header = i18n.item_table.sell_price,
         header = i18n.item_table.sell_price,
         fields = {'item_sell_prices.name', 'item_sell_prices.amount'},
         fields = {'item_sell_prices.name', 'item_sell_prices.amount'},
Line 1,119: Line 1,204:
             h.na_or_val(tr, table.concat(tbl, '<br>'))
             h.na_or_val(tr, table.concat(tbl, '<br>'))
         end,
         end,
        order = 18002,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'boss', 'boss_name'},
         order = 19000,
        args = {'boss', 'boss_name'},
         header = i18n.item_table.boss_name,
         header = i18n.item_table.boss_name,
         fields = {'maps.area_id'},
         fields = {'maps.area_id'},
Line 1,171: Line 1,256:
             h.na_or_val(tr, table.concat(tbl, '<br>'))
             h.na_or_val(tr, table.concat(tbl, '<br>'))
         end,
         end,
        order = 19001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'boss', 'boss_number'},
         order = 19001,
        args = {'boss', 'boss_number'},
         header = i18n.item_table.boss_number,
         header = i18n.item_table.boss_number,
         fields = {'maps.area_id'},
         fields = {'maps.area_id'},
Line 1,219: Line 1,304:
                 tbl[#tbl+1] = v['areas.boss_monster_ids']
                 tbl[#tbl+1] = v['areas.boss_monster_ids']
             end
             end
             h.na_or_val(tr, #tbl)
             tr
                :tag('td')
                    :attr('data-sort-value', #tbl)
                    :wikitext(#tbl)
         end,
         end,
        order = 19002,
     },
     },
     {
     {
         arg = {'legacy'},
         order = 20000,
        args = {'legacy'},
         header = i18n.item_table.legacy,
         header = i18n.item_table.legacy,
         fields = {'items.name'},
         fields = {'items.name'},
Line 1,306: Line 1,394:
                     :node(tbl)
                     :node(tbl)
         end,
         end,
        order = 21001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'granted_skills'},
         order = 21000,
        args = {'granted_skills'},
         header = i18n.item_table.granted_skills,
         header = i18n.item_table.granted_skills,
         fields = {'items.name'},
         fields = {'items.name'},
Line 1,429: Line 1,517:
             h.na_or_val(tr, table.concat(tbl, '<br>'))
             h.na_or_val(tr, table.concat(tbl, '<br>'))
         end,
         end,
        order = 22001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'alternate_art',
         order = 23000,
        args = {'alternate_art'},
         header = i18n.item_table.alternate_art,
         header = i18n.item_table.alternate_art,
         fields = {'items.alternate_art_inventory_icons'},
         fields = {'items.alternate_art_inventory_icons'},
Line 1,457: Line 1,545:
                     :wikitext(table.concat(out, ''))
                     :wikitext(table.concat(out, ''))
         end,
         end,
        order = 23000,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
}
}


data_map.skill_gem_new = {
data_map.skill_gem = {
     {
     {
         arg = 'icon',
         order = 1000,
        args = {'icon'},
         header = i18n.item_table.support_gem_letter,
         header = i18n.item_table.support_gem_letter,
         fields = {'skill_gems.support_gem_letter_html'},
         fields = {'skill_gems.support_gem_letter_html'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 1000,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'skill_icon',
         order = 1001,
        args = {'skill_icon'},
         header = i18n.item_table.skill_icon,
         header = i18n.item_table.skill_icon,
         fields = {'skill.skill_icon'},
         fields = {'skill.skill_icon'},
         display = h.tbl.display.factory.value{options = {
         display = h.display_value_factory{
            [1] = {
            fmt_options = {
                fmt='[[%s]]',
                [1] = {
                    fmt = '[[%s]]',
                },
             },
             },
         }},
         },
        order = 1001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'stat', 'stat_text'},
         order = 2000,
        args = {'stat', 'stat_text'},
         header = i18n.item_table.stats,
         header = i18n.item_table.stats,
         fields = {'skill.stat_text'},
         fields = {'skill.stat_text'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 2000,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = {'quality', 'quality_stat_text'},
         order = 2001,
        args = {'quality', 'quality_stat_text'},
         header = i18n.item_table.quality_stats,
         header = i18n.item_table.quality_stats,
         fields = {'skill.quality_stat_text'},
         fields = {'skill.quality_stat_text'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 2001,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'description',
         order = 2100,
        args = {'description'},
         header = i18n.item_table.description,
         header = i18n.item_table.description,
         fields = {'skill.description'},
         fields = {'skill.description'},
         display = h.tbl.display.factory.value{},
         display = h.display_value_factory{},
        order = 2100,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'level',
         order = 3000,
         header = m_game.level_requirement.icon,
        args = {'level'},
         fields = h.tbl.range_fields{field='items.required_level'},
         header = m_util.html.abbr(
         display = h.tbl.display.factory.range{field='items.required_level'},
            string.format(
        order = 3004,
                '[[%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'},
     },
     },
     {
     {
         arg = 'crit',
         order = 3001,
        header = i18n.item_table.skill_critical_strike_chance,
         args = {'str'},
         fields = {'skill_levels.critical_strike_chance'},
         header = m_util.html.abbr(
         display = h.tbl.display.factory.value{options = {
             string.format(
             [1] = {
                 '[[%s|link=|alt=%s]]',
                 fmt='%s%%',
                 i18n.item_table.str_icon,
                 skill_levels = true,
                i18n.item_table.str_gem
             },
             ),
         }},
            i18n.item_table.str_gem
         order = 4000,
         ),
         options = {
         fields = {'skill_gems.strength_percent'},
             [1] = {
         display = h.display_yesno_factory{
                skill_levels = true,
             field = 'skill_gems.strength_percent',
             },
             condition = h.value_greater_than_zero,
         },
         },
        sort_type = 'text',
     },
     },
     {
     {
         arg = 'cast_time',
         order = 3002,
         header = i18n.item_table.cast_time,
        args = {'dex'},
         fields = {'skill.cast_time'},
         header = m_util.html.abbr(
         display = h.tbl.display.factory.value{options = {
            string.format(
        }},
                '[[%s|link=|alt=%s]]',
        order = 4001,
                i18n.item_table.dex_icon,
        options = {
                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',
     },
     },
     {
     {
         arg = {'aspd', 'attack_speed', 'attack_speed_multiplier'},
         order = 3003,
        header = i18n.item_table.attack_speed_multiplier,
         args = {'int'},
         fields = {'skill_levels.attack_speed_multiplier'},
         header = m_util.html.abbr(
         display = h.tbl.display.factory.value{options = {
             string.format(
             [1] = {
                 '[[%s|link=|alt=%s]]',
                 fmt='%s%%',
                 i18n.item_table.int_icon,
                 skill_levels = true,
                i18n.item_table.int_gem
             },
             ),
         }},
            i18n.item_table.int_gem
         order = 4002,
         ),
         options = {
         fields = {'skill_gems.intelligence_percent'},
             [1] = {
         display = h.display_yesno_factory{
                skill_levels = true,
             field = 'skill_gems.intelligence_percent',
             },
             condition = h.value_greater_than_zero,
         },
         },
        sort_type = 'text',
     },
     },
     {
     {
         arg = 'dmgeff',
         order = 4000,
         header = i18n.item_table.damage_effectiveness,
        args = {'crit'},
         fields = {'skill_levels.damage_effectiveness'},
         header = i18n.item_table.skill_critical_strike_chance,
         display = h.tbl.display.factory.value{options = {
         fields = {'skill_levels.critical_strike_chance'},
            [1] = {
         display = h.display_value_factory{
                fmt='%s%%',
            fmt_options = {
                 skill_levels = true,
                [1] = {
                    fmt = '%s%%',
                 },
             },
             },
         }},
         },
         order = 4003,
         field_options = {
        options = {
             [1] = {
             [1] = {
                 skill_levels = true,
                 skill_levels = true,
Line 1,576: Line 1,683:
     },
     },
     {
     {
         arg = {'mcm', 'cost_multiplier'},
         order = 4001,
         header = i18n.item_table.cost_multiplier,
        args = {'cast_time'},
         fields = {'skill_levels.cost_multiplier'},
        header = i18n.item_table.cast_time,
         display = h.tbl.display.factory.value{options = {
        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] = {
             [1] = {
                fmt='%s%%',
                 skill_levels = true,
                 skill_levels = true,
             },
             },
         }},
         },
         order = 5000,
    },
         options = {
    {
         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] = {
             [1] = {
                 skill_levels = true,
                 skill_levels = true,
Line 1,593: Line 1,726:
     },
     },
     {
     {
         arg = 'mana',
         order = 5000,
         header = i18n.item_table.mana_cost,
        args = {'mcm', 'cost_multiplier'},
         fields = {'skill_levels.cost_amounts', 'skill_levels.mana_reservation_percent', 'skill_levels.mana_reservation_flat'},
        header = i18n.item_table.cost_multiplier,
         display = function (tr, data, fields, data2)
        fields = {'skill_levels.cost_multiplier'},
             local appendix = ''
        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 cost_field = ''
             local sdata = data2.skill_levels[data['items._pageName']]
             local sdata = data2.skill_levels[data['items._pageName']]
Line 1,612: Line 1,764:
                 cost_field = 'skill_levels.cost_amounts'
                 cost_field = 'skill_levels.cost_amounts'
             end
             end
 
             h.display_value_factory{
             h.tbl.display.factory.value{options = {
                fmt_options = {
                [1] = {
                    [1] = {
                    fmt='%d' .. appendix,
                        fmt = '%d' .. appendix,
                     skill_levels = true,
                     },
                 },
                 },
             }}(tr, data, {cost_field}, data2)
             }(tr, data, {cost_field}, data2)
         end,
         end,
        order = 5001,
         -- Need one set of options per field.
         -- Need one set of options per field.
         options = {
         field_options = {
             [1] = {
             [1] = {
                 skill_levels = true,
                 skill_levels = true,
Line 1,635: Line 1,786:
     },
     },
     {
     {
         arg = 'vaal',
         order = 6000,
        args = {'vaal'},
         header = i18n.item_table.vaal_souls_requirement,
         header = i18n.item_table.vaal_souls_requirement,
         fields = {'skill_levels.vaal_souls_requirement'},
         fields = {'skill_levels.vaal_souls_requirement'},
         display = h.tbl.display.factory.value{options = {
         display = h.display_value_factory{},
            [1] = {
         field_options = {
                skill_levels = true,
            },
        }},
        order = 6000,
         options = {
             [1] = {
             [1] = {
                 skill_levels = true,
                 skill_levels = true,
Line 1,651: Line 1,798:
     },
     },
     {
     {
         arg = 'vaal',
         order = 6001,
        args = {'vaal'},
         header = i18n.item_table.stored_uses,
         header = i18n.item_table.stored_uses,
         fields = {'skill_levels.vaal_stored_uses'},
         fields = {'skill_levels.vaal_stored_uses'},
         display = h.tbl.display.factory.value{options = {
         display = h.display_value_factory{},
            [1] = {
         field_options = {
                skill_levels = true,
            },
        }},
        order = 6001,
         options = {
             [1] = {
             [1] = {
                 skill_levels = true,
                 skill_levels = true,
Line 1,667: Line 1,810:
     },
     },
     {
     {
         arg = 'radius',
         order = 7000,
        args = {'radius'},
         header = i18n.item_table.primary_radius,
         header = i18n.item_table.primary_radius,
         fields = {'skill.radius', 'skill.radius_description'},
         fields = {'skill.radius', 'skill.radius_description'},
         options = {[2] = {optional = true}},
         field_options = {
            [2] = {
                optional = true,
            },
        },
         display = function (tr, data)
         display = function (tr, data)
             tr
             tr
                 :tag('td')
                 :tag('td')
                     :attr('data-sort-value', data['skill.radius'])
                     :attr('data-sort-value', data['skill.radius'])
                     :wikitext(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_description'}(nil, nil, data['skill.radius']))
                     :wikitext(h.display_descriptor_value_factory{tbl=data, key='skill.radius_description'}(data['skill.radius']))
         end,
         end,
        order = 7000,
     },
     },
     {
     {
         arg = 'radius',
         order = 7001,
        args = {'radius'},
         header = i18n.item_table.secondary_radius,
         header = i18n.item_table.secondary_radius,
         fields = {'skill.radius_secondary', 'skill.radius_secondary_description'},
         fields = {'skill.radius_secondary', 'skill.radius_secondary_description'},
         options = {[2] = {optional = true}},
         field_options = {
            [2] = {
                optional = true,
            },
        },
         display = function (tr, data)
         display = function (tr, data)
             tr
             tr
                 :tag('td')
                 :tag('td')
                     :attr('data-sort-value', data['skill.radius_secondary'])
                     :attr('data-sort-value', data['skill.radius_secondary'])
                     :wikitext(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_secondary_description'}(nil, nil, data['skill.radius_secondary']))
                     :wikitext(h.display_descriptor_value_factory{tbl=data, key='skill.radius_secondary_description'}(data['skill.radius_secondary']))
         end,
         end,
        order = 7001,
     },
     },
     {
     {
         arg = 'radius',
         order = 7002,
        args = {'radius'},
         header = i18n.item_table.tertiary_radius,
         header = i18n.item_table.tertiary_radius,
         fields = {'skill.radius_tertiary', 'skill.radius_tertiary_description'},
         fields = {'skill.radius_tertiary', 'skill.radius_tertiary_description'},
         options = {[2] = {optional = true}},
         field_options = {
            [2] = {
                optional = true,
            },
        },
         display = function (tr, data)
         display = function (tr, data)
             tr
             tr
                 :tag('td')
                 :tag('td')
                     :attr('data-sort-value', data['skill.radius_tertiary'])
                     :attr('data-sort-value', data['skill.radius_tertiary'])
                   :wikitext(h.tbl.display.factory.descriptor_value{tbl=data, key='skill.radius_tertiary_description'}(nil, nil, data['skill.radius_tertiary']))
                   :wikitext(h.display_descriptor_value_factory{tbl=data, key='skill.radius_tertiary_description'}(data['skill.radius_tertiary']))
         end,
         end,
        order = 7002,
     },
     },
}
}
for i, attr in ipairs(m_game.constants.attribute_order) do
    local attr_data = m_game.constants.attributes[attr]
    table.insert(data_map.generic_item, 7, {
        arg = attr_data.arg,
        header = attr_data.icon,
        fields = h.tbl.range_fields{field=string.format('items.required_%s', attr)},
        display = h.tbl.display.factory.range{field=string.format('items.required_%s', attr)},
        order = 5000+i,
    })
    table.insert(data_map.skill_gem_new, 1, {
        arg = attr_data.arg,
        header = attr_data.icon,
        fields = {string.format('skill_gems.%s_percent', attr)},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data[string.format('skill_gems.%s_percent', attr)])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3000+i,
    })
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Exported functions
-- Main functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


local p = {}
local function _item_table(args)
 
--
-- Template:Item table
--
 
function p.item_table(frame)
     --[[
     --[[
     Creates a generic table for items.
     Creates a generic table for items.
Line 1,758: Line 1,883:


     local t = os.clock()
     local t = os.clock()
    -- args
    local tpl_args = getArgs(frame, {
            parentFirst = true
        })
    frame = m_util.misc.get_frame(frame)


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


     if tpl_args.mode == nil then
     -- Handle deprecated "q_" args
         tpl_args.mode = 'item'
    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
     end


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


Line 1,790: Line 1,926:


     local row_infos = {}
     local row_infos = {}
     for _, row_info in ipairs(modes[tpl_args.mode].data) do
     for _, row_info in ipairs(modes[args.mode].data) do
         local enabled = false
         local enabled = false
         if row_info.arg == nil then
         if type(row_info.args) == 'table' then
            enabled = true
             for _, a in ipairs(row_info.args) do
        elseif type(row_info.arg) == 'string' and m_util.cast.boolean(tpl_args[row_info.arg]) then
                 if m_util.cast.boolean(args[a]) then
            enabled = true
        elseif type(row_info.arg) == 'table' then
             for _, argument in ipairs(row_info.arg) do
                 if m_util.cast.boolean(tpl_args[argument]) then
                     enabled = true
                     enabled = true
                     break
                     break
                 end
                 end
             end
             end
        else
            enabled = true
         end
         end
         if enabled then
         if enabled then
             row_info.options = row_info.options or {}
             row_info.field_options = row_info.field_options or {}
             row_infos[#row_infos+1] = row_info
             row_infos[#row_infos+1] = row_info
         end
         end
Line 1,814: Line 1,947:
     local stat_columns = {}
     local stat_columns = {}
     local query_stats = {}
     local query_stats = {}
     local i = 0
     for i=1, math.huge do -- repeat until no more columns are found
    repeat
        i = i + 1
 
         local prefix = string.format('stat_column%s_', i)
         local prefix = string.format('stat_column%s_', i)
        if args[prefix .. 'stat1_id'] == nil then
            -- Each column requires at least one stat id
            break
        end
         local col_info = {
         local col_info = {
             header = tpl_args[prefix .. 'header'] or tostring(i),
             header = args[prefix .. 'header'] or tostring(i),
             format = tpl_args[prefix .. 'format'],
             format = args[prefix .. 'format'],
             stat_format = tpl_args[prefix .. 'stat_format'] or 'separate',
             stat_format = args[prefix .. 'stat_format'] or 'separate',
             order = tonumber(tpl_args[prefix .. 'order']) or (10000000 + i),
             order = tonumber(args[prefix .. 'order']) or (10000000 + i),
             stats = {},
             stats = {},
            options = {},
         }
         }
 
         for j=1, math.huge do
        local j = 0
             local stat_id = args[string.format('%sstat%s_id', prefix, j)]
         repeat
             if stat_id == nil then
            j = j +1
                 break
 
             local stat_info = {
                id = tpl_args[string.format('%sstat%s_id', prefix, j)],
            }
 
            if stat_info.id then
                col_info.stats[#col_info.stats+1] = stat_info
                query_stats[stat_info.id] = {}
             else
                -- Stop iteration entirely if this was the first index but no stat was supplied. We assume that we stop in this case.
                if j == 1 then
                    i = nil
                end
                -- stop iteration
                 j = nil
             end
             end
        until j == nil
            table.insert(col_info.stats, {id=stat_id})
 
             query_stats[stat_id] = true
        -- Don't add this column if no stats were provided.
        if #col_info.stats > 0 then
             stat_columns[#stat_columns+1] = col_info
         end
         end
     until i == nil
        table.insert(stat_columns, col_info)
     end


     for _, col_info in ipairs(stat_columns) do
     for _, col_info in ipairs(stat_columns) do
         local row_info = {
         local row_info = {
            --arg
             header = col_info.header,
             header = col_info.header,
             fields = {},
             fields = {},
             display = function(tr, data, properties)
             display = function (tr, data)
                 if col_info.stat_format == 'separate' then
                 if col_info.stat_format == 'separate' then
                     local stat_texts = {}
                     local stat_texts = {}
Line 1,870: Line 1,985:
                         local stat = (results2.stats[data['items._pageName']] or {})[stat_info.id]
                         local stat = (results2.stats[data['items._pageName']] or {})[stat_info.id]
                         if stat ~= nil then
                         if stat ~= nil then
                             stat_texts[#stat_texts+1] = m_util.html.format_value(tpl_args, stat, {no_color=true})
                             stat_texts[#stat_texts+1] = m_util.html.format_value(args, stat, {color=false})
                             vmax = vmax + stat.max
                             vmax = vmax + stat.max
                         end
                         end
Line 1,876: Line 1,991:


                     if num_stats ~= #stat_texts then
                     if num_stats ~= #stat_texts then
                         tr:wikitext(m_util.html.td.na())
                         tr:node(m_util.html.table_cell('na'))
                     else
                     else
                         local text
                         local text
Line 1,912: Line 2,027:
                         :attr('data-sort-value', total_stat.max)
                         :attr('data-sort-value', total_stat.max)
                         :attr('class', 'tc -mod')
                         :attr('class', 'tc -mod')
                         :wikitext(string.format(col_info.format, m_util.html.format_value(tpl_args, total_stat, {no_color=true})))
                         :wikitext(string.format(col_info.format, m_util.html.format_value(args, total_stat, {no_color=true})))
                 else
                 else
                     error(string.format(i18n.errors.generic_argument_parameter, 'stat_format', col_info.stat_format))
                     error(string.format(i18n.errors.generic_argument_parameter, 'stat_format', col_info.stat_format))
Line 1,927: Line 2,042:
     end)
     end)


     -- Parse query arguments
     -- Build Cargo query
     local tables_assoc = {items=true}
     local tables = {'items'}
     local fields = {
     local fields = {
         'items._pageID',
         'items._pageID',
Line 1,938: Line 2,053:
         'items.size_y',
         'items.size_y',
     }
     }
 
    local query = {
    --
        where = args.where,
    local prepend = {
        groupBy = table.concat({'items._pageID', args.groupBy}, ', '),
         q_groupBy=true,
        having = args.having,
         q_tables=true,
        orderBy = args.orderBy,
         limit = args.limit,
         offset = args.offset,
     }
     }
    local query = {}
    for key, value in pairs(tpl_args) do
        if string.sub(key, 0, 2) == 'q_' then
            if prepend[key] then
                value = ',' .. value
            end
            query[string.sub(key, 3)] = value
        end
    end


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


    -- Minimum required tables and fields, based on display options
     local skill_levels = {}
     local skill_levels = {}
     for _, rowinfo in ipairs(row_infos) do
     for _, rowinfo in ipairs(row_infos) do
Line 1,970: Line 2,082:
         end
         end
         for index, field in ipairs(rowinfo.fields) do
         for index, field in ipairs(rowinfo.fields) do
             rowinfo.options[index] = rowinfo.options[index] or {}
             rowinfo.field_options[index] = rowinfo.field_options[index] or {}
             if rowinfo.options[index].skill_levels then
             if rowinfo.field_options[index].skill_levels then
                 skill_levels[#skill_levels+1] = field
                 skill_levels[#skill_levels+1] = field
             else
             else
                 fields[#fields+1] = field
                 fields[#fields+1] = field
                 tables_assoc[m_util.string.split(field, '%.')[1]] = true
                 tables[#tables+1] = m_util.string.split(field, '.', true)[1]
             end
             end
         end
         end
     end
     end
     if #skill_levels > 0 then
     if #skill_levels > 0 then
         fields[#fields+1] = 'skill.max_level'
         fields[#fields+1] = 'skill.max_level'
        tables_assoc.skill = true
         tables[#tables+1] = 'skill'
    end
 
    -- Reformat the tables and fields so they can be retrieved correctly:
    local tables = {}
    for table_name,_ in pairs(tables_assoc) do
         tables[#tables+1] = table_name
    end
    local tbls = table.concat(tables,',') .. (query.tables or '')
    query.tables = m_util.string.split(tbls, ',')
 
    for index, field in ipairs(fields) do
        fields[index] = string.format('%s=%s', field, field)
     end
     end
     query.fields = fields
     tables = m_util.table.remove_duplicates(tables)


     -- Take care of the minimum required joins, joins from templates
     -- Minimum required joins, based on display options
    -- must still be userdefined:
     local joins = {}
     local joins = {}
     for index, table_name in ipairs(tables) do
     for _, table_name in ipairs(tables) do
         if table_name ~= 'items' then
         if table_name ~= 'items' then
             joins[#joins+1] = string.format('items._pageID=%s._pageID', table_name)
             joins[#joins+1] = string.format('items._pageID=%s._pageID', table_name)
         end
         end
     end
     end
     if #joins > 0 and query.join then
 
        query.join = table.concat(joins, ',') .. ',' .. query.join
     -- Append additional tables
    elseif #joins > 0 and not query.join then
    args.tables = m_util.cast.table(args.tables)
         query.join = table.concat(joins, ',')
    if type(args.tables) == 'table' and #args.tables > 0 then
    elseif #joins == 0 and query.join then
         tables = m_util.table.merge(tables, args.tables)
        -- leave query.join as is
     end
     end


     -- Needed to eliminate duplicates supplied via table joins:
     -- Make join clause
    query.groupBy = 'items._pageID' .. (query.groupBy or '')
    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:
     -- Query results
     local results = m_cargo.query(query.tables, query.fields, query)
     local results = m_cargo.query(tables, fields, query)


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


Line 2,057: Line 2,157:


         if #stat_columns > 0 then
         if #stat_columns > 0 then
             local pages = {}
             local stat_results = m_cargo.query(
            for _, row in ipairs(results) do
                pages[#pages+1] = string.format('item_stats._pageID="%s"', row['items._pageID'])
            end
 
            local query_stat_ids = {}
            for stat_id, _ in pairs(query_stats) do
                query_stat_ids[#query_stat_ids+1] = string.format('item_stats.id="%s"', stat_id)
            end
 
            if tpl_args.q_where then
                tpl_args.q_where = string.format(' AND (%s)', tpl_args.q_where)
            else
                tpl_args.q_where = ''
            end
 
            local temp = m_cargo.query(
                 {'items', 'item_stats'},
                 {'items', 'item_stats'},
                 {'item_stats._pageName', 'item_stats.id', 'item_stats.min', 'item_stats.max', 'item_stats.avg'},
                 {'item_stats._pageName', 'item_stats.id', 'item_stats.min', 'item_stats.max', 'item_stats.avg'},
                 {
                 {
                     where=string.format('item_stats.is_implicit <= "" AND (%s) AND (%s)', table.concat(query_stat_ids, ' OR '), table.concat(pages, ' OR ')),
                    join = 'items._pageID=item_stats._pageID',
                     join='items._pageID=item_stats._pageID',
                     where = string.format(
                    -- Cargo workaround: avoid duplicates using groupBy
                        'item_stats._pageID IN (%s) AND item_stats.id IN ("%s")',
                     groupBy='items._pageID, item_stats.id',
                        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
             for _, row in ipairs(temp) do
                 local stat = {
                 local stat = {
                     min = tonumber(row['item_stats.min']),
                     min = tonumber(row['item_stats.min']),
Line 2,099: Line 2,185:
         end
         end
     end
     end


     --
     --
Line 2,107: Line 2,192:
     local tbl = mw.html.create('table')
     local tbl = mw.html.create('table')
     tbl:attr('class', 'wikitable sortable item-table')
     tbl:attr('class', 'wikitable sortable item-table')
     if m_util.cast.boolean(tpl_args.responsive) then
     if m_util.cast.boolean(args.responsive) then
         tbl:addClass('responsive-table')
         tbl:addClass('responsive-table')
     end
     end
Line 2,115: Line 2,200:
     tr
     tr
         :tag('th')
         :tag('th')
             :wikitext(modes[tpl_args.mode].header)
             :wikitext(modes[args.mode].header)
             :done()
             :done()
     for _, row_info in ipairs(row_infos) do
     for _, row_info in ipairs(row_infos) do
Line 2,142: Line 2,227:
         }
         }


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


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


Line 2,162: Line 2,247:
                 -- 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 or row[field] == '' then
                 if row[field] == nil or row[field] == '' then
                     local opts = rowinfo.options[index]
                     local options = rowinfo.field_options[index]
                     if opts.optional ~= true and opts.skill_levels ~= true then
                     if options.optional ~= true and options.skill_levels ~= true then
                         display = false
                         display = false
                         break
                         break
Line 2,174: Line 2,259:
                 rowinfo.display(tr, row, rowinfo.fields, results2)
                 rowinfo.display(tr, row, rowinfo.fields, results2)
             else
             else
                 tr:wikitext(m_util.html.td.na())
                 tr:node(m_util.html.table_cell('na'))
             end
             end
         end
         end
Line 2,183: Line 2,268:
         cats[#cats+1] = i18n.categories.query_limit
         cats[#cats+1] = i18n.categories.query_limit
     end
     end
     if #results == 0 then
     if #results == 0 then
         cats[#cats+1] = i18n.categories.no_results
         cats[#cats+1] = i18n.categories.no_results
     end
     end


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


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


Line 2,198: Line 2,282:
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------


function p.map_item_drops(frame)
local function _map_item_drops(args)
     --[[
     --[[
     Gets the area id from the map item and activates
     Gets the area id from the map item and activates
Line 2,206: Line 2,290:
     = p.map_item_drops{page='Underground River Map (War for the Atlas)'}
     = p.map_item_drops{page='Underground River Map (War for the Atlas)'}
     ]]
     ]]
    -- Get args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)


     local tables = {'maps'}
     local tables = {'maps'}
Line 2,221: Line 2,299:
         groupBy = 'maps._pageName',
         groupBy = 'maps._pageName',
     }
     }
     if tpl_args.page then
     if args.page then
         -- Join with _pageData in order to check for page redirect
         -- Join with _pageData in order to check for page redirect
         tables[#tables+1] = '_pageData'
         tables[#tables+1] = '_pageData'
Line 2,227: Line 2,305:
         query.where = string.format(
         query.where = string.format(
             '_pageData._pageName="%s"',
             '_pageData._pageName="%s"',
             tpl_args.page
             args.page
         )
         )
         query.join = 'maps._pageName = _pageData._pageNameOrRedirect'
         query.join = 'maps._pageName = _pageData._pageNameOrRedirect'
Line 2,242: Line 2,320:
         id = results[1]['maps.area_id']
         id = results[1]['maps.area_id']
     end
     end
     return frame:expandTemplate{ title = 'Area item drops', args = {area_id=id} }
     return mw.getCurrentFrame():expandTemplate{ title = 'Area item drops', args = {area_id=id} }
end
end


Line 2,249: Line 2,327:
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------


function p.prophecy_description(frame)
local function _prophecy_description(args)
     -- Get args
     args.page = args.page or tostring(mw.title.getCurrentTitle())
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
 
    tpl_args.page = tpl_args.page or tostring(mw.title.getCurrentTitle())


     local results = m_cargo.query(
     local results = m_cargo.query(
Line 2,262: Line 2,334:
         {'prophecies.objective', 'prophecies.reward'},
         {'prophecies.objective', 'prophecies.reward'},
         {
         {
             where=string.format('prophecies._pageName="%s"', tpl_args.page),
             where=string.format('prophecies._pageName="%s"', args.page),
             -- Only need each page name once
             -- Only need each page name once
             groupBy='prophecies._pageName',
             groupBy='prophecies._pageName',
Line 2,286: Line 2,358:




function p.simple_item_list(frame)
local function _simple_item_list(args)
     --[[
     --[[
     Creates a simple list of items.
     Creates a simple list of items.
Line 2,300: Line 2,372:
     }
     }
     ]]
     ]]
    -- Args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)


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


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


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


     local tables = m_util.string.split(tpl_args.q_tables or '', ',%s*')
     local tables = m_util.cast.table(args.q_tables)
     table.insert(tables, 1, 'items')
     table.insert(tables, 1, 'items')


Line 2,341: Line 2,407:
     local out = {}
     local out = {}
     for _, row in ipairs(results) do
     for _, row in ipairs(results) do
         local page
         local page
         if tpl_args.use_name_as_link ~= nil then
         if args.use_name_as_link ~= nil then
             page = row['items.name']
             page = row['items.name']
         else
         else
             page = row['items._pageName']
             page = row['items._pageName']
         end
         end
 
 
         local link = h.item_link{
         local link = h.item_link{
             page=page,
             page=page,
             name=row['items.name'],
             name=row['items.name'],
             inventory_icon=row['items.inventory_icon'] or '',
             inventory_icon=row['items.inventory_icon'] or '',
             html=row['items.html'] or '',
             html=row['items.html'] or '',
             skip_query=true
             skip_query=true
         }
         }
 
 
         if tpl_args.format == nil then
         if args.format == nil then
             out[#out+1] = string.format('* %s', link)
             out[#out+1] = string.format('* %s', link)
         elseif tpl_args.format == 'none' then
         elseif args.format == 'none' then
             out[#out+1] = link
             out[#out+1] = link
         elseif tpl_args.format == 'li' then
         elseif args.format == 'li' then
             out[#out+1] = string.format('<li>%s</li>', link)
             out[#out+1] = string.format('<li>%s</li>', link)
         else
         else
             error(string.format(i18n.errors.generic_argument_parameter, 'format', tpl_args.format))
             error(string.format(i18n.errors.generic_argument_parameter, 'format', args.format))
         end
         end
     end
     end
 
 
     if tpl_args.format == nil then
     if args.format == nil then
         return table.concat(out, '\n')
         return table.concat(out, '\n')
     elseif tpl_args.format == 'none' then
     elseif args.format == 'none' then
         return table.concat(out, '\n')
         return table.concat(out, '\n')
     elseif tpl_args.format == 'li' then
     elseif args.format == 'li' then
         return table.concat(out)
         return table.concat(out)
     end
     end
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 stuff
-- Debug
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
p.debug = {}
p.debug = {}
Line 2,406: Line 2,504:


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


return p
return p

Latest revision as of 23:10, 25 October 2024

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


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

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)])
                :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',
    },
    -- Tinctures 23xx
    {
        order = 2300,
        args = {'tincture'},
        header = i18n.item_table.tincture_debuff_interval,
        fields = h.range_fields_factory{fields={'tinctures.debuff_interval'}},
        display = h.display_range_factory{field='tinctures.debuff_interval'},
    },
    {
        order = 2301,
        args = {'tincture'},
        header = i18n.item_table.tincture_cooldown,
        fields = h.range_fields_factory{fields={'tinctures.cooldown'}},
        display = h.display_range_factory{field='tinctures.cooldown'},
    },
    -- 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 = {'icon'},
        header = i18n.item_table.support_gem_letter,
        fields = {'skill_gems.support_gem_letter_html'},
        display = h.display_value_factory{},
        sort_type = 'text',
    },
    {
        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._pageID',
        'items._pageName',
        'items.name',
        'items.inventory_icon',
        'items.html',
        'items.size_x',
        'items.size_y',
    }
    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)

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

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

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

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

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

    --
    -- Display the table
    --

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

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

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

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

    -- Rows:
    for _, row in ipairs(results) do
        tr = tbl:tag('tr')

        local il_args = {
            skip_query=true,
            page=row['items._pageName'],
            name=row['items.name'],
            inventory_icon=row['items.inventory_icon'],
            width=row['items.size_x'],
            height=row['items.size_y'],
        }

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

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

        tr
            :tag('td')
                :wikitext(h.item_link(il_args))
                :done()

        for _, rowinfo in ipairs(row_infos) do
            -- this has been cast from a function in an earlier step
            local display = true
            
            for index, field in ipairs(rowinfo.fields) do
                -- this will bet set to an empty value not nil confusingly
                if row[field] == nil or row[field] == '' then
                    local 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',
        'items.name',
        'items.class',
    }

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

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

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

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

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

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

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

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

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

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

local p = {}

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

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

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

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

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

function p.debug._tbl_data(tbl)
    keys = {}
    for _, data in ipairs(tbl) do
        if type(data.arg) == 'string' then
            keys[data.arg] = 1
        elseif type(data.arg) == 'table' then
            for _, arg in ipairs(data.arg) do
                keys[arg] = 1
            end
        end
    end

    local out = {}
    for key, _ in pairs(keys) do
        out[#out+1] = string.format("['%s'] = '1'", key)
    end

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

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

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

return p