Module:Item2/sandbox: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
Line 35: Line 35:
--
--
-- remove the ul if name_list is not provided
-- remove the ul if name_list is not provided
-- maybe smw


require('Module:No globals')
require('Module:No globals')
Line 41: Line 40:
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_skill = require('Module:Skill/sandbox')


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


-- Lazy loading
-- Lazy loading
local f_skill -- require('Module:Skill')._skill
local f_item_link -- require('Module:Item link').item_link
local f_item_link -- require('Module:Item link').item_link
local f_query_area_info -- require('Module:Area').query_area_info
local f_query_area_info -- require('Module:Area').query_area_info
Line 61: Line 60:


local core = use_sandbox and require('Module:Item2/core/sandbox') or require('Module:Item2/core')
local core = use_sandbox and require('Module:Item2/core/sandbox') or require('Module:Item2/core')
local c = {}
c.item_classes = {}


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 70: Line 66:


local h = {}
local h = {}
-- Lazy loading for Module:Skill
function h.skill(tpl_args, frame)
    if not f_skill then
        f_skill = use_sandbox and require('Module:Skill/sandbox')._skill or require('Module:Skill')._skill
    end
    return f_skill(args)
end


-- Lazy loading for Module:Item link
-- Lazy loading for Module:Item link
Line 88: Line 92:


-- Lazy loading for Module:Item2/cargo
-- Lazy loading for Module:Item2/cargo
function h.build_cargo_data(tpl_args, frame, item_classes)
function h.build_cargo_data(tpl_args, frame, tables)
     if not f_build_cargo_data then
     if not f_build_cargo_data then
         f_build_cargo_data = use_sandbox and require('Module:Item2/cargo/sandbox').build_cargo_data or require('Module:Item2/cargo').build_cargo_data
         f_build_cargo_data = use_sandbox and require('Module:Item2/cargo/sandbox').build_cargo_data or require('Module:Item2/cargo').build_cargo_data
     end
     end
     return f_build_cargo_data(tpl_args, frame, item_classes)
     return f_build_cargo_data(tpl_args, frame, tables)
end
end


Line 190: Line 194:


function h.process_arguments(tpl_args, frame, params)
function h.process_arguments(tpl_args, frame, params)
    tpl_args._processed_args = tpl_args._processed_args or {}
     for _, k in ipairs(params) do
     for _, k in ipairs(params) do
         local data = core.map[k]
         local data = core.map[k]
Line 204: Line 209:
             end
             end
             if tpl_args[k] == nil then
             if tpl_args[k] == nil then
                 if tpl_args.class_id and c.item_classes[tpl_args.class_id].defaults ~= nil and c.item_classes[tpl_args.class_id].defaults[k] ~= nil then
                 if tpl_args.class_id and tpl_args._item_config.defaults ~= nil and tpl_args._item_config.defaults[k] ~= nil then
                     tpl_args[k] = c.item_classes[tpl_args.class_id].defaults[k]
                    -- Item defaults
                     tpl_args[k] = tpl_args._item_config.defaults[k]
                 elseif data.default ~= nil then
                 elseif data.default ~= nil then
                    -- Generic defaults
                     if type(data.default) == 'function' then
                     if type(data.default) == 'function' then
                         tpl_args[k] = data.default(tpl_args, frame)
                         tpl_args[k] = data.default(tpl_args, frame)
Line 215: Line 222:
             end
             end
         end
         end
        table.insert(tpl_args._processed_args, k)
     end
     end
end
end


-- add defaults from class specifics and class groups
--
function h.build_item_classes(tpl_args, frame)
-- Display
    core.map.class.func(tpl_args, frame)
--
   
    c.item_classes[tpl_args.class_id] = {
        tables = {},
        args = {},
        late_args = {},
        defaults = {},
    }
   
    for _, table_name in ipairs(cfg.tables) do
        table.insert(c.item_classes[tpl_args.class_id].tables, table_name)
    end


    local item_classes_extend = {'tables', 'args', 'late_args'}
function h.strip_random_stats(tpl_args, frame, stat_text, container_type)
      
     if tpl_args._flags.random_mods and container_type == 'inline' then
    for _, row in pairs(cfg.class_groups) do
         repeat
         for class, _ in pairs(row.keys) do
             local text = string.match(stat_text, '<th class="mw%-customtoggle%-31">(.+)</th>')
             if class == tpl_args.class_id then
            if text ~= nil then
                for _, k in ipairs(item_classes_extend) do
                stat_text = string.gsub(stat_text, '<table class="random%-modifier%-stats.+</table>', text, 1)
                    if row[k] ~= nil then
                        for _, value in ipairs(row[k]) do
                            table.insert(c.item_classes[tpl_args.class_id][k], value)
                        end
                    end
                end
                break
             end
             end
         end
         until text == nil
    end
 
    local class_specifics = cfg.class_specifics[tpl_args.class_id]
    if class_specifics then
        for _, k in ipairs(item_classes_extend) do
            if class_specifics[k] ~= nil then
                for _, value in ipairs(class_specifics[k]) do
                    table.insert(c.item_classes[tpl_args.class_id][k], value)
                end
            end
        end
        if class_specifics.defaults ~= nil then
            for key, value in pairs(class_specifics.defaults) do
                c.item_classes[tpl_args.class_id].defaults[key] = value
            end
      end
     end
     end
    return stat_text
end
end


function h.process_mods(tpl_args, frame)
function h.add_to_container_from_map(tpl_args, frame, container, mapping, container_type)
    -- If the item does not have its own implicit mods, fall back to the implicit mods on the base item.
     local statcont = mw.html.create('span')
     local implicit_mods = tpl_args._defined_implicit_mods
     statcont
     if #implicit_mods == 0 then
        :attr('class', 'item-stats')
         implicit_mods = tpl_args._base_implicit_mods
         :done()
     end
     local count = 0 -- Number of groups in container
     for _, v in ipairs(implicit_mods) do
     for _, group in ipairs(mapping) do
        table.insert(tpl_args._mods, v)
         local lines = {}
    end
         if group.func == nil then
    if #tpl_args._mods > 0 then
            for _, line in ipairs(group) do
         local mods = {}
                local show = true
         local mod_ids = {}
                if container_type == 'inline' and line.inline == false then -- TODO: This is not used currently. Need to address what is hidden in inline view.
        local non_random_mod_ids = {}
                    show = false
        for _, mod_data in ipairs(tpl_args._mods) do
                 elseif line.show == false then
            if mod_data.result == nil then
                    show = false
                 mods[mod_data.id] = mod_data
                 elseif type(line.show) == 'function' then
                 mod_ids[#mod_ids+1] = mod_data.id
                    show = line.show(tpl_args, frame, container_type)
                 if not mod_data.is_random then
                end
                     table.insert(non_random_mod_ids, mod_data.id)
                 if show then
                     lines[#lines+1] = line.func(tpl_args, frame, container_type)
                 end
                 end
             end
             end
             tpl_args._subobjects[#tpl_args._subobjects+1] = {
        else
                _table = 'item_mods',
             lines = group.func(tpl_args, frame, container_type)
                id = mod_data.id,
                text = mod_data.stat_text,
                is_implicit = mod_data.is_implicit,
                is_random = mod_data.is_random,
            }
         end
         end
          
         if #lines > 0 then
        local results = m_cargo.array_query{
            count = count + 1
             tables={'mods'},
             local heading = ''
             fields={'mods._pageName', 'mods.id', 'mods.required_level', 'mods.stat_text'},
             if group.heading == nil then
             id_field='mods.id',
             elseif type(group.heading) == 'function' then
            id_array=mod_ids,
                heading = group.heading()
        }
             else
       
                heading = string.format('<em class="header">%s</em><br>', group.heading)
        for _, data in ipairs(results) do
             end
             local mod_data = mods[data['mods.id']]
             statcont
            mod_data.result = data
                 :tag('span')
              
                 :attr('class', 'group ' .. (group.class or ''))
             if mod_data.is_random == false then
                 :wikitext(heading .. table.concat(lines, '<br>'))
                -- update item level requirement
                 :done()
                 local keys = {'required_level_final'}
                 -- only update base item requirement if this is an implicit
                if mod_data.key == 'implicit' then
                    keys[#keys+1] = 'required_level'
                 end
               
                for _, key in ipairs(keys) do
                    local req = math.floor(tonumber(data['mods.required_level']) * cfg.item_required_level_modifier_contribution)
                    if req > tpl_args[key] then
                        tpl_args[key] = req
                    end
                 end
            end
         end
         end
       
    end
        -- fetch stats
 
       
    -- Add groups to container
         results = m_cargo.query(
    if count > 0 then
            {'mods', 'mod_stats'},
         container:node(statcont)
            {'mods.id', 'mod_stats.id', 'mod_stats.min', 'mod_stats.max'},
    end
            {
end
                join='mods._pageID=mod_stats._pageID',
 
                where=string.format('mod_stats.id IS NOT NULL AND mods.id IN ("%s")', table.concat(mod_ids, '", "')),
function h.make_main_container(tpl_args, frame, container_type)
            }
    local container = mw.html.create('span')
         )
         :attr('class', 'item-box -' .. tpl_args.frame_type)
        for _, data in ipairs(results) do
   
            -- Stat subobject
    if tpl_args.class_id == 'DivinationCard' then
            local mod_data = mods[data['mods.id']]
         container
            if mod_data.result.stats == nil then
             :tag('span')
                mod_data.result.stats = {data, }
                 :attr('class', 'divicard-wrapper')
            else
                 :tag('span')
                mod_data.result.stats[#mod_data.result.stats+1] = data
                    :attr('class', 'divicard-art')
            end
                    :wikitext( '[[' .. tpl_args.card_art .. '|link=|alt=]]' )
          
                    :done()
             local id = data['mod_stats.id']
                 :tag('span')
            local value = {
                    :attr('class', 'divicard-frame')
                 min = tonumber(data['mod_stats.min']),
                    :wikitext( '[[File:Divination card frame.png|link=|alt=]]' )
                 max = tonumber(data['mod_stats.max']),
                    :done()
            }
                 :tag('span')
            value.avg = (value.min+value.max)/2
                    :attr('class', 'divicard-header')
           
                    :wikitext(tpl_args.name)
            local prefix = ''
                    :done()
            if mod_data.is_random then
                 :tag('span')
                prefix = '_random'
                     :attr('class', 'divicard-stack')
            end
                     :wikitext(tpl_args.stack_size)
            core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_stats')
                    :done()
            if mod_data.is_implicit then
                :tag('span')
                 core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_implicit_stats')
                    :attr('class', 'divicard-reward')
            else
                    :tag('span')
                core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_explicit_stats')
                        :wikitext(tpl_args.description)
            end
                        :done()
        end
                    :done()
       
                 :tag('span')
        if tpl_args._flags.sell_prices_override ~= true then
                     :attr('class', 'divicard-flavour text-color -flavour')
            -- fetch sell prices
                    :tag('span')
            results = m_cargo.query(
                        :wikitext(tpl_args.flavour_text)
                 {'mods', 'mod_sell_prices'},
                        :done()
                {'mods.id', 'mod_sell_prices.amount', 'mod_sell_prices.name'},
                     :done()
                 {
                 :done()
                     join='mods._pageID=mod_sell_prices._pageID',
        --TODO Extras?
                    -- must be non random mods to avoid accumulating sell prices of randomized modifiers
    else
                     where=string.format('mod_sell_prices.amount IS NOT NULL AND mods.id IN ("%s")', table.concat(non_random_mod_ids, '", "')),
        local header_css
                }
        local line_type
            )
        if tpl_args.base_item and tpl_args.rarity_id ~= 'normal' then
           
            line_type = 'double'
            for _, data in ipairs(results) do
         else
                 local mod_data = mods[data['mods.id']]
             line_type = 'single'
                if not mod_data.is_implicit then
                     local values = {
                        name = data['mod_sell_prices.name'],
                        amount = tonumber(data['mod_sell_prices.amount']),
                    }
                    -- sell_prices is defined in tpl_args.sell_prices_override
                     tpl_args.sell_prices[values.name] = (tpl_args.sell_prices[values.name] or 0) + values.amount
                 end
            end
        end
    end
    if tpl_args._flags.sell_prices_override ~= true then
        local missing_sell_price = true
         for _, _ in pairs(tpl_args.sell_prices) do
             missing_sell_price = false
            break
         end
         end
          
          
         if missing_sell_price then
         local name_line = tpl_args.name
            tpl_args.sell_prices[i18n.tooltips.default_vendor_offer] = 1
         if tpl_args.base_item and not tpl_args._flags.is_prophecy then
         end
             name_line = name_line .. '<br>' .. tpl_args.base_item
       
        -- Set sell price on page
        for name, amount in pairs(tpl_args.sell_prices) do
            -- sell_price_order is defined in tpl_args.sell_prices_override
             tpl_args.sell_price_order[#tpl_args.sell_price_order+1] = name
            tpl_args._subobjects[#tpl_args._subobjects+1] = {
                _table = 'item_sell_prices',
                amount = amount,
                name = name,
            }
         end
         end
        table.sort(tpl_args.sell_price_order)
    end
end


function h.process_base_item(tpl_args, frame)
        -- Symbols - These are displayed in the item box header to indicate certain flags and/or item influences
    local where
        local symbols
    if tpl_args.base_item_id ~= nil then
        local influences = tpl_args.influences or {}
         where = string.format('items.metadata_id="%s"', tpl_args.base_item_id)
        if tpl_args.is_replica then
    elseif tpl_args.base_item_page ~= nil then
            symbols = {'replica', 'replica'}
        where = string.format('items._pageName="%s"' , tpl_args.base_item_page)
            if #influences > 0 then
    elseif tpl_args.base_item ~= nil then
                symbols[2] = influences[1]
         where = string.format('items.name="%s"' , tpl_args.base_item)
            end
    elseif tpl_args.rarity_id ~= 'normal' then
         elseif #influences > 0 then
        error(i18n.errors.missing_base_item)
            symbols = {influences[1], influences[1]}
    else
            if #influences > 1 then
         return
                symbols[2] = influences[2]
            end
        elseif tpl_args.is_fractured then
            symbols = {'fractured', 'fractured'}
        elseif tpl_args.is_synthesised then
            symbols = {'synthesised', 'synthesised'}
        elseif tpl_args.is_veiled then
            symbols = {'veiled', 'veiled'}
        end
       
         container
            :tag('span')
                :attr( 'class', 'header -' .. line_type )
                :tag('span')
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[1] or '') )
                    :done()
                :wikitext(name_line)
                :tag('span')
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[2] or '') )
                    :done()
            :done()
           
         h.add_to_container_from_map(tpl_args, frame, container, c.item_infobox_groups, container_type)
     end
     end
      
      
     if where ~= nil and tpl_args.rarity_id == 'normal' and not tpl_args._flags.is_prophecy then
     if tpl_args.skill_icon ~= nil then
         error(i18n.errors.missing_rarity)
         container:wikitext(string.format('[[%s]]', tpl_args.skill_icon))
     end
     end
      
      
     where = string.format('%s AND items.class_id="%s" AND items.rarity_id="normal"', where, tpl_args.class_id)
     return container
   
end
     local join = {}
 
      
--
    for _, table_name in ipairs(c.item_classes[tpl_args.class_id].tables) do
-- Factory
        if table_name ~= 'items' then
--
             join[#join+1] = string.format('items._pageID=%s._pageID', table_name)
 
h.factory = {}
 
function h.factory.args_present(...)
     local args = {...}
     return function (tpl_args)
        for _, k in ipairs(args) do
            if tpl_args[k] == nil then
                return false
             elseif type(tpl_args[k]) == 'table' and #tpl_args[k] == 0 then
                return false
            end
         end
         end
        return true
     end
     end
   
end
    local fields = {
 
        'items._pageName',
function h.factory.display_raw_value(key)
         'items.name',
    return function(tpl_args, frame)
        'items.metadata_id',
         return tpl_args[key]
     }
    end
      
end
     for _, k in ipairs(tpl_args._base_item_args) do
 
         local data = core.map[k]
function h.factory.descriptor_value(args)
         if data.field ~= nil then
    -- Arguments:
             fields[#fields+1] = string.format('%s.%s', data.table, data.field)
    --  key
     --  tbl
     args = args or {}
     return function (tpl_args, frame, value)
         args.tbl = args.tbl or tpl_args
         if args.tbl[args.key] then
             value = m_util.html.abbr(value, args.tbl[args.key])
         end
         end
        return value
     end
     end
   
end
    local result = m_cargo.query(
 
        c.item_classes[tpl_args.class_id].tables,
-- ----------------------------------------------------------------------------
        fields,
-- Additional configuration
        {
-- ----------------------------------------------------------------------------
            where=where,
 
            join=table.concat(join, ','),
local c = {}
            groupBy='items._pageID',
 
        }
-- helper to loop over the range variables easier
    )
c.range_map = {
   
     min = {
    if #result > 1 then
         var = '_range_minimum',
        error(i18n.errors.duplicate_base_items)
     },
        -- TODO be more explicit in the error?
     max = {
     elseif #result == 0 then
        var = '_range_maximum',
         error(i18n.errors.base_item_not_found)
     },
     end
     avg = {
      
        var = '_range_average',
    result = result[1]
    },
      
}
     tpl_args.base_item_data = result
    h.process_arguments(tpl_args, frame, {'base_item', 'base_item_page', 'base_item_id'})


     --Copy values..
--
     for _, k in ipairs(tpl_args._base_item_args) do
-- Contents here are meant to resemble the ingame infobox of items
        local data = core.map[k]
--
        if data.field ~= nil and data.func_fetch == nil then
c.item_infobox_groups = {
            local value = result[string.format('%s.%s', data.table, data.field)]
     -- [n]:
             -- I can just use data.default since it will be nil if not provided (nil == nil). Neat! ;)
    --  class: Additional css class added to group tag
             if value ~= nil and (tpl_args[k] == data.default or type(data.default) == 'function') then
     --  heading: Group heading text (used for extras)
                 tpl_args[k] = value
    --  lines:
                if data.func ~= nil then
    --  [n]:
                     tpl_args[k] = data.func(tpl_args, frame, tpl_args[k])
    --    show: Show line if this function returns true; Always show if boolean true. Default: Always show
    --    func: Function that returns line text
    {
        -- Cosmetic item type
        {
            show = h.factory.args_present('cosmetic_type'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'cosmetic_type',
                        fmt = '%s',
                        color = 'default'
                    },
                },
             },
        },
        -- Weapon type
        {
             show = function (tpl_args, frame)
                 if tpl_args.class_id == nil then  
                     return false
                 end
                 end
                 if data.func_copy ~= nil then
                 return cfg.class_groups.weapons.keys[tpl_args.class_id] ~= nil
                    data.func_copy(tpl_args, frame)
            end,
                 end
            func = function (tpl_args, frame)
             elseif value == nil and not data.debug_ignore_nil then
                local v = i18n.item_class_map[tpl_args.class_id]
                 if tpl_args.debug then
                return m_util.html.format_value(tpl_args, frame, {min=v, max=v}, {color = 'default'})
                     error(string.format(i18n.debug.base_item_field_not_found, data.table, data.field))
            end,
        },
        -- Hideout item type
        {
            show = function (tpl_args, frame)
                 return tpl_args.class_id == 'HideoutDoodad'
            end,
             func = function (tpl_args, frame)
                return i18n.item_class_map[tpl_args.class_id]
            end,
        },
        {
            show = h.factory.args_present('gem_tags'),
            func = function (tpl_args, frame)
                 local out = {}
                for i, tag in ipairs(tpl_args.gem_tags) do
                     out[#out+1] = string.format(i18n.gem_tag_category, tag, tag)
                 end
                 end
            elseif tpl_args[k] ~= data.default then
                 return table.concat(out, ', ')
                 if tpl_args.debug then
             end,
                    error(string.format(i18n.debug.field_value_mismatch, k, tostring(tpl_args[k])))
         },
                end
         {
             end
            show = h.factory.args_present('support_gem_letter_html'),
         elseif data.func_fetch ~= nil then
             func = core.factory.infobox_line{
            data.func_fetch(tpl_args, frame)
                 parts = {
         end
                    {
    end
                        key = 'support_gem_letter_html',
end
                    },
 
function h.process_quest_rewards(tpl_args, frame)
    local rid = 1
    local continue
    tpl_args.quest_rewards = {}
    tpl_args.vendor_rewards = {}
    repeat
        continue = true
        local prefix = string.format('quest_reward%s_', rid)
       
        local input_args = {
             shared = {
                 ['type'] = true,
                ['quest'] = false,
                ['quest_id'] = false,
                ['act'] = true,  
                ['class_ids'] = false,
                 },
                 },
            vendor = {
                 fmt = i18n.tooltips.support_icon,
                 ['npc'] = true,
             },
             },
             quest = {
        },
                 ['sockets'] = false,  
        {
                 ['item_level'] = false,
             show = function (tpl_args, frame)
                ['rarity_id'] = false,  
                 return tpl_args.skill_levels and cfg.class_groups.gems.keys[tpl_args.class_id]
                ['notes'] = false,
            end,
            },
            func = function (tpl_args, frame)
        }
                 local value = {
       
                    base = 1,
        local rdata = {}
                    min = 1,
       
                    max = tpl_args.max_level,
        for key, is_required in pairs(input_args.shared) do
                }
            rdata[key] = tpl_args[prefix .. key]
                local options = {
            if is_required then
                    color = 'value',
                 if rdata[key] == nil then
                }
                     continue = false
                 return string.format(
                     break
                     i18n.tooltips.level,
                 end
                     m_util.html.format_value(tpl_args, frame, value, options)
             end
                 )
         end
             end,
          
         },
        if rdata.quest == nil or rdata.quest_id == nil then
         {
             continue = false
            show = function (tpl_args, frame)
        end
                return tpl_args.skill_costs
       
             end,
        if continue and rdata.type == 'vendor' or rdata.type == 'quest' then
            func = function (tpl_args, frame)
            for key, is_required in pairs(input_args[rdata.type]) do
                 local parts = {}
                 rdata[key] = tpl_args[prefix .. key]
                if not tpl_args.skill_costs.has_spending_cost then
                if is_required then
                    -- Try falling back to deprecated parameters
                    if rdata[key] == nil then
                    if not tpl_args.has_reservation_mana_cost then
                        continue = false
                        parts[1] = {
                         break
                            key = 'mana_cost',
                            fmt = '%i',
                            color = false,
                            inline = string.format('%%s %s', m_game.constants.skill.cost_types.mana.long_upper),
                            inline_color = 'value',
                         }
                     end
                     end
                 end
                 else
            end
                    for i=1, #tpl_args.skill_costs do
        else
                        if not tpl_args.skill_costs[i].is_reservation then -- Only get spending costs
            continue = false
                            local cost_type = tpl_args.skill_costs[i].type
        end
                            parts[#parts+1] = {
       
                                key = {'costs', i, 'amount'},
        if continue then
                                fmt = '%i',
            rdata.classes = {}
                                color = false,
            if rdata.class_ids ~= nil then
                                inline = string.format(
                rdata.class_ids = m_util.string.split(rdata.class_ids, ',%s*')
                                    '%%s%s %s',
                for index, class_id in ipairs(rdata.class_ids) do
                                    string.find(cost_type, 'percent', 1, true) and '%%' or '',
                    local class = m_game.constants.characters[class_id]  
                                    m_game.constants.skill.cost_types[cost_type].long_upper
                    if class == nil then
                                ),
                        error(string.format('Class id %s is invalid', class_id))
                                inline_color = 'value',
                    else
                            }
                         rdata.class_ids[index] = class.str_id
                         end
                        rdata.classes[index] = class.name
                     end
                     end
                 end
                 end
            end
                return core.factory.infobox_line{
           
                    type = 'gem',
           
                    parts = parts,
            if rdata.item_level then
                    sep = ', ',
                rdata.item_level = m_util.cast.number(rdata.item_level)
                    fmt = i18n.tooltips.cost,
             end
                }(tpl_args, frame)
              
             end,
             if rdata.rarity_id then
        },
                 if m_game.constants.rarities[rdata.rarity_id] == nil then
        {
                     error(string.format(i18n.errors.invalid_rarity_id, tostring(rdata.rarity_id)))
             show = function (tpl_args, frame)
                return tpl_args.skill_costs
             end,
            func = function (tpl_args, frame)
                 if not tpl_args.skill_costs then
                     return
                 end
                 end
            end
                local parts = {}
           
                if not tpl_args.skill_costs.has_reservation_cost then
            rdata._table = rdata.type .. '_rewards'
                    -- Try falling back to deprecated parameters
            rdata.type = nil
                    if tpl_args.has_reservation_mana_cost then
           
                        parts[1] = {
            tpl_args[rdata._table] = rdata
                            key = 'mana_cost',
           
                            fmt = '%i',
            m_cargo.store(frame, rdata)
                            color = false,
           
                            inline = string.format(
            -- TODO: Verify quests and quest ids?
                                '%%s%s %s',
        end
                                tpl_args.has_percentage_mana_cost and '%%' or '',
       
                                m_game.constants.skill.cost_types.mana.long_upper
        rid = rid + 1
                            ),
    until continue == false
                            inline_color = 'value',
end
                        }
 
                    end
--
                else
-- Display
                    for i=1, #tpl_args.skill_costs do
--
                        if tpl_args.skill_costs[i].is_reservation then -- Only get reservation costs
 
                            local cost_type = tpl_args.skill_costs[i].type
function h.strip_random_stats(tpl_args, frame, stat_text, container_type)
                            parts[#parts+1] = {
    if tpl_args._flags.random_mods and container_type == 'inline' then
                                key = {'costs', i, 'amount'},
        repeat
                                fmt = '%i',
            local text = string.match(stat_text, '<th class="mw%-customtoggle%-31">(.+)</th>')
                                color = false,
            if text ~= nil then
                                inline = string.format(
                stat_text = string.gsub(stat_text, '<table class="random%-modifier%-stats.+</table>', text, 1)
                                    '%%s%s %s',
            end
                                    string.find(cost_type, 'percent', 1, true) and '%%' or '',
        until text == nil
                                    m_game.constants.skill.cost_types[cost_type].long_upper
    end
                                ),
    return stat_text
                                inline_color = 'value',
end
                            }
 
                        end
function h.add_to_container_from_map(tpl_args, frame, container, mapping, container_type)
                     end
    local statcont = mw.html.create('span')
    statcont
        :attr('class', 'item-stats')
        :done()
    local count = 0 -- Number of groups in container
    for _, group in ipairs(mapping) do
        local lines = {}
        if group.func == nil then
            for _, line in ipairs(group) do
                local show = true
                if container_type == 'inline' and line.inline == false then -- TODO: This is not used currently. Need to address what is hidden in inline view.
                    show = false
                elseif line.show == false then
                    show = false
                elseif type(line.show) == 'function' then
                     show = line.show(tpl_args, frame, container_type)
                 end
                 end
                 if show then
                 return core.factory.infobox_line{
                     lines[#lines+1] = line.func(tpl_args, frame, container_type)
                    type = 'gem',
                end
                    parts = parts,
            end
                    sep = ', ',
         else
                     fmt = i18n.tooltips.reservation,
             lines = group.func(tpl_args, frame, container_type)
                }(tpl_args, frame)
        end
            end,
        if #lines > 0 then
        },
             count = count + 1
         {
            local heading = ''
             show = true, -- TODO: Show only if has mana_multiplier
            if group.heading == nil then
             func = core.factory.infobox_line{
            elseif type(group.heading) == 'function' then
                type = 'gem',
                heading = group.heading()
                parts = {
            else
                    {
                heading = string.format('<em class="header">%s</em><br>', group.heading)
                        key = 'mana_multiplier',
            end
                        hide_default = 100,
            statcont
                        fmt = '%i',
                :tag('span')
                        color = false,
                 :attr('class', 'group ' .. (group.class or ''))
                        inline = '%s%%',
                 :wikitext(heading .. table.concat(lines, '<br>'))
                        inline_color = 'value',
                :done()
                    },
         end
                 },
    end
                 fmt = i18n.tooltips.mana_multiplier,
 
            },
    -- Add groups to container
         },
    if count > 0 then
        {
        container:node(statcont)
            show = true, -- TODO: Show only if has cooldown
    end
            func = core.factory.infobox_line{
end
                type = 'gem',
 
                parts = {
function h.make_main_container(tpl_args, frame, container_type)
                    {
    local container = mw.html.create('span')
                        key = 'cooldown',  
        :attr('class', 'item-box -' .. tpl_args.frame_type)
                        hide_default = 0,
   
                        fmt = '%.2f ' .. m_game.units.seconds.short_lower,
    if tpl_args.class_id == 'DivinationCard' then
                     },
        container
                 },
            :tag('span')
                fmt = i18n.tooltips.cooldown_time,
                :attr('class', 'divicard-wrapper')
            },
                :tag('span')
        },
                    :attr('class', 'divicard-art')
        {
                    :wikitext( '[[' .. tpl_args.card_art .. '|link=|alt=]]' )
            -- TODO: Combine with cooldown. Multi-use non-vaal skills display uses together with cooldown time. E.g., Cooldown Time: 8.00 sec (3 uses)
                     :done()
            show = true,
                 :tag('span')
            func = core.factory.infobox_line{
                    :attr('class', 'divicard-frame')
                 type = 'gem',
                    :wikitext( '[[File:Divination card frame.png|link=|alt=]]' )
                parts = {
                    :done()
                     {
                :tag('span')
                        key = 'stored_uses',
                    :attr('class', 'divicard-header')
                         hide_default = 0,
                    :wikitext(tpl_args.name)
                         fmt = '%i',
                    :done()
                     },
                :tag('span')
                },
                    :attr('class', 'divicard-stack')
                fmt = i18n.tooltips.stored_uses,
                    :wikitext(tpl_args.stack_size)
            },
                    :done()
        },
                 :tag('span')
        {
                     :attr('class', 'divicard-reward')
            show = true, -- TODO: Show only if has vaal_souls_requirement
                    :tag('span')
            func = core.factory.infobox_line{
                         :wikitext(tpl_args.description)
                type = 'gem',
                         :done()
                parts = {
                    :done()
                    {
                :tag('span')
                        key = 'vaal_souls_requirement',
                     :attr('class', 'divicard-flavour text-color -flavour')
                        hide_default = 0,
                    :tag('span')
                        fmt = '%i',
                        :wikitext(tpl_args.flavour_text)
                    },
                        :done()
                },
                    :done()
                fmt = i18n.tooltips.vaal_souls_per_use,
                :done()
             },
        --TODO Extras?
         },
    else
        {
        local header_css
            show = true, -- TODO: Show only if has vaal_stored_uses
        local line_type
            func = core.factory.infobox_line{
        if tpl_args.base_item and tpl_args.rarity_id ~= 'normal' then
                type = 'gem',
            line_type = 'double'
                parts = {
        else
                    {
            line_type = 'single'
                        key = 'vaal_stored_uses',
        end
                        hide_default = 0,
       
                        fmt = '%i',
        local name_line = tpl_args.name
                    },
        if tpl_args.base_item and not tpl_args._flags.is_prophecy then
                },
             name_line = name_line .. '<br>' .. tpl_args.base_item
                 fmt = i18n.tooltips.stored_uses, -- TODO: Singular or plural based on number
         end
             },
 
        },
        -- Symbols - These are displayed in the item box header to indicate certain flags and/or item influences
         {
        local symbols
             show = true, -- TODO: Show only if has vaal_soul_gain_prevention_time
        local influences = tpl_args.influences or {}
             func = core.factory.infobox_line{
        if tpl_args.is_replica then
                 type = 'gem',
            symbols = {'replica', 'replica'}
                parts = {
            if #influences > 0 then
                    {
                 symbols[2] = influences[1]
                        key = 'vaal_soul_gain_prevention_time',
             end
                        hide_default = 0,
         elseif #influences > 0 then
                        -- Technically it rounds to nearest, but it is given in milliseconds in the data,
             symbols = {influences[1], influences[1]}
                        fmt = '%i ' .. m_game.units.seconds.short_lower,
             if #influences > 1 then
                    },
                 symbols[2] = influences[2]
                },
            end
                fmt = i18n.tooltips.vaal_soul_gain_prevention_time,
        elseif tpl_args.is_fractured then
             },
            symbols = {'fractured', 'fractured'}
         },
        elseif tpl_args.is_synthesised then
         {
            symbols = {'synthesised', 'synthesised'}
             show = function (tpl_args, frame)
        elseif tpl_args.is_veiled then
                 return tpl_args.cast_time and tpl_args.gem_tags and (m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.spell.tag) or m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.warcry.tag))
             symbols = {'veiled', 'veiled'}
            end,
         end
            func = function (tpl_args, frame)
          
                 return core.factory.infobox_line{
        container
                     parts = {
             :tag('span')
                        {
                 :attr( 'class', 'header -' .. line_type )
                            key = 'cast_time',
                :tag('span')
                            fmt = function (tpl_args, frame, value)
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[1] or '') )
                                if value.min == 0 then
                    :done()
                                    return i18n.tooltips.instant_cast_time
                :wikitext(name_line)
                                end
                 :tag('span')
                                return '%.2f ' .. m_game.units.seconds.short_lower
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[2] or '') )
                            end,
                     :done()
                        },
            :done()
                    },
           
                    fmt = m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.spell.tag) and i18n.tooltips.cast_time or i18n.tooltips.use_time,
        h.add_to_container_from_map(tpl_args, frame, container, c.item_infobox_groups, container_type)
                }(tpl_args, frame)
    end
            end,
   
         },
    if tpl_args.skill_icon ~= nil then
        {
        container:wikitext(string.format('[[%s]]', tpl_args.skill_icon))
             show = true, -- TODO: Show only if has critical_strike_chance
    end
            func = core.factory.infobox_line{
   
                 type = 'gem',
    return container
                 parts = {
end
                    {
 
                        key = 'critical_strike_chance',
--
                        hide_default = 0,
-- Factory
                        fmt = '%.2f%%',
--
                    },
 
                },
h.factory = {}
                fmt = i18n.tooltips.critical_strike_chance,
 
            },
function h.factory.args_present(...)
         },
    local args = {...}
        {
    return function (tpl_args)
            show = true, -- TODO: Show only if has attack_speed_multiplier
         for _, k in ipairs(args) do
            func = core.factory.infobox_line{
             if tpl_args[k] == nil then
                type = 'gem',
                 return false
                parts = {
            elseif type(tpl_args[k]) == 'table' and #tpl_args[k] == 0 then
                    {
                 return false
                        key = 'attack_speed_multiplier',
            end
                        hide_default = 100,
        end
                        fmt = '%i',
        return true
                        color = false,
    end
                        inline = '%s%% ' .. i18n.tooltips.of_base_stat,
end
                        inline_color = 'value',
 
                    },
function h.factory.display_raw_value(key)
                },
    return function(tpl_args, frame)
                fmt = i18n.tooltips.attack_speed_multiplier,
         return tpl_args[key]
            },
    end
         },
end
         {
 
            show = true, -- TODO: Show only if has damage_multiplier
function h.factory.descriptor_value(args)
            func = core.factory.infobox_line{
    -- Arguments:
                type = 'gem',
    --  key
                parts = {
    --  tbl
                    {
    args = args or {}
                        key = 'damage_multiplier',
    return function (tpl_args, frame, value)
                        hide_default = 100,
        args.tbl = args.tbl or tpl_args
                        fmt = '%i',
        if args.tbl[args.key] then
                        color = false,
            value = m_util.html.abbr(value, args.tbl[args.key])
                        inline = '%s%% ' .. i18n.tooltips.of_base_stat,
         end
                        inline_color = 'value',
         return value
                    },
    end
                },
end
                fmt = i18n.tooltips.damage_multiplier,
 
            },
-- ----------------------------------------------------------------------------
        },
-- Additional configuration
        {
-- ----------------------------------------------------------------------------
            show = true, -- TODO: Show only if has damage_effectiveness
 
            func = core.factory.infobox_line{
-- helper to loop over the range variables easier
                type = 'gem',
c.range_map = {
                parts = {
    min = {
                    {
        var = '_range_minimum',
                        key = 'damage_effectiveness',
    },
                        hide_default = 100,
    max = {
                        fmt = '%i',
        var = '_range_maximum',
                        color = false,
    },
                        inline = '%s%%',
    avg = {
                        inline_color = 'value',
        var = '_range_average',
                    },
    },
                },
}
                fmt = i18n.tooltips.damage_effectiveness,
 
            },
--
         },
-- Contents here are meant to resemble the ingame infobox of items
         {
--
             show = h.factory.args_present('projectile_speed'),
c.item_infobox_groups = {
             func = core.factory.infobox_line{
    -- [n]:
                 parts = {
    --  class: Additional css class added to group tag
    --  heading: Group heading text (used for extras)
    --  lines:
    --  [n]:
    --    show: Show line if this function returns true; Always show if boolean true. Default: Always show
    --    func: Function that returns line text
    {
         -- Cosmetic item type
         {
             show = h.factory.args_present('cosmetic_type'),
             func = core.factory.infobox_line{
                 parts = {
                     {
                     {
                         key = 'cosmetic_type',
                         key = 'projectile_speed',
                        fmt = '%s',
                        color = 'default'
                     },
                     },
                 },
                 },
                fmt = i18n.tooltips.projectile_speed,
             },
             },
         },
         },
        -- Weapon type
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('radius'),
                 if tpl_args.class_id == nil then
            func = core.factory.infobox_line{
                     return false
                 parts = {
                end
                     {
                return cfg.class_groups.weapons.keys[tpl_args.class_id] ~= nil
                        key = 'radius',
            end,
                        func = h.factory.descriptor_value{key='radius_description'},
            func = function (tpl_args, frame)
                    },
                local v = i18n.item_class_map[tpl_args.class_id]
                    {
                return m_util.html.format_value(tpl_args, frame, {min=v, max=v}, {color = 'default'})
                        key = 'radius_secondary',
             end,
                        func = h.factory.descriptor_value{key='radius_secondary_description'},
                    },
                    {
                        key = 'radius_tertiary',
                        func = h.factory.descriptor_value{key='radius_tertiary_description'},
                    },
                },
                sep = ' / ',
                fmt = i18n.tooltips.radius,
             },
         },
         },
         -- Hideout item type
         -- Quality is before item stats, but after gem stuff and item class
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('quality'),
                 return tpl_args.class_id == 'HideoutDoodad'
            func = core.factory.infobox_line{
            end,
                 parts = {
            func = function (tpl_args, frame)
                    {
                 return i18n.item_class_map[tpl_args.class_id]
                        key = 'quality',
             end,
                        fmt = '+%i%%',
                        color = 'mod',
                        hide_default = 0,
                    },
                },
                 fmt = i18n.tooltips.quality,
             },
         },
         },
        -- Weapon only
         {
         {
             show = h.factory.args_present('gem_tags'),
             show = h.factory.args_present('physical_damage_html'),
            func = function (tpl_args, frame)
                local out = {}
                for i, tag in ipairs(tpl_args.gem_tags) do
                    out[#out+1] = string.format(i18n.gem_tag_category, tag, tag)
                end
                return table.concat(out, ', ')
            end,
        },
        {
            show = h.factory.args_present('support_gem_letter_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'support_gem_letter_html',
                         key = 'physical_damage_html',
                        fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.support_icon,
                 fmt = i18n.tooltips.physical_damage,
             },
             },      
         },
         },
         {
         {
             show = function (tpl_args, frame)
             show = true, -- Elemental Damage
                return tpl_args.skill_levels and cfg.class_groups.gems.keys[tpl_args.class_id]
            end,
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 local value = {
                 local keys = {'fire_damage_html', 'cold_damage_html', 'lightning_damage_html'}
                    base = 1,
                local elements = {}
                     min = 1,
                for _, key in ipairs(keys) do
                     max = tpl_args.max_level,
                     if tpl_args[key] then
                 }
                        elements[#elements+1] = tpl_args[key]
                 local options = {
                     end
                    color = 'value',
                 end
                }
                 local text = table.concat(elements, ', ') -- returns empty string if elements is empty
                return string.format(
                if text ~= '' then
                    i18n.tooltips.level,
                    return string.format(i18n.tooltips.elemental_damage, text)
                    m_util.html.format_value(tpl_args, frame, value, options)
                end
                 )
                 return
             end,
             end,    
         },
         },
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('chaos_damage_html'),
                return tpl_args.skill_costs
             func = core.factory.infobox_line{
            end,
                 parts = {
             func = function (tpl_args, frame)
                     {
                 local parts = {}
                         key = 'chaos_damage_html',
                if not tpl_args.skill_costs.has_spending_cost then
                        fmt = '%s',
                     -- Try falling back to deprecated parameters
                        color = false, -- html already has color
                    if not tpl_args.has_reservation_mana_cost then
                    },
                         parts[1] = {
                },
                            key = 'mana_cost',
                fmt = i18n.tooltips.chaos_damage,
                            fmt = '%i',
            },      
                            color = false,
        },
                            inline = string.format('%%s %s', m_game.constants.skill.cost_types.mana.long_upper),
        {
                            inline_color = 'value',
            show = h.factory.args_present('critical_strike_chance_html'),
                        }
            func = core.factory.infobox_line{
                    end
                parts = {
                else
                    {
                    for i=1, #tpl_args.skill_costs do
                        key = 'critical_strike_chance_html',
                        if not tpl_args.skill_costs[i].is_reservation then -- Only get spending costs
                        fmt = '%s',
                            local cost_type = tpl_args.skill_costs[i].type
                    },
                            parts[#parts+1] = {
                },
                                key = {'costs', i, 'amount'},
                fmt = i18n.tooltips.critical_strike_chance,
                                fmt = '%i',
            },
                                color = false,
        },
                                inline = string.format(
        {
                                    '%%s%s %s',
            show = h.factory.args_present('attack_speed_html'),
                                    string.find(cost_type, 'percent', 1, true) and '%%' or '',
            func = core.factory.infobox_line{
                                    m_game.constants.skill.cost_types[cost_type].long_upper
                parts = {
                                ),
                     {
                                inline_color = 'value',
                        key = 'attack_speed_html',
                            }
                        fmt = '%s',
                        end
                     },
                    end
                },
                end
                fmt = i18n.tooltips.attacks_per_second,
                return core.factory.infobox_line{
            },
                     type = 'gem',
                    parts = parts,
                     sep = ', ',
                    fmt = i18n.tooltips.cost,
                }(tpl_args, frame)
            end,
         },
         },
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('weapon_range_html'),
                return tpl_args.skill_costs
             func = core.factory.infobox_line{
            end,
                 parts = {
             func = function (tpl_args, frame)
                     {
                if not tpl_args.skill_costs then
                         key = 'weapon_range_html',
                    return
                        fmt = '%s',
                end
                    },
                 local parts = {}
                },
                if not tpl_args.skill_costs.has_reservation_cost then
                fmt = i18n.tooltips.weapon_range,
                     -- Try falling back to deprecated parameters
            },
                    if tpl_args.has_reservation_mana_cost then
        },
                         parts[1] = {
        -- Map only
                            key = 'mana_cost',
        {
                            fmt = '%i',
            show = h.factory.args_present('map_area_level'),
                            color = false,
            func = core.factory.infobox_line{
                            inline = string.format(
                parts = {
                                '%%s%s %s',
                     {
                                tpl_args.has_percentage_mana_cost and '%%' or '',
                        key = 'map_area_level',
                                m_game.constants.skill.cost_types.mana.long_upper
                        fmt = '%i',
                            ),
                     },
                            inline_color = 'value',
                },
                        }
                fmt = i18n.tooltips.map_level,
                    end
            },
                else
                    for i=1, #tpl_args.skill_costs do
                        if tpl_args.skill_costs[i].is_reservation then -- Only get reservation costs
                            local cost_type = tpl_args.skill_costs[i].type
                            parts[#parts+1] = {
                                key = {'costs', i, 'amount'},
                                fmt = '%i',
                                color = false,
                                inline = string.format(
                                    '%%s%s %s',
                                    string.find(cost_type, 'percent', 1, true) and '%%' or '',
                                    m_game.constants.skill.cost_types[cost_type].long_upper
                                ),
                                inline_color = 'value',
                            }
                        end
                    end
                end
                return core.factory.infobox_line{
                     type = 'gem',
                    parts = parts,
                     sep = ', ',
                    fmt = i18n.tooltips.reservation,
                }(tpl_args, frame)
            end,
         },
         },
         {
         {
             show = true, -- TODO: Show only if has mana_multiplier
             show = h.factory.args_present('map_tier'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'mana_multiplier',
                         key = 'map_tier',
                        hide_default = 100,
                         fmt = '%i',
                         fmt = '%i',
                        color = false,
                        inline = '%s%%',
                        inline_color = 'value',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.mana_multiplier,
                 fmt = i18n.tooltips.map_tier,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has cooldown
             show = function (tpl_args, frame)
                return tpl_args.map_guild_character ~= nil and tpl_args.rarity_id == 'normal'
            end,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'cooldown',
                         key = 'map_guild_character',
                        hide_default = 0,
                         fmt = '%s',
                         fmt = '%.2f ' .. m_game.units.seconds.short_lower,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.cooldown_time,
                 fmt = i18n.tooltips.map_guild_character,
             },
             },
         },
         },
         {
         {
             -- TODO: Combine with cooldown. Multi-use non-vaal skills display uses together with cooldown time. E.g., Cooldown Time: 8.00 sec (3 uses)
             show = function (tpl_args, frame)
             show = true,
                return tpl_args.unique_map_guild_character ~= nil and tpl_args.rarity_id == 'unique'
             end,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'stored_uses',
                         key = 'unique_map_guild_character',
                        hide_default = 0,
                         fmt = '%s',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.stored_uses,
                 fmt = i18n.tooltips.map_guild_character,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has vaal_souls_requirement
             show = true,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 type = 'gem',
                 type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'vaal_souls_requirement',
                         key = 'map_item_drop_quantity_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                         hide_default = 0,
                         hide_default = 0,
                        fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.vaal_souls_per_use,
                 fmt = i18n.tooltips.item_quantity,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has vaal_stored_uses
             show = true,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 type = 'gem',
                 type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'vaal_stored_uses',
                         key = 'map_item_drop_rarity_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                         hide_default = 0,
                         hide_default = 0,
                        fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.stored_uses, -- TODO: Singular or plural based on number
                 fmt = i18n.tooltips.item_rarity,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has vaal_soul_gain_prevention_time
             show = true,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 type = 'gem',
                 type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'vaal_soul_gain_prevention_time',
                         key = 'map_pack_size_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                         hide_default = 0,
                         hide_default = 0,
                        -- Technically it rounds to nearest, but it is given in milliseconds in the data,
                        fmt = '%i ' .. m_game.units.seconds.short_lower,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.vaal_soul_gain_prevention_time,
                 fmt = i18n.tooltips.monster_pack_size,
             },
             },
         },
         },
        -- Jewel Only
         {
         {
             show = function (tpl_args, frame)
             show = true,
                return tpl_args.cast_time and tpl_args.gem_tags and (m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.spell.tag) or m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.warcry.tag))
             func = core.factory.infobox_line{
            end,
                parts = {
             func = function (tpl_args, frame)
                    {
                return core.factory.infobox_line{
                        key = 'item_limit',
                    parts = {
                        fmt = '%i',
                        {
                            key = 'cast_time',
                            fmt = function (tpl_args, frame, value)
                                if value.min == 0 then
                                    return i18n.tooltips.instant_cast_time
                                end
                                return '%.2f ' .. m_game.units.seconds.short_lower
                            end,
                        },
                     },
                     },
                    fmt = m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.spell.tag) and i18n.tooltips.cast_time or i18n.tooltips.use_time,
                },
                }(tpl_args, frame)
                fmt = i18n.tooltips.limited_to,
            end,
            },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has critical_strike_chance
             show = h.factory.args_present('jewel_radius_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'critical_strike_chance',
                         key = 'jewel_radius_html',
                         hide_default = 0,
                         fmt = '%s',
                         fmt = '%.2f%%',
                         color = false, -- html already has color
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.critical_strike_chance,
                 fmt = i18n.tooltips.radius,
             },
             },
         },
         },
        -- Flask only
         {
         {
             show = true, -- TODO: Show only if has attack_speed_multiplier
             show = h.factory.args_present('flask_mana_html', 'flask_duration_html'),
            --func = core.factory.display_flask('flask_mana'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'attack_speed_multiplier',
                         key = 'flask_mana_html',
                        hide_default = 100,
                         fmt = '%s',
                         fmt = '%i',
                    },
                        color = false,
                    {
                         inline = '%s%% ' .. i18n.tooltips.of_base_stat,
                         key = 'flask_duration_html',
                         inline_color = 'value',
                         fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.attack_speed_multiplier,
                 fmt = i18n.tooltips.flask_mana_recovery,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has damage_multiplier
             show = h.factory.args_present('flask_life_html', 'flask_duration_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'damage_multiplier',
                         key = 'flask_life_html',
                        hide_default = 100,
                         fmt = '%s',
                         fmt = '%i',
                    },
                        color = false,
                    {
                         inline = '%s%% ' .. i18n.tooltips.of_base_stat,
                         key = 'flask_duration_html',
                         inline_color = 'value',
                         fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.damage_multiplier,
                 fmt = i18n.tooltips.flask_life_recovery,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has damage_effectiveness
            -- don't display for mana/life flasks
             show = function (tpl_args, frame)
                for _, k in ipairs({'flask_life_html', 'flask_mana_html'}) do
                    if tpl_args[k] ~= nil then
                        return false
                    end
                end
                return tpl_args['flask_duration_html'] ~= nil
            end,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'damage_effectiveness',
                         key = 'flask_duration_html',
                        hide_default = 100,
                         fmt = '%s',
                         fmt = '%i',
                        color = false,
                        inline = '%s%%',
                        inline_color = 'value',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.damage_effectiveness,
                 fmt = i18n.tooltips.flask_duration,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('projectile_speed'),
             show = h.factory.args_present('charges_per_use_html', 'charges_max_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'projectile_speed',
                         key = 'charges_per_use_html',
                        fmt = '%s',
                    },
                    {
                        key = 'charges_max_html',
                        fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.projectile_speed,
                 fmt = i18n.tooltips.flask_charges_per_use,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('radius'),
             show = h.factory.args_present('buff_stat_text'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'radius',
                         key = 'buff_stat_text',
                         func = h.factory.descriptor_value{key='radius_description'},
                         color = 'mod',
                    },
                    {
                        key = 'radius_secondary',
                        func = h.factory.descriptor_value{key='radius_secondary_description'},
                    },
                    {
                        key = 'radius_tertiary',
                        func = h.factory.descriptor_value{key='radius_tertiary_description'},
                     },
                     },
                 },
                 },
                sep = ' / ',
                fmt = i18n.tooltips.radius,
             },
             },
         },
         },
         -- Quality is before item stats, but after gem stuff and item class
         -- Armor only
         {
         {
             show = h.factory.args_present('quality'),
             show = h.factory.args_present('block_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'quality',
                         key = 'block_html',
                         fmt = '+%i%%',
                         fmt = '%s',
                        color = 'mod',
                         hide_default = 0,
                         hide_default = 0,
                        hide_default_key = 'block',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.quality,
                 fmt = i18n.tooltips.chance_to_block,
             },
             },
         },
         },
        -- Weapon only
         {
         {
             show = h.factory.args_present('physical_damage_html'),
             show = h.factory.args_present('armour_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'physical_damage_html',
                         key = 'armour_html',
                         fmt = '%s',
                         fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'armour',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.physical_damage,
                 fmt = i18n.tooltips.armour,
             },      
             },
         },
         },
         {
         {
            show = true, -- Elemental Damage
             show = h.factory.args_present('evasion_html'),
            func = function (tpl_args, frame)
                local keys = {'fire_damage_html', 'cold_damage_html', 'lightning_damage_html'}
                local elements = {}
                for _, key in ipairs(keys) do
                    if tpl_args[key] then
                        elements[#elements+1] = tpl_args[key]
                    end
                end
                local text = table.concat(elements, ', ') -- returns empty string if elements is empty
                if text ~= '' then
                    return string.format(i18n.tooltips.elemental_damage, text)
                end
                return
            end,     
        },
        {
             show = h.factory.args_present('chaos_damage_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'chaos_damage_html',
                         key = 'evasion_html',
                         fmt = '%s',
                         fmt = '%s',
                         color = false, -- html already has color
                         hide_default = 0,
                        hide_default_key = 'evasion',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.chaos_damage,
                 fmt = i18n.tooltips.evasion,
             },      
             },
         },
         },
         {
         {
             show = h.factory.args_present('critical_strike_chance_html'),
             show = h.factory.args_present('energy_shield_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'critical_strike_chance_html',
                         key = 'energy_shield_html',
                         fmt = '%s',
                         fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'energy_shield',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.critical_strike_chance,
                 fmt = i18n.tooltips.energy_shield,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('attack_speed_html'),
             show = h.factory.args_present('movement_speed'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'attack_speed_html',
                         key = 'movement_speed',
                         fmt = '%s',
                         fmt = '%s%%',
                        hide_default = 0,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.attacks_per_second,
                 fmt = i18n.tooltips.movement_speed,
             },
             },
         },
         },
        -- Amulet only
         {
         {
             show = h.factory.args_present('weapon_range_html'),
             show = h.factory.args_present('talisman_tier'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'weapon_range_html',
                         key = 'talisman_tier',
                         fmt = '%s',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.weapon_range,
                 fmt = i18n.tooltips.talisman_tier,
             },
             },
         },
         },
         -- Map only
         -- Misc
         {
         {
             show = h.factory.args_present('map_area_level'),
             show = h.factory.args_present('stack_size'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_area_level',
                         key = 'stack_size',
                         fmt = '%i',
                         fmt = '%i',
                        hide_default = 1,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.map_level,
                 fmt = i18n.tooltips.stack_size,
             },
             },
         },
         },
        -- Essence stuff
         {
         {
             show = h.factory.args_present('map_tier'),
             show = h.factory.args_present('essence_level'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_tier',
                         key = 'essence_level',
                         fmt = '%i',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.map_tier,
                 fmt = i18n.tooltips.essence_level,
             },
             },
         },
         },
        -- Blight items
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('blight_item_tier'),
                return tpl_args.map_guild_character ~= nil and tpl_args.rarity_id == 'normal'
            end,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_guild_character',
                         key = 'blight_item_tier',
                         fmt = '%s',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.map_guild_character,
                 fmt = i18n.tooltips.blight_item_tier,
             },
             },
         },
         },
        -- Harvest seeds (upper section)
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('seed_tier'),
                return tpl_args.unique_map_guild_character ~= nil and tpl_args.rarity_id == 'unique'
            end,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'unique_map_guild_character',
                         key = 'seed_tier',
                         fmt = '%s',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.map_guild_character,
                 fmt = i18n.tooltips.seed_tier,
             },
             },
         },
         },
         {
         {
             show = true,
             show = h.factory.args_present('seed_tier'),
            func = function (tpl_args, value)
                return i18n.tooltips.seed_monster
            end,
        },
        {
            show = h.factory.args_present('seed_type_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_item_drop_quantity_+%',
                         key = 'seed_type_html',
                         fmt = '+%i%%',
                         fmt = '%s',
                        color = 'mod',
                        hide_default = 0,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.item_quantity,
                 fmt = i18n.tooltips.seed_lifeforce_gained,
             },
             },
         },
         },
         {
         {
             show = true,
             show = h.factory.args_present('seed_growth_cycles'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_item_drop_rarity_+%',
                         key = 'seed_growth_cycles',
                         fmt = '+%i%%',
                         fmt = '%s',
                        color = 'mod',
                        hide_default = 0,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.item_rarity,
                 fmt = i18n.tooltips.seed_growth_cycles,
             },
             },
         },
         },
        -- Heist
         {
         {
             show = true,
             show = h.factory.args_present('heist_required_npcs'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_pack_size_+%',
                         key = 'heist_required_npcs',
                         fmt = '+%i%%',
                         fmt = '%s',
                        color = 'mod',
                        hide_default = 0,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.monster_pack_size,
                 fmt = i18n.tooltips.heist_required_npc,
             },
             },
         },
         },
        -- Jewel Only
    },
    -- Requirements
    {
         {
         {
             show = true,
             show = h.factory.args_present('master', 'master_level_requirement'),
             func = core.factory.infobox_line{
             func = function (tpl_args, frame)
                parts = {
                local data
                    {
                for i, rowdata in ipairs(m_game.constants.masters) do
                         key = 'item_limit',
                    if tpl_args.master == rowdata.full then
                         fmt = '%i',
                         data = rowdata
                     },
                         break
                 },
                     end
                 fmt = i18n.tooltips.limited_to,
                end
             },
                  
                 return m_util.html.poe_color('default', i18n.tooltips.requires, string.format('[[%s|%s %s]]', data.full, data.short_upper, tpl_args.master_level_requirement))
             end
         },
         },
        -- Instead of item level, show drop level if any
         {
         {
             show = h.factory.args_present('jewel_radius_html'),
             show = true, -- Requires...
             func = core.factory.infobox_line{
             func = function (tpl_args, frame)
                 parts = {
                 local parts = {
                     {
                     {
                         key = 'jewel_radius_html',
                         key = 'required_level_final_html',
                         fmt = '%s',
                         hide_default = 1,
                         color = false, -- html already has color
                        hide_default_key = 'required_level_final',
                         inline = i18n.tooltips.level_inline,
                        inline_color = false,
                     },
                     },
                 },
                 }
                 fmt = i18n.tooltips.radius,
                for _, attr in ipairs(m_game.constants.attribute_order) do
             },
                    parts[#parts+1] = {
                        key = string.format('required_%s_html', attr),
                        hide = function (tpl_args, frame, value)
                            local min = m_game.constants.characters.minimum_attributes[m_game.constants.attributes[attr].arg]
                            return value.min <= min and value.max <= min
                        end,
                        hide_key = string.format('required_%s', attr),
                        inline = ', %s ' .. m_game.constants.attributes[attr].short_upper,
                        inline_color = false,
                    }
                 end
                local requirements = core.factory.infobox_line{parts = parts}(tpl_args, frame)
                if requirements == nil then -- return early
                    return
                end
                requirements = string.gsub(requirements, '^, ', '')
                return string.format(i18n.tooltips.requires, requirements)
             end,
         },
         },
        -- Flask only
         {
         {
             show = h.factory.args_present('flask_mana_html', 'flask_duration_html'),
             show = h.factory.args_present('heist_required_job', 'heist_required_job_level'),
            --func = core.factory.display_flask('flask_mana'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'flask_mana_html',
                         key = 'heist_required_job_level',
                         fmt = '%s',
                         fmt = '%s',
                     },
                     },
                     {
                     {
                         key = 'flask_duration_html',
                         key = 'heist_required_job',
                         fmt = '%s',
                         fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.flask_mana_recovery,
                 fmt = i18n.tooltips.heist_required_job,
             },
             },
         },
         },
         {
    },
             show = h.factory.args_present('flask_life_html', 'flask_duration_html'),
    -- Gem description
             func = core.factory.infobox_line{
    {
                parts = {
        class = 'tc -gemdesc',
                    {
         {
                        key = 'flask_life_html',
             show = h.factory.args_present('gem_description'),
                        fmt = '%s',
             func = h.factory.display_raw_value('gem_description'),
                    },
                    {
                        key = 'flask_duration_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.flask_life_recovery,
            },
         },
         },
    },
    -- Gem Quality Stats
    {
        class = 'tc -mod',
         {
         {
             -- don't display for mana/life flasks
             show = h.factory.args_present('skill_quality'),
             show = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 for _, k in ipairs({'flask_life_html', 'flask_mana_html'}) do
                local span = mw.html.create('span')
                     if tpl_args[k] ~= nil then
                span
                         return false
                    :addClass('quality')
                     end
                   
                local span2 = span:tag('span')
                span2
                    :addClass('quality-header')
                    :tag('span')
                        :wikitext(m_util.html.poe_color('default', i18n.tooltips.gem_quality))
                   
                 for i, quality_data in ipairs(tpl_args.skill_quality) do
                    local span_inner = span2:tag('span')
                    span_inner
                        :addClass('quality-section-select')
                        :addClass('quality-' .. i)
                        :wikitext(i)
                        :tag('span')
                            :wikitext(i18n.tooltips['gem_quality_' .. i])
                           
                     if i == 1 then
                         span_inner:addClass('quality-selected')
                     end
                 end
                 end
                 return tpl_args['flask_duration_html'] ~= nil
                  
                for i, quality_data in ipairs(tpl_args.skill_quality) do
                    local span_inner = span:tag('span')
                    span_inner
                        :addClass('quality-box')
                        :addClass('quality-' .. i)
                        :wikitext(quality_data.stat_text)
                   
                    if i == 1 then
                        span_inner:addClass('quality-selected')
                    end
                end
               
                return tostring(span)
             end,
             end,
            func = core.factory.infobox_line{
        },
                parts = {
    },
                    {
    -- Gem Implicit Stats
                        key = 'flask_duration_html',
    {
                        fmt = '%s',
         class = 'tc -mod',
                    },
                },
                fmt = i18n.tooltips.flask_duration,
            },
         },
         {
         {
             show = h.factory.args_present('charges_per_use_html', 'charges_max_html'),
             show = function (tpl_args, frame)
             func = core.factory.infobox_line{
                return cfg.class_groups.gems.keys[tpl_args.class_id] and tpl_args.stat_text
                 parts = {
            end,
                    {
             func = function (tpl_args, frame)
                        key = 'charges_per_use_html',
                 local lines = {}
                        fmt = '%s',
                lines[#lines+1] = tpl_args.stat_text
                     },
                for _, tag in ipairs(tpl_args.gem_tags) do
                    {
                     if tag == m_game.constants.item.gem_tags.vaal.tag then
                         key = 'charges_max_html',
                         lines[#lines+1] = i18n.tooltips.corrupted
                         fmt = '%s',
                         break
                     },
                     end
                 },
                 end
                 fmt = i18n.tooltips.flask_charges_per_use,
                 return table.concat(lines, '<br>')
             },
             end,
         },
         },
    },
    -- Implicit Stats
    {
        class = 'tc -mod',
        func = function (tpl_args, frame, container_type)
            if tpl_args.implicit_stat_text then
                return {h.strip_random_stats(tpl_args, frame, tpl_args.implicit_stat_text, container_type)}
            else
                return {}
            end
        end,
    },
    -- Stats
    {
        class = 'tc -mod',
        func = function (tpl_args, frame, container_type)
            if tpl_args.explicit_stat_text then
                return {h.strip_random_stats(tpl_args, frame, tpl_args.explicit_stat_text, container_type)}
            else
                return {}
            end
        end,
    },
    -- Experience
    --[[{
         {
         {
             show = h.factory.args_present('buff_stat_text'),
             show = h.factory.args_present('experience'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'buff_stat_text',
                         key = 'experience',
                         color = 'mod',
                         fmt = '%i',
                     },
                     },
                 },
                 },
             },
             },
         },
         },
         -- Armor only
    },]]--
    -- Harvest seeds (lower section)
    {
         class = 'tc -mod',
        {
            show = h.factory.args_present('seed_consumed_wild_lifeforce_percentage'),
            func = function (tpl_args, frame)
                if tpl_args.seed_consumed_wild_lifeforce_percentage > 0 then
                    return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_wild_lifeforce_percentage, m_util.html.poe_color('wild', m_game.seed_types.wild))
                end
            end
        },
        {
            show = h.factory.args_present('seed_consumed_vivid_lifeforce_percentage'),
            func = function (tpl_args, frame)
                if tpl_args.seed_consumed_vivid_lifeforce_percentage > 0 then
                    return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_vivid_lifeforce_percentage, m_util.html.poe_color('vivid', m_game.seed_types.vivid))
                end
            end
        },
        {
            show = h.factory.args_present('seed_consumed_primal_lifeforce_percentage'),
            func = function (tpl_args, frame)
                if tpl_args.seed_consumed_primal_lifeforce_percentage > 0 then
                    return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_primal_lifeforce_percentage, m_util.html.poe_color('primal', m_game.seed_types.primal))
                end
            end
        },
         {
         {
             show = h.factory.args_present('block_html'),
             show = h.factory.args_present('seed_required_nearby_seed_tier', 'seed_type_html', 'seed_required_nearby_seed_amount'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'block_html',
                         key = 'seed_required_nearby_seed_amount',
                         fmt = '%s',
                         fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'block',
                     },
                     },
                },
                fmt = i18n.tooltips.chance_to_block,
            },
        },
        {
            show = h.factory.args_present('armour_html'),
            func = core.factory.infobox_line{
                parts = {
                     {
                     {
                         key = 'armour_html',
                         key = 'seed_type_html',
                         fmt = '%s',
                         fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'armour',
                     },
                     },
                },
                fmt = i18n.tooltips.armour,
            },
        },
        {
            show = h.factory.args_present('evasion_html'),
            func = core.factory.infobox_line{
                parts = {
                     {
                     {
                         key = 'evasion_html',
                         key = 'seed_required_nearby_seed_tier',
                         fmt = '%s',
                         fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'evasion',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.evasion,
                 fmt = i18n.tooltips.seed_required_seeds,
                color = 'mod',
             },
             },
         },
         },
    },
    {
        class = 'tc -crafted',
         {
         {
             show = h.factory.args_present('energy_shield_html'),
             show = h.factory.args_present('seed_effect'),
            func = h.factory.display_raw_value('seed_effect'),
        },
    },
    -- Description (currency, doodads)
    {
        class = 'tc -mod',
        {
            show = h.factory.args_present('description'),
            func = h.factory.display_raw_value('description'),
        },
        --[[{
            show = h.factory.args_present('plant_booster_additional_crafting_options'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'energy_shield_html',
                         key = 'plant_booster_additional_crafting_options',
                         fmt = '%s',
                         fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'energy_shield',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.energy_shield,
                 fmt = i18n.tooltips.plant_booster_additional_crafting_options,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('movement_speed'),
             show = h.factory.args_present('plant_booster_extra_chances'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'movement_speed',
                         key = 'plant_booster_extra_chances',
                         fmt = '%s%%',
                         fmt = '%s%%',
                        hide_default = 0,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.movement_speed,
                 fmt = i18n.tooltips.plant_booster_extra_chances,
             },
             },
         },
         },
        -- Amulet only
         {
         {
             show = h.factory.args_present('talisman_tier'),
             show = h.factory.args_present('plant_booster_lifeforce'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'talisman_tier',
                         key = 'plant_booster_lifeforce',
                         fmt = '%i',
                         fmt = '%s%%',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.talisman_tier,
                 fmt = i18n.tooltips.plant_booster_lifeforce,
             },
             },
         },
         },]]
         -- Misc
    },
    {
         class = 'tc -crafted',
         {
         {
             show = h.factory.args_present('stack_size'),
             show = h.factory.args_present('incubator_effect'),
             func = core.factory.infobox_line{
             func = h.factory.display_raw_value('incubator_effect'),
                parts = {
                    {
                        key = 'stack_size',
                        fmt = '%i',
                        hide_default = 1,
                    },
                },
                fmt = i18n.tooltips.stack_size,
            },
         },
         },
         -- Essence stuff
    },
    -- Variations (for doodads)
    {
         class = 'tc -mod',
         {
         {
             show = h.factory.args_present('essence_level'),
             show = h.factory.args_present('variation_count'),
             func = core.factory.infobox_line{
             func = function (tpl_args, frame)
                 parts = {
                local txt
                     {
                 if tpl_args.variation_count == 1 then
                        key = 'essence_level',
                     txt = i18n.tooltips.variation_singular
                        fmt = '%i',
                else
                     },
                     txt = i18n.tooltips.variation_plural
                 },
                 end
                 fmt = i18n.tooltips.essence_level,
                 return string.format('%i %s', tpl_args.variation_count, txt)
             },
             end,
         },
         },
         -- Blight items
    },
    -- Flavour Text
    {
         class = 'tc -flavour',
         {
         {
             show = h.factory.args_present('blight_item_tier'),
             show = h.factory.args_present('flavour_text'),
             func = core.factory.infobox_line{
             func = h.factory.display_raw_value('flavour_text'),
                parts = {
                    {
                        key = 'blight_item_tier',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.blight_item_tier,
            },
         },
         },
         -- Harvest seeds (upper section)
    },
    -- Prophecy text
    {
         class = 'tc -value',
         {
         {
             show = h.factory.args_present('seed_tier'),
             show = h.factory.args_present('prediction_text'),
             func = core.factory.infobox_line{
             func = h.factory.display_raw_value('prediction_text'),
                parts = {
        },
                    {
    },
                        key = 'seed_tier',
    -- Can not be traded or modified
                        fmt = '%i',
    {
                    },
        class = 'tc -canttradeormodify',
                 },
        {
                fmt = i18n.tooltips.seed_tier,
            show = h.factory.args_present('cannot_be_traded_or_modified'),
             },
            func = function (tpl_args, frame)
                 if tpl_args.cannot_be_traded_or_modified == true then
                    return i18n.tooltips.cannot_be_traded_or_modified
                end
             end,
         },
         },
    },
    -- Help text
    {
        class = 'tc -help',
         {
         {
             show = h.factory.args_present('seed_tier'),
             show = h.factory.args_present('help_text'),
             func = function (tpl_args, value)
             func = h.factory.display_raw_value('help_text'),
                return i18n.tooltips.seed_monster
            end,
         },
         },
    },
    -- Cost (i.e. vendor costs)
    {
        --class = '',
         {
         {
             show = h.factory.args_present('seed_type_html'),
             show = h.factory.args_present('master_favour_cost'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'seed_type_html',
                         key = 'master_favour_cost',
                         fmt = '%s',
                         color = 'currency',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.seed_lifeforce_gained,
                 fmt = i18n.tooltips.favour_cost,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('seed_growth_cycles'),
             show = h.factory.args_present('seal_cost'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'seed_growth_cycles',
                         key = 'seal_cost',
                         fmt = '%s',
                         fmt = function (tpl_args, frame, value)
                            return '%dx ' .. h.item_link{metadata_id='Metadata/Items/Currency/CurrencySilverCoin', html=''}
                        end,
                        color = 'currency',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.seed_growth_cycles,
                 fmt = i18n.tooltips.seal_cost,
             },
             },
         },
         },
        -- Heist
    },
}
 
--
-- This is meant to show additional information about the item in a separate infobox
--
c.extra_display_groups = {
    {
        heading = i18n.tooltips.extra_info,
        class = '',
         {
         {
             show = h.factory.args_present('heist_required_npcs'),
             show = h.factory.args_present('atlas_connections'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'heist_required_npcs',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.heist_required_npc,
            },
        },
    },
    -- Requirements
    {
        {
            show = h.factory.args_present('master', 'master_level_requirement'),
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 local data
                 local fields = {
                for i, rowdata in ipairs(m_game.constants.masters) do
                    [false] = {
                     if tpl_args.master == rowdata.full then
                        value = '✗',
                         data = rowdata
                        sort = 0,
                         break
                        class = 'table-cell-xmark',
                     end
                    },
                 end
                     [true] = {
                        value = '✓',
                         sort = 1,
                         class = 'table-cell-checkmark',
                     },
                 }
                  
                  
                 return m_util.html.poe_color('default', i18n.tooltips.requires, string.format('[[%s|%s %s]]', data.full, data.short_upper, tpl_args.master_level_requirement))
                 local tbl = mw.html.create('table')
            end
                tbl
        },
                    :attr('class', 'wikitable')
        -- Instead of item level, show drop level if any
                    :attr('style', 'width:100%;')
        {
                    :tag('tr')
            show = true, -- Requires...
                        :tag('th')
            func = function (tpl_args, frame)
                            :attr('colspan', 6)
                local parts = {
                            :attr('style', 'text-decoration: underline;')
                    {
                            :wikitext(i18n.tooltips.header_overall)
                         key = 'required_level_final_html',
                            :done()
                         hide_default = 1,
                        :done()
                         hide_default_key = 'required_level_final',
                    :tag('tr')
                         inline = i18n.tooltips.level_inline,
                        :tag('th')
                         inline_color = false,
                            :wikitext(i18n.tooltips.header_upgrades)
                     },
                            :done()
                }
                        :tag('th')
                 for _, attr in ipairs(m_game.constants.attribute_order) do
                            :wikitext(0)
                     parts[#parts+1] = {
                            :done()
                        key = string.format('required_%s_html', attr),
                         :tag('th')
                         hide = function (tpl_args, frame, value)
                            :wikitext(1)
                             local min = m_game.constants.characters.minimum_attributes[m_game.constants.attributes[attr].arg]
                            :done()
                             return value.min <= min and value.max <= min
                         :tag('th')
                        end,
                            :wikitext(2)
                        hide_key = string.format('required_%s', attr),
                            :done()
                         inline = ', %s ' .. m_game.constants.attributes[attr].short_upper,
                         :tag('th')
                         inline_color = false,
                            :wikitext(3)
                    }
                            :done()
                end
                         :tag('th')
                local requirements = core.factory.infobox_line{parts = parts}(tpl_args, frame)
                            :wikitext(4)
                if requirements == nil then -- return early
                            :done()
                    return
                         :done()
                end
                      
                requirements = string.gsub(requirements, '^, ', '')
                 for _, vtype in ipairs({'tier', 'level'}) do
                return string.format(i18n.tooltips.requires, requirements)
                     local tr = tbl:tag('tr')
            end,
                    tr
        },
                         :tag('th')
        {
                             :wikitext(i18n.tooltips['header_map_' .. vtype])
            show = h.factory.args_present('heist_required_job', 'heist_required_job_level'),
                             :done()
            func = core.factory.infobox_line{
                   
                 parts = {
                    for i=0,4 do
                     {
                         local value = tpl_args['atlas_map_tier' .. i]
                         key = 'heist_required_job_level',
                         if value == 0 then
                        fmt = '%s',
                            value = fields[false].value
                    },
                        elseif vtype == 'level' then
                    {
                            value = value + 67
                        key = 'heist_required_job',
                        end
                        fmt = '%s',
                        tr
                    },
                            :tag('td')
                },
                                :wikitext(value)
                fmt = i18n.tooltips.heist_required_job,
                                :done()
            },
                    end
        },
                    tr:done()
    },
                end
    -- Gem description
               
    {
                 tbl
        class = 'tc -gemdesc',
                     :tag('tr')
        {
                         :tag('th')
            show = h.factory.args_present('gem_description'),
                            :attr('colspan', 6)
            func = h.factory.display_raw_value('gem_description'),
                            :attr('style', 'text-decoration: underline;')
        },
                            :wikitext(i18n.tooltips.header_connections)
    },
                            :done()
    -- Gem Quality Stats
                        :done()
    {
               
        class = 'tc -mod',
                -- sort alphabetically
        {
                local sorted = {}
            show = h.factory.args_present('skill_quality'),
                for key, value in pairs(tpl_args.atlas_connections) do
            func = function (tpl_args, frame)
                    sorted[#sorted+1] = key
                local span = mw.html.create('span')
                end
                span
                table.sort(sorted)
                    :addClass('quality')
               
                for _, key in ipairs(sorted) do
                    local tr = tbl:tag('tr')
                    tr
                        :tag('th')
                            :wikitext(key)
                            :done()
                      
                      
                local span2 = span:tag('span')
                    for i=0,4 do
                span2
                         local field = fields[tpl_args.atlas_connections[key]['region' .. i]]
                    :addClass('quality-header')
                        tr
                    :tag('span')
                            :tag('td')
                         :wikitext(m_util.html.poe_color('default', i18n.tooltips.gem_quality))
                                :attr('data-sort-value', field.sort)
                   
                                :attr('class', field.class)
                for i, quality_data in ipairs(tpl_args.skill_quality) do
                                :wikitext(field.value)
                    local span_inner = span2:tag('span')
                                :done()
                    span_inner
                        :addClass('quality-section-select')
                        :addClass('quality-' .. i)
                        :wikitext(i)
                        :tag('span')
                            :wikitext(i18n.tooltips['gem_quality_' .. i])
                           
                    if i == 1 then
                        span_inner:addClass('quality-selected')
                     end
                     end
                    tr:done()
                 end
                 end
                  
                  
                 for i, quality_data in ipairs(tpl_args.skill_quality) do
                 return tostring(tbl)
                    local span_inner = span:tag('span')
            end
                     span_inner
        },
                         :addClass('quality-box')
    },
                         :addClass('quality-' .. i)
    -- Drop info
                         :wikitext(quality_data.stat_text)
    {
                   
        heading = i18n.tooltips.drop_restrictions,
                    if i == 1 then
        class = '',
                         span_inner:addClass('quality-selected')
        {
                    end
            show = h.factory.args_present('drop_enabled'),
                 end
            func = core.factory.infobox_line{
                  
                parts = {
                 return tostring(span)
                     {
             end,
                         key = 'drop_level',
                         fmt = '%i',
                          
                    },
                    {
                        key = 'drop_level_maximum',
                         fmt = '%i',
                        hide_default = 100,
                    },
                 },
                 sep = ' / ',
                 fmt = i18n.tooltips.level,
             },
         },
         },
    },
    -- Gem Implicit Stats
    {
        class = 'tc -mod',
         {
         {
             show = function (tpl_args, frame)
             show = function (tpl_args, frame)
                 return cfg.class_groups.gems.keys[tpl_args.class_id] and tpl_args.stat_text
                 if tpl_args.drop_enabled == false then
                    return true
                end
                return false
             end,
             end,
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 local lines = {}
                 local span = mw.html.create('span')
                 lines[#lines+1] = tpl_args.stat_text
                 span
                 for _, tag in ipairs(tpl_args.gem_tags) do
                    :attr('class', 'infobox-disabled-drop')
                    if tag == m_game.constants.item.gem_tags.vaal.tag then
                    :wikitext(i18n.tooltips.drop_disabled)
                        lines[#lines+1] = i18n.tooltips.corrupted
                    :done()
                        break
                 return tostring(span)
                     end
            end,
        },
        {
            show = function (tpl_args, frame)
                if tpl_args.is_drop_restricted == true and tpl_args.drop_enabled ~= false then
                     return true
                 end
                 end
                 return table.concat(lines, '<br>')
                 return false
            end,
            func = function (tpl_args, frame)
                local span = mw.html.create('span')
                span
                    :attr('class', 'infobox-restricted-drop')
                    :wikitext(i18n.tooltips.drop_restricted)
                    :done()
                return tostring(span)
             end,
             end,
         },
         },
    },
        {
    -- Implicit Stats
            show = h.factory.args_present('drop_leagues'),
    {
            func = function (tpl_args, frame)
        class = 'tc -mod',
                 return string.format(i18n.tooltips.league_restriction, m_util.html.poe_color('value', table.concat(tpl_args.drop_leagues, ', ')))
        func = function (tpl_args, frame, container_type)
            if tpl_args.implicit_stat_text then
                 return {h.strip_random_stats(tpl_args, frame, tpl_args.implicit_stat_text, container_type)}
            else
                return {}
             end
             end
         end,
         },
        {
            show = h.factory.args_present('drop_areas_html'),
            func = h.factory.display_raw_value('drop_areas_html'),
        },
        {
            show = h.factory.args_present('drop_text'),
            func = h.factory.display_raw_value('drop_text'),
        },
     },
     },
    -- Stats
     {
     {
         class = 'tc -mod',
         heading = i18n.tooltips.purchase_costs,
        func = function (tpl_args, frame, container_type)
            if tpl_args.explicit_stat_text then
                return {h.strip_random_stats(tpl_args, frame, tpl_args.explicit_stat_text, container_type)}
            else
                return {}
            end
        end,
    },
    -- Experience
    --[[{
         {
         {
             show = h.factory.args_present('experience'),
             show = function (tpl_args, frame)
            func = core.factory.infobox_line{
                 for rarity, data in pairs(tpl_args.purchase_costs) do
                parts = {
                    if #data > 0 then
                    {
                        return true
                        key = 'experience',
                     end
                        fmt = '%i',
                    },
                 },
            },
        },
    },]]--
    -- Harvest seeds (lower section)
    {
        class = 'tc -mod',
        {
            show = h.factory.args_present('seed_consumed_wild_lifeforce_percentage'),
            func = function (tpl_args, frame)
                if tpl_args.seed_consumed_wild_lifeforce_percentage > 0 then
                     return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_wild_lifeforce_percentage, m_util.html.poe_color('wild', m_game.seed_types.wild))
                 end
                 end
             end
                return false
        },
             end,
        {
            show = h.factory.args_present('seed_consumed_vivid_lifeforce_percentage'),
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 if tpl_args.seed_consumed_vivid_lifeforce_percentage > 0 then
                 local tbl = mw.html.create('table')
                    return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_vivid_lifeforce_percentage, m_util.html.poe_color('vivid', m_game.seed_types.vivid))
                tbl
                 end
                    --:attr('class', 'wikitable')
             end
                    :attr('style', 'width: 100%; margin-top: 0px;')
                   
                for _, rarity_id in ipairs(m_game.constants.rarity_order) do
                    local data = tpl_args.purchase_costs[rarity_id]
                    if #data > 0 then
                        local tr = tbl:tag('tr')
                        tr
                            :tag('td')
                                :wikitext(m_game.constants.rarities[rarity_id].long_upper)
                        local td = tr:tag('td')
                        for _, purchase_data in ipairs(data) do
                            td:wikitext(string.format('%dx [[%s]]<br />', purchase_data.amount, purchase_data.name))
                        end
                    end
                 end
               
                return tostring(tbl)
             end,
         },
         },
    },
    {
        heading = i18n.tooltips.sell_price,
         {
         {
             show = h.factory.args_present('seed_consumed_primal_lifeforce_percentage'),
             show = h.factory.args_present('sell_price_order'),
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 if tpl_args.seed_consumed_primal_lifeforce_percentage > 0 then
                 local out = {}
                     return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_primal_lifeforce_percentage, m_util.html.poe_color('primal', m_game.seed_types.primal))
                for _, item_name in ipairs(tpl_args.sell_price_order) do
                     out[#out+1] = string.format('%dx [[%s]]', tpl_args.sell_prices[item_name], item_name)
                 end
                 end
             end
               
                return table.concat(out, '<br />')
             end,
         },
         },
         {
    },
             show = h.factory.args_present('seed_required_nearby_seed_tier', 'seed_type_html', 'seed_required_nearby_seed_amount'),
    -- Damage per second
    {
        heading = i18n.tooltips.damage_per_second,
         {
             show = h.factory.args_present('physical_dps_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'seed_required_nearby_seed_amount',
                         key = 'physical_dps_html',
                         fmt = '%s',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                },
                fmt = i18n.tooltips.physical_dps,
            },
        },
        {
            show = h.factory.args_present('fire_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                     {
                     {
                         key = 'seed_type_html',
                         key = 'fire_dps_html',
                         fmt = '%s',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                },
                fmt = i18n.tooltips.fire_dps,
            },
        },
        {
            show = h.factory.args_present('cold_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                     {
                     {
                         key = 'seed_required_nearby_seed_tier',
                         key = 'cold_dps_html',
                         fmt = '%s',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.seed_required_seeds,
                 fmt = i18n.tooltips.cold_dps,
                color = 'mod',
             },
             },
         },
         },
    },
         {
    {
             show = h.factory.args_present('lightning_dps_html'),
        class = 'tc -crafted',
        {
            show = h.factory.args_present('seed_effect'),
            func = h.factory.display_raw_value('seed_effect'),
        },
    },
    -- Description (currency, doodads)
    {
        class = 'tc -mod',
        {
            show = h.factory.args_present('description'),
            func = h.factory.display_raw_value('description'),
        },
         --[[{
             show = h.factory.args_present('plant_booster_additional_crafting_options'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'plant_booster_additional_crafting_options',
                         key = 'lightning_dps_html',
                         fmt = '%s',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.plant_booster_additional_crafting_options,
                 fmt = i18n.tooltips.lightning_dps,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('plant_booster_extra_chances'),
             show = h.factory.args_present('chaos_dps_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'plant_booster_extra_chances',
                         key = 'chaos_dps_html',
                         fmt = '%s%%',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.plant_booster_extra_chances,
                 fmt = i18n.tooltips.chaos_dps,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('plant_booster_lifeforce'),
             show = h.factory.args_present('elemental_dps_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'plant_booster_lifeforce',
                         key = 'elemental_dps_html',
                         fmt = '%s%%',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.plant_booster_lifeforce,
                 fmt = i18n.tooltips.elemental_dps,
             },
             },
         },]]
         },
    },
    {
        class = 'tc -crafted',
         {
         {
             show = h.factory.args_present('incubator_effect'),
             show = h.factory.args_present('poison_dps_html'),
             func = h.factory.display_raw_value('incubator_effect'),
             func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'poison_dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.poison_dps,
            },
         },
         },
    },
    -- Variations (for doodads)
    {
        class = 'tc -mod',
         {
         {
             show = h.factory.args_present('variation_count'),
             show = h.factory.args_present('dps_html'),
             func = function (tpl_args, frame)
             func = core.factory.infobox_line{
                 local txt
                 parts = {
                if tpl_args.variation_count == 1 then
                    {
                     txt = i18n.tooltips.variation_singular
                        key = 'dps_html',
                 else
                        fmt = '%s',
                    txt = i18n.tooltips.variation_plural
                        color = false, -- the html already contains the colour
                end
                     },
                return string.format('%i %s', tpl_args.variation_count, txt)
                 },
             end,
                fmt = i18n.tooltips.dps,
             },
         },
         },
     },
     },
    -- Flavour Text
     {
     {
         class = 'tc -flavour',
         heading = i18n.tooltips.misc,
         {
         {
             show = h.factory.args_present('flavour_text'),
             show = h.factory.args_present('class'),
             func = h.factory.display_raw_value('flavour_text'),
             func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'class',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.item_class,
            },
         },
         },
    },
    -- Prophecy text
    {
        class = 'tc -value',
         {
         {
             show = h.factory.args_present('prediction_text'),
             show = h.factory.args_present('metadata_id'),
            func = h.factory.display_raw_value('prediction_text'),
        },
    },
    -- Can not be traded or modified
    {
        class = 'tc -canttradeormodify',
        {
            show = h.factory.args_present('cannot_be_traded_or_modified'),
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 if tpl_args.cannot_be_traded_or_modified == true then
                 return core.factory.infobox_line{
                     return i18n.tooltips.cannot_be_traded_or_modified
                    parts = {
                 end
                        {
                            key = 'metadata_id',
                            fmt = '%s',
                            inline = m_util.html.abbr('%s', tpl_args.metadata_id),
                        },
                    },
                     fmt = tostring(
                        mw.html.create('span')
                            :addClass('u-truncate-line')
                            :wikitext(i18n.tooltips.metadata_id)
                    ),
                 }(tpl_args, frame)
             end,
             end,
         },
         },
     },
     },
    -- Help text
}
    {
 
        class = 'tc -help',
-- ----------------------------------------------------------------------------
        {
-- Subroutines
            show = h.factory.args_present('help_text'),
-- ----------------------------------------------------------------------------
            func = h.factory.display_raw_value('help_text'),
 
        },
local s = {}
    },
    -- Cost (i.e. vendor costs)
    {
        --class = '',
        {
            show = h.factory.args_present('master_favour_cost'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'master_favour_cost',
                        color = 'currency',
                    },
                },
                fmt = i18n.tooltips.favour_cost,
            },
        },
        {
            show = h.factory.args_present('seal_cost'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'seal_cost',
                        fmt = function (tpl_args, frame, value)
                            return '%dx ' .. h.item_link{metadata_id='Metadata/Items/Currency/CurrencySilverCoin', html=''}
                        end,
                        color = 'currency',
                    },
                },
                fmt = i18n.tooltips.seal_cost,
            },
        },
    },
}


--
-- Subroutines for p.item. These exist to declutter the main function. Each one of these is only called once.
-- This is meant to show additional information about the item in a separate infobox
 
--
function s.get_item_config(tpl_args, frame)
c.extra_display_groups = {
     -- Returns primary item configuration, based on item class
     {
 
        heading = i18n.tooltips.extra_info,
    h.process_arguments(tpl_args, frame, {'class_id', 'class'})
        class = '',
    local config = {
        {
        tables = cfg.tables,
            show = h.factory.args_present('atlas_connections'),
        args = cfg.default_args,
            func = function (tpl_args, frame)
        late_args = cfg.late_args,
                local fields = {
        defaults = {},
                    [false] = {
    }
                        value = '',
    local extend_keys = m_util.table.keys(config)
                        sort = 0,
    for _, row in pairs(cfg.class_groups) do
                        class = 'table-cell-xmark',
        if row.keys[tpl_args.class_id] then
                    },
            for _, k in ipairs(extend_keys) do
                    [true] = {
                if row[k] then
                        value = '✓',
                     for _, v in ipairs(row[k]) do
                        sort = 1,
                         table.insert(config[k], v)
                        class = 'table-cell-checkmark',
                    end
                    },
                end
                }
            end
               
            break
                local tbl = mw.html.create('table')
        end
                tbl
    end
                    :attr('class', 'wikitable')
    local class_specifics = cfg.class_specifics[tpl_args.class_id]
                    :attr('style', 'width:100%;')
    if class_specifics then
                     :tag('tr')
        for _, k in ipairs(extend_keys) do
                         :tag('th')
            if class_specifics[k] then
                            :attr('colspan', 6)
                for _, v in ipairs(class_specifics[k]) do
                            :attr('style', 'text-decoration: underline;')
                     table.insert(config[k], v)
                            :wikitext(i18n.tooltips.header_overall)
                end
                            :done()
            end
                        :done()
        end
                     :tag('tr')
    end
                        :tag('th')
    return config
                            :wikitext(i18n.tooltips.header_upgrades)
end
                            :done()
 
                        :tag('th')
function s.process_quest_rewards(tpl_args, frame)
                            :wikitext(0)
    local rid = 1
                            :done()
    local continue
                        :tag('th')
    tpl_args.quest_rewards = {}
                            :wikitext(1)
    tpl_args.vendor_rewards = {}
                            :done()
    repeat
                        :tag('th')
        continue = true
                            :wikitext(2)
        local prefix = string.format('quest_reward%s_', rid)
                            :done()
       
                        :tag('th')
        local input_args = {
                            :wikitext(3)
            shared = {
                            :done()
                ['type'] = true,
                        :tag('th')
                ['quest'] = false,
                            :wikitext(4)
                ['quest_id'] = false,
                            :done()
                ['act'] = true,
                        :done()
                ['class_ids'] = false,
                   
                 },
                 for _, vtype in ipairs({'tier', 'level'}) do
            vendor = {
                    local tr = tbl:tag('tr')
                ['npc'] = true,
                    tr
            },
                        :tag('th')
            quest = {
                            :wikitext(i18n.tooltips['header_map_' .. vtype])
                ['sockets'] = false,
                            :done()
                ['item_level'] = false,
                   
                ['rarity_id'] = false,
                    for i=0,4 do
                ['notes'] = false,
                        local value = tpl_args['atlas_map_tier' .. i]
            },
                        if value == 0 then
        }
                            value = fields[false].value
       
                        elseif vtype == 'level' then
        local rdata = {}
                            value = value + 67
       
                        end
        for key, is_required in pairs(input_args.shared) do
                        tr
            rdata[key] = tpl_args[prefix .. key]
                            :tag('td')
            if is_required then
                                :wikitext(value)
                if rdata[key] == nil then
                                :done()
                    continue = false
                    end
                     break
                     tr:done()
                 end
                 end
               
            end
                tbl
        end
                    :tag('tr')
       
                        :tag('th')
        if rdata.quest == nil or rdata.quest_id == nil then
                            :attr('colspan', 6)
            continue = false
                            :attr('style', 'text-decoration: underline;')
        end
                            :wikitext(i18n.tooltips.header_connections)
       
                            :done()
        if continue and rdata.type == 'vendor' or rdata.type == 'quest' then
                        :done()
            for key, is_required in pairs(input_args[rdata.type]) do
               
                rdata[key] = tpl_args[prefix .. key]
                -- sort alphabetically
                if is_required then
                local sorted = {}
                     if rdata[key] == nil then
                for key, value in pairs(tpl_args.atlas_connections) do
                        continue = false
                     sorted[#sorted+1] = key
                        break
                    end
                 end
                 end
                 table.sort(sorted)
            end
               
        else
                 for _, key in ipairs(sorted) do
            continue = false
                     local tr = tbl:tag('tr')
        end
                     tr
       
                         :tag('th')
        if continue then
                            :wikitext(key)
            rdata.classes = {}
                            :done()
            if rdata.class_ids ~= nil then
                      
                 rdata.class_ids = m_util.string.split(rdata.class_ids, ',%s*')
                    for i=0,4 do
                 for index, class_id in ipairs(rdata.class_ids) do
                         local field = fields[tpl_args.atlas_connections[key]['region' .. i]]
                     local class = m_game.constants.characters[class_id]
                         tr
                     if class == nil then
                            :tag('td')
                         error(string.format('Class id %s is invalid', class_id))
                                :attr('data-sort-value', field.sort)
                     else
                                :attr('class', field.class)
                         rdata.class_ids[index] = class.str_id
                                :wikitext(field.value)
                         rdata.classes[index] = class.name
                                :done()
                     end
                     end
                    tr:done()
                 end
                 end
               
                return tostring(tbl)
             end
             end
        },
           
    },
           
    -- Drop info
            if rdata.item_level then
    {
                rdata.item_level = m_util.cast.number(rdata.item_level)
        heading = i18n.tooltips.drop_restrictions,
            end
        class = '',
           
        {
             if rdata.rarity_id then
             show = h.factory.args_present('drop_enabled'),
                if m_game.constants.rarities[rdata.rarity_id] == nil then
            func = core.factory.infobox_line{
                     error(string.format(i18n.errors.invalid_rarity_id, tostring(rdata.rarity_id)))
                parts = {
                    {
                        key = 'drop_level',
                        fmt = '%i',
                       
                     },
                    {
                        key = 'drop_level_maximum',
                        fmt = '%i',
                        hide_default = 100,
                    },
                },
                sep = ' / ',
                fmt = i18n.tooltips.level,
            },
        },
        {
            show = function (tpl_args, frame)
                if tpl_args.drop_enabled == false then
                    return true
                 end
                 end
                return false
             end
             end,
              
             func = function (tpl_args, frame)
            rdata._table = rdata.type .. '_rewards'
                local span = mw.html.create('span')
            rdata.type = nil
                span
           
                    :attr('class', 'infobox-disabled-drop')
            tpl_args[rdata._table] = rdata
                    :wikitext(i18n.tooltips.drop_disabled)
           
                    :done()
            m_cargo.store(frame, rdata)
                return tostring(span)
           
             end,
             -- TODO: Verify quests and quest ids?
         },
        end
         {
          
            show = function (tpl_args, frame)
         rid = rid + 1
                if tpl_args.is_drop_restricted == true and tpl_args.drop_enabled ~= false then
    until continue == false
                    return true
end
                end
 
                return false
function s.process_base_item(tpl_args, frame)
            end,
    local where
            func = function (tpl_args, frame)
    if tpl_args.base_item_id ~= nil then
                local span = mw.html.create('span')
        where = string.format('items.metadata_id="%s"', tpl_args.base_item_id)
                span
    elseif tpl_args.base_item_page ~= nil then
                    :attr('class', 'infobox-restricted-drop')
        where = string.format('items._pageName="%s"' , tpl_args.base_item_page)
                    :wikitext(i18n.tooltips.drop_restricted)
    elseif tpl_args.base_item ~= nil then
                    :done()
        where = string.format('items.name="%s"' , tpl_args.base_item)
                return tostring(span)
    elseif tpl_args.rarity_id ~= 'normal' then
            end,
        error(i18n.errors.missing_base_item)
        },
    else
        {
        return
            show = h.factory.args_present('drop_leagues'),
    end
            func = function (tpl_args, frame)
   
                return string.format(i18n.tooltips.league_restriction, m_util.html.poe_color('value', table.concat(tpl_args.drop_leagues, ', ')))
    if where ~= nil and tpl_args.rarity_id == 'normal' and not tpl_args._flags.is_prophecy then
            end
        error(i18n.errors.missing_rarity)
        },
    end
         {
   
             show = h.factory.args_present('drop_areas_html'),
    where = string.format('%s AND items.class_id="%s" AND items.rarity_id="normal"', where, tpl_args.class_id)
            func = h.factory.display_raw_value('drop_areas_html'),
   
        },
    local join = {}
         {
   
             show = h.factory.args_present('drop_text'),
    for _, table_name in ipairs(tpl_args._item_config.tables) do
            func = h.factory.display_raw_value('drop_text'),
         if table_name ~= 'items' then
         },
             join[#join+1] = string.format('items._pageID=%s._pageID', table_name)
     },
        end
     {
    end
         heading = i18n.tooltips.purchase_costs,
   
    local fields = {
        'items._pageName',
        'items.name',
        'items.metadata_id',
    }
   
    for _, k in ipairs(tpl_args._base_item_args) do
         local data = core.map[k]
        if data.field ~= nil then
             fields[#fields+1] = string.format('%s.%s', data.table, data.field)
         end
    end
      
     local result = m_cargo.query(
         tpl_args._item_config.tables,
        fields,
         {
         {
             show = function (tpl_args, frame)
             where=where,
                for rarity, data in pairs(tpl_args.purchase_costs) do
            join=table.concat(join, ','),
                    if #data > 0 then
            groupBy='items._pageID',
                        return true
        }
                    end
    )
                end
   
                return false
    if #result > 1 then
            end,
        error(i18n.errors.duplicate_base_items)
            func = function (tpl_args, frame)
        -- TODO be more explicit in the error?
                local tbl = mw.html.create('table')
    elseif #result == 0 then
                tbl
        error(i18n.errors.base_item_not_found)
                    --:attr('class', 'wikitable')
    end
                    :attr('style', 'width: 100%; margin-top: 0px;')
   
                   
    result = result[1]
                for _, rarity_id in ipairs(m_game.constants.rarity_order) do
   
                    local data = tpl_args.purchase_costs[rarity_id]
    tpl_args.base_item_data = result
                    if #data > 0 then
    h.process_arguments(tpl_args, frame, {'base_item', 'base_item_page', 'base_item_id'})
                        local tr = tbl:tag('tr')
 
                        tr
    --Copy values..
                            :tag('td')
    for _, k in ipairs(tpl_args._base_item_args) do
                                :wikitext(m_game.constants.rarities[rarity_id].long_upper)
        local data = core.map[k]
                        local td = tr:tag('td')
        if data.field ~= nil and data.func_fetch == nil then
                        for _, purchase_data in ipairs(data) do
            local value = result[string.format('%s.%s', data.table, data.field)]
                            td:wikitext(string.format('%dx [[%s]]<br />', purchase_data.amount, purchase_data.name))
            -- I can just use data.default since it will be nil if not provided (nil == nil). Neat! ;)
                        end
            if value ~= nil and (tpl_args[k] == data.default or type(data.default) == 'function') then
                    end
                tpl_args[k] = value
                if data.func ~= nil then
                    tpl_args[k] = data.func(tpl_args, frame, tpl_args[k])
                end
                if data.func_copy ~= nil then
                    data.func_copy(tpl_args, frame)
                end
            elseif value == nil and not data.debug_ignore_nil then
                if tpl_args.debug then
                    error(string.format(i18n.debug.base_item_field_not_found, data.table, data.field))
                 end
                 end
               
             elseif tpl_args[k] ~= data.default then
                return tostring(tbl)
                 if tpl_args.debug then
             end,
                     error(string.format(i18n.debug.field_value_mismatch, k, tostring(tpl_args[k])))
        },
    },
    {
        heading = i18n.tooltips.sell_price,
        {
            show = h.factory.args_present('sell_price_order'),
            func = function (tpl_args, frame)
                 local out = {}
                for _, item_name in ipairs(tpl_args.sell_price_order) do
                     out[#out+1] = string.format('%dx [[%s]]', tpl_args.sell_prices[item_name], item_name)
                 end
                 end
               
            end
                return table.concat(out, '<br />')
        elseif data.func_fetch ~= nil then
            end,
            data.func_fetch(tpl_args, frame)
        },
        end
     },
    end
    -- Damage per second
end
    {
 
         heading = i18n.tooltips.damage_per_second,
function s.process_mods(tpl_args, frame)
         {
     for _, k in ipairs({'implicit', 'explicit'}) do
             show = h.factory.args_present('physical_dps_html'),
        local success = true
             func = core.factory.infobox_line{
         local i = 1
                parts = {
         while success do
                    {
             success = h.validate_mod(tpl_args, frame, {key=k, i=i})
                        key = 'physical_dps_html',
             i = i + 1
                        fmt = '%s',
        end
                        color = false, -- the html already contains the colour
    end
                    },
 
                },
    -- If the item does not have its own implicit mods, fall back to the implicit mods on the base item.
                fmt = i18n.tooltips.physical_dps,
    local implicit_mods = tpl_args._defined_implicit_mods
            },
    if #implicit_mods == 0 then
         },
        implicit_mods = tpl_args._base_implicit_mods
         {
    end
             show = h.factory.args_present('fire_dps_html'),
    for _, v in ipairs(implicit_mods) do
             func = core.factory.infobox_line{
        table.insert(tpl_args._mods, v)
                 parts = {
    end
                    {
    if #tpl_args._mods > 0 then
                        key = 'fire_dps_html',
        local mods = {}
                        fmt = '%s',
         local mod_ids = {}
                        color = false, -- the html already contains the colour
         local non_random_mod_ids = {}
                    },
        for _, mod_data in ipairs(tpl_args._mods) do
                 },
             if mod_data.result == nil then
                 fmt = i18n.tooltips.fire_dps,
                mods[mod_data.id] = mod_data
             },
                mod_ids[#mod_ids+1] = mod_data.id
         },
                if not mod_data.is_random then
         {
                    table.insert(non_random_mod_ids, mod_data.id)
             show = h.factory.args_present('cold_dps_html'),
                end
             func = core.factory.infobox_line{
            end
                parts = {
             tpl_args._subobjects[#tpl_args._subobjects+1] = {
                    {
                 _table = 'item_mods',
                        key = 'cold_dps_html',
                id = mod_data.id,
                        fmt = '%s',
                text = mod_data.stat_text,
                        color = false, -- the html already contains the colour
                 is_implicit = mod_data.is_implicit,
                    },
                 is_random = mod_data.is_random,
                },
             }
                fmt = i18n.tooltips.cold_dps,
         end
             },
       
         },
         local results = m_cargo.array_query{
         {
             tables={'mods'},
             show = h.factory.args_present('lightning_dps_html'),
             fields={'mods._pageName', 'mods.id', 'mods.required_level', 'mods.stat_text'},
             func = core.factory.infobox_line{
            id_field='mods.id',
                 parts = {
             id_array=mod_ids,
                    {
         }
                        key = 'lightning_dps_html',
          
                        fmt = '%s',
        for _, data in ipairs(results) do
                        color = false, -- the html already contains the colour
             local mod_data = mods[data['mods.id']]
                    },
             mod_data.result = data
                 },
           
                fmt = i18n.tooltips.lightning_dps,
            if mod_data.is_random == false then
             },
                -- update item level requirement
         },
                 local keys = {'required_level_final'}
         {
                -- only update base item requirement if this is an implicit
            show = h.factory.args_present('chaos_dps_html'),
                if mod_data.key == 'implicit' then
             func = core.factory.infobox_line{
                    keys[#keys+1] = 'required_level'
                 parts = {
                end
                    {
               
                        key = 'chaos_dps_html',
                 for _, key in ipairs(keys) do
                        fmt = '%s',
                    local req = math.floor(tonumber(data['mods.required_level']) * cfg.item_required_level_modifier_contribution)
                        color = false, -- the html already contains the colour
                    if req > tpl_args[key] then
                    },
                        tpl_args[key] = req
                },
                    end
                fmt = i18n.tooltips.chaos_dps,
                end
             },
             end
         },
        end
        {
       
             show = h.factory.args_present('elemental_dps_html'),
         -- fetch stats
             func = core.factory.infobox_line{
          
                 parts = {
        results = m_cargo.query(
                    {
            {'mods', 'mod_stats'},
                        key = 'elemental_dps_html',
             {'mods.id', 'mod_stats.id', 'mod_stats.min', 'mod_stats.max'},
                        fmt = '%s',
            {
                        color = false, -- the html already contains the colour
                 join='mods._pageID=mod_stats._pageID',
                    },
                where=string.format('mod_stats.id IS NOT NULL AND mods.id IN ("%s")', table.concat(mod_ids, '", "')),
                },
             }
                fmt = i18n.tooltips.elemental_dps,
         )
             },
        for _, data in ipairs(results) do
        },
            -- Stat subobject
        {
             local mod_data = mods[data['mods.id']]
             show = h.factory.args_present('poison_dps_html'),
             if mod_data.result.stats == nil then
             func = core.factory.infobox_line{
                mod_data.result.stats = {data, }
                 parts = {
            else
                    {
                 mod_data.result.stats[#mod_data.result.stats+1] = data
                        key = 'poison_dps_html',
            end
                        fmt = '%s',
       
                        color = false, -- the html already contains the colour
            local id = data['mod_stats.id']
                    },
            local value = {
                },
                min = tonumber(data['mod_stats.min']),
                fmt = i18n.tooltips.poison_dps,
                max = tonumber(data['mod_stats.max']),
             },
            }
         },
            value.avg = (value.min+value.max)/2
         {
              
            show = h.factory.args_present('dps_html'),
             local prefix = ''
             func = core.factory.infobox_line{
             if mod_data.is_random then
                 parts = {
                 prefix = '_random'
                    {
            end
                        key = 'dps_html',
            core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_stats')
                        fmt = '%s',
            if mod_data.is_implicit then
                        color = false, -- the html already contains the colour
                core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_implicit_stats')
                    },
            else
                 },
                core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_explicit_stats')
                fmt = i18n.tooltips.dps,
             end
            },
         end
        },
          
    },
        if tpl_args._flags.sell_prices_override ~= true then
    {
            -- fetch sell prices
        heading = i18n.tooltips.misc,
             results = m_cargo.query(
        {
                 {'mods', 'mod_sell_prices'},
            show = h.factory.args_present('class'),
                 {'mods.id', 'mod_sell_prices.amount', 'mod_sell_prices.name'},
             func = core.factory.infobox_line{
                {
                 parts = {
                    join='mods._pageID=mod_sell_prices._pageID',
                     {
                    -- must be non random mods to avoid accumulating sell prices of randomized modifiers
                         key = 'class',
                    where=string.format('mod_sell_prices.amount IS NOT NULL AND mods.id IN ("%s")', table.concat(non_random_mod_ids, '", "')),
                         fmt = '%s',
                }
                     },
            )
                },
           
                fmt = i18n.tooltips.item_class,
             for _, data in ipairs(results) do
             },
                local mod_data = mods[data['mods.id']]
        },
                 if not mod_data.is_implicit then
        {
                     local values = {
            show = h.factory.args_present('metadata_id'),
                         name = data['mod_sell_prices.name'],
             func = function (tpl_args, frame)
                         amount = tonumber(data['mod_sell_prices.amount']),  
                return core.factory.infobox_line{
                     }
                    parts = {
                    -- sell_prices is defined in tpl_args.sell_prices_override
                        {
                    tpl_args.sell_prices[values.name] = (tpl_args.sell_prices[values.name] or 0) + values.amount
                            key = 'metadata_id',
                end
                            fmt = '%s',
             end
                            inline = m_util.html.abbr('%s', tpl_args.metadata_id),
        end
                        },
    end
                    },
    if tpl_args._flags.sell_prices_override ~= true then
                    fmt = tostring(
        local missing_sell_price = true
                        mw.html.create('span')
        for _, _ in pairs(tpl_args.sell_prices) do
                            :addClass('u-truncate-line')
             missing_sell_price = false
                            :wikitext(i18n.tooltips.metadata_id)
            break
                    ),
        end
                 }(tpl_args, frame)
       
             end,
        if missing_sell_price then
         },
            tpl_args.sell_prices[i18n.tooltips.default_vendor_offer] = 1
     },
        end
}
       
        -- Set sell price on page
        for name, amount in pairs(tpl_args.sell_prices) do
            -- sell_price_order is defined in tpl_args.sell_prices_override
            tpl_args.sell_price_order[#tpl_args.sell_price_order+1] = name
            tpl_args._subobjects[#tpl_args._subobjects+1] = {
                _table = 'item_sell_prices',
                amount = amount,
                 name = name,
             }
        end
         table.sort(tpl_args.sell_price_order)
     end
end


-- ----------------------------------------------------------------------------
function s.process_stats(tpl_args, frame)
-- Exported functions
    -- Add stats - this is for when mods are not set, but we still need stats to calcuate new armour values etc
-- ----------------------------------------------------------------------------
    m_util.args.stats(tpl_args, {prefix='extra_'})
    for _, stat in ipairs(tpl_args.extra_stats) do
        if stat.value ~= nil then
            stat.min = stat.value
            stat.max = stat.value
            stat.avg = stat.value
        end
        core.stats_update(tpl_args, stat.id, stat, nil, '_stats')
        core.stats_update(tpl_args, stat.id, stat, nil, '_explicit_stats')
    end


local p = {}
    -- Transpose stats into cargo data
 
    for _, random_prefix in ipairs({'', '_random'}) do
--
        for _, type_prefix in ipairs({'', '_implicit', '_explicit'}) do
-- Template:Item
            for id, data in pairs(tpl_args[random_prefix .. type_prefix .. '_stats']) do
--
                local is_implicit
function p.item(frame)
                if type_prefix == '_implicit' then
     local t = os.clock()
                    is_implicit = true
                elseif type_prefix == '_explicit' then
                    is_implicit = false
                end
                tpl_args._subobjects[#tpl_args._subobjects+1] = {
                    _table = 'item_stats',
                    id = id,
                    min = data.min,
                    max = data.max,
                    avg = data.avg,
                    is_implicit = is_implicit,
                    is_random = random_prefix == '_random',
                }
            end
        end
     end
      
      
     local tpl_args = getArgs(frame, {
     -- Handle extra stats (for gems)
        parentFirst = true
     if cfg.class_groups.gems.keys[tpl_args.class_id] then
    })
        h.skill(tpl_args, frame)
     frame = m_util.misc.get_frame(frame)
    end
      
      
     --
     --
     -- Shared args
     -- Handle local stats increases/reductions/additions
     --
     --
      
      
     tpl_args._flags = {}
     local skip = {}
    tpl_args._base_item_args = {}
    tpl_args._base_implicit_mods = {}
    tpl_args._defined_implicit_mods = {}
    tpl_args._mods = {}
    for _, k in ipairs({'', '_random'}) do
        for _, prefix in ipairs({'', '_implicit', '_explicit'}) do
            tpl_args[k .. prefix .. '_stats'] = {}
        end
    end
    tpl_args._subobjects = {}
    tpl_args._properties = {}
    tpl_args._errors = {}
      
      
    h.process_arguments(tpl_args, frame, {'class_id'})
     -- general stats
    h.build_item_classes(tpl_args, frame)
     for k, data in pairs(core.stat_map) do
    local cargo_data = h.build_cargo_data(tpl_args, frame, c.item_classes)
         local value = tpl_args[k]
   
         if value ~= nil and skip[k] == nil then
     -- Using general purpose function to handle release and removal versions
             value = {min=value, max=value, base=value}
    m_util.args.version(tpl_args, {frame=frame, set_properties=true})
             -- If stats are overriden we scan save some CPU time here
   
            local overridden = false
     -- Must validate some argument early. It is required for future things
            if data.stats_override ~= nil then
    h.process_arguments(tpl_args, frame, cfg.default_args)
                for stat_id, override_value in pairs(data.stats_override) do
    h.process_arguments(tpl_args, frame, c.item_classes[tpl_args.class_id].args)
                    local stat_value = tpl_args._stats[stat_id]
   
                    if stat_value ~= nil then
    -- Base Item
                        -- Use the value of stat
    h.process_base_item(tpl_args, frame)
                        if override_value == true then
   
                            value.min = stat_value.min
    -- Prophecy special snowflake
                            value.max = stat_value.max
    if tpl_args._flags.is_prophecy then
                            overridden = true
         err = h.process_arguments(tpl_args, frame, cfg.prophecy_args)
                        elseif stat_value ~= 0 then
         if err then
                            value.min = override_value.min
            return err
                            value.max = override_value.max
        end
                            overridden = true
    end
                        end
   
                    end
    -- Mods
                end
    for _, k in ipairs({'implicit', 'explicit'}) do
          end
        local success = true
               
        local i = 1
          if overridden == false then
        while success do
                -- The simple cases; this must be using ipairs as "add" must apply before
             success = h.validate_mod(tpl_args, frame, {key=k, i=i})
                for _, operator in ipairs({'add', 'more'}) do
             i = i + 1
                    local st = data['stats_' .. operator]
        end
                    if st ~= nil then
    end
                        for _, statid in ipairs(st) do
   
                            if tpl_args._stats[statid] ~= nil then
    h.process_mods(tpl_args, frame)
                                h.stat[operator](value, tpl_args._stats[statid])
       
                            end
    -- Add stats - this is for when mods are not set, but we still need stats to calcuate new armour values etc
                        end
    m_util.args.stats(tpl_args, {prefix='extra_'})
                     end
    for _, stat in ipairs(tpl_args.extra_stats) do
        if stat.value ~= nil then
            stat.min = stat.value
            stat.max = stat.value
            stat.avg = stat.value
        end
        core.stats_update(tpl_args, stat.id, stat, nil, '_stats')
        core.stats_update(tpl_args, stat.id, stat, nil, '_explicit_stats')
    end
 
    -- Transpose stats into cargo data
    for _, random_prefix in ipairs({'', '_random'}) do
        for _, type_prefix in ipairs({'', '_implicit', '_explicit'}) do
            for id, data in pairs(tpl_args[random_prefix .. type_prefix .. '_stats']) do
                local is_implicit
                if type_prefix == '_implicit' then
                     is_implicit = true
                elseif type_prefix == '_explicit' then
                    is_implicit = false
                 end
                 end
                 tpl_args._subobjects[#tpl_args._subobjects+1] = {
                  
                     _table = 'item_stats',
                -- For increased stats we need to add them up first
                     id = id,
                for stat_key, stat_func in pairs({increased=h.stat.more, increased_inverse=h.stat.more_inverse}) do
                    min = data.min,
                     local st = data['stats_' .. stat_key]
                    max = data.max,
                     if st ~= nil then
                    avg = data.avg,
                        local total_increase = {min=0, max=0}
                     is_implicit = is_implicit,
                        for _, statid in ipairs(st) do
                     is_random = random_prefix == '_random',
                            if tpl_args._stats[statid] ~= nil then
                 }
                                for var, current_value in pairs(total_increase) do
                                    total_increase[var] = current_value + tpl_args._stats[statid][var]
                                end
                            end
                        end
                        stat_func(value, total_increase)
                     end
                end
               
                if data.minimum ~= nil then
                     for _, key in ipairs({'min', 'max'}) do
                        if value[key] < data.minimum then
                            value[key] = data.minimum
                        end
                    end
                 end
            else
 
             end
             end
        end
           
    end
            value.avg = (value.min + value.max) / 2
   
           
    -- Handle extra stats (for gems)
            -- don't add the properties unless we need to
   
            if (data.default ~= nil and (value.min ~= data.default or value.max ~= data.default)) or data.default == nil then
    if cfg.class_groups.gems.keys[tpl_args.class_id] then
                 for short_key, range_data in pairs(c.range_map) do
        m_skill._skill(tpl_args, frame)
                     tpl_args[data.field .. range_data.var] = value[short_key]
    end
   
    --
    -- Handle local stats increases/reductions/additions
    --
   
    local skip = {}
   
    -- general stats
    for k, data in pairs(core.stat_map) do
        local value = tpl_args[k]
       
        if value ~= nil and skip[k] == nil then
            value = {min=value, max=value, base=value}
            -- If stats are overriden we scan save some CPU time here
            local overridden = false
            if data.stats_override ~= nil then
                 for stat_id, override_value in pairs(data.stats_override) do
                     local stat_value = tpl_args._stats[stat_id]
                    if stat_value ~= nil then
                        -- Use the value of stat
                        if override_value == true then
                            value.min = stat_value.min
                            value.max = stat_value.max
                            overridden = true
                        elseif stat_value ~= 0 then
                            value.min = override_value.min
                            value.max = override_value.max
                            overridden = true
                        end
                    end
                 end
                 end
          end
                  
                  
          if overridden == false then
                 -- process to HTML to use on list pages or other purposes
                 -- The simple cases; this must be using ipairs as "add" must apply before
                 h.handle_range_args(tpl_args, frame, k, data.field, value, data.html_fmt_options or {})
                 for _, operator in ipairs({'add', 'more'}) do
            end
                    local st = data['stats_' .. operator]
           
                    if st ~= nil then
            for short_key, range_data in pairs(c.range_map) do
                        for _, statid in ipairs(st) do
                tpl_args[k .. range_data.var] = value[short_key]
                            if tpl_args._stats[statid] ~= nil then
            end
                                h.stat[operator](value, tpl_args._stats[statid])
        end
                            end
    end
                        end
end
                    end
 
function s.process_weapon_dps(tpl_args, frame)
    for key, data in pairs(core.dps_map) do
        local damage = {
            min = {},
            max = {},
        }
        for var_type, value in pairs(damage) do
            -- covers the min/max/avg range
            for short_key, range_data in pairs(c.range_map) do
                value[short_key] = 0
                for _, damage_key in ipairs(data.damage_args) do
                    value[short_key] = value[short_key] + (tpl_args[string.format('%s_%s%s', damage_key, var_type, range_data.var)] or 0)
                 end
                 end
               
            end
                -- For increased stats we need to add them up first
        end
                for stat_key, stat_func in pairs({increased=h.stat.more, increased_inverse=h.stat.more_inverse}) do
        local value = {}
                    local st = data['stats_' .. stat_key]
        for short_key, range_data in pairs(c.range_map) do
                    if st ~= nil then
            local result = (damage.min[short_key] + damage.max[short_key]) / 2 * tpl_args[string.format('attack_speed%s', range_data.var)]
                        local total_increase = {min=0, max=0}
            value[short_key] = result
                        for _, statid in ipairs(st) do
            tpl_args[string.format('%s%s', data.field, range_data.var)] = result
                            if tpl_args._stats[statid] ~= nil then
        end
                                for var, current_value in pairs(total_increase) do
        if value.avg > 0 then
                                    total_increase[var] = current_value + tpl_args._stats[statid][var]
            h.handle_range_args(tpl_args, frame, key, data.field, value, data.html_fmt_options or {})
                                end
        end
                            end
    end
                        end
end
                        stat_func(value, total_increase)
 
                    end
function s.get_categories(tpl_args, frame)
                end
    local cats = {}
               
    if tpl_args.rarity_id == 'unique' then
                if data.minimum ~= nil then
        cats[#cats+1] = string.format(i18n.categories.unique_affix, tpl_args.class)
                    for _, key in ipairs({'min', 'max'}) do
    elseif tpl_args._flags.is_prophecy then
                        if value[key] < data.minimum then
        cats[#cats+1] = i18n.categories.prophecies
                            value[key] = data.minimum
    elseif tpl_args._flags.is_blight_item then
                        end
        cats[#cats+1] = i18n.categories.blight_item
                    end
    elseif tpl_args.is_talisman then
                end
        cats[#cats+1] = i18n.categories.talismans
            else
    elseif tpl_args.is_essence then
 
        cats[#cats+1] = i18n.categories.essences
            end
    elseif tpl_args.class_id == 'Map' then
           
        cats[#cats+1] = string.format('%s %s', tpl_args.map_series, tpl_args.class)
            value.avg = (value.min + value.max) / 2
    else
           
        cats[#cats+1] = tpl_args.class
            -- don't add the properties unless we need to
    end
            if (data.default ~= nil and (value.min ~= data.default or value.max ~= data.default)) or data.default == nil then
   
                for short_key, range_data in pairs(c.range_map) do
    if tpl_args.rarity_id ~= 'normal' or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
                    tpl_args[data.field .. range_data.var] = value[short_key]
        cats[#cats+1] = i18n.categories.derived_items
                end
    else
               
        cats[#cats+1] = i18n.categories.base_items
                -- process to HTML to use on list pages or other purposes
    end
                h.handle_range_args(tpl_args, frame, k, data.field, value, data.html_fmt_options or {})
   
            end
    for _, attr in ipairs(m_game.constants.attribute_order) do
           
        if tpl_args[attr .. '_percent'] then
            for short_key, range_data in pairs(c.range_map) do
            cats[#cats+1] = string.format('%s %s', m_game.constants.attributes[attr].long_upper, tpl_args.class)
                tpl_args[k .. range_data.var] = value[short_key]
        end
            end
    end
   
    local affix
    if tpl_args.class_id == 'Active Skill Gem' or tpl_args.class_id == 'Support Skill Gem' then
        affix = i18n.categories.gem_tag_affix
    end
    if affix ~= nil then
        for _, tag in ipairs(tpl_args.gem_tags) do
            cats[#cats+1] = string.format(affix, tag)
         end
         end
     end
     end


    -- calculate and handle weapon dps
     if #tpl_args._defined_implicit_mods > 0 and tpl_args.rarity_id ~= 'normal' then
     if cfg.class_groups.weapons.keys[tpl_args.class_id] then
        cats[#cats+1] = i18n.categories.implicit_modifier_override
        for key, data in pairs(core.dps_map) do
            local damage = {
                min = {},
                max = {},
            }
       
            for var_type, value in pairs(damage) do
                -- covers the min/max/avg range
                for short_key, range_data in pairs(c.range_map) do
                    value[short_key] = 0
                    for _, damage_key in ipairs(data.damage_args) do
                        value[short_key] = value[short_key] + (tpl_args[string.format('%s_%s%s', damage_key, var_type, range_data.var)] or 0)
                    end
                end
            end
 
            local value = {}
            for short_key, range_data in pairs(c.range_map) do
                local result = (damage.min[short_key] + damage.max[short_key]) / 2 * tpl_args[string.format('attack_speed%s', range_data.var)]
                value[short_key] = result
                tpl_args[string.format('%s%s', data.field, range_data.var)] = result
            end
           
            if value.avg > 0 then
                h.handle_range_args(tpl_args, frame, key, data.field, value, data.html_fmt_options or {})
            end
        end
     end
     end
      
      
     -- late processing
     if #tpl_args.alternate_art_inventory_icons > 0 then
    h.process_arguments(tpl_args, frame, cfg.late_args) -- General late args
        cats[#cats+1] = i18n.categories.alternate_artwork
    h.process_arguments(tpl_args, frame, c.item_classes[tpl_args.class_id].late_args) -- Class-specific late args
    end
      
      
     -- Handle upgrade from restrictions/info
     if tpl_args.release_version == nil then
     h.process_upgraded_from(tpl_args, frame)
        cats[#cats+1] = i18n.categories.missing_release_version
     end
      
      
     -- Quest reward info
     if tpl_args._flags.text_modifier and not tpl_args.suppress_improper_modifiers_category then
     h.process_quest_rewards(tpl_args, frame)
        cats[#cats+1] = i18n.categories.improper_modifiers
     end
      
      
     -- ------------------------------------------------------------------------
     --  
     -- Infobox handling
     for _, k in ipairs({'broken_upgraded_from_reference', 'duplicate_upgraded_from_reference', 'duplicate_query_area_ids', 'sell_prices_override'}) do
    -- ------------------------------------------------------------------------
        if tpl_args._flags[k] then
    -- Store the infobox so it can be accessed with ease on other pages
            cats[#cats+1] = i18n.categories[k]
    tpl_args.html = tostring(h.make_main_container(tpl_args, frame, 'inline'))
         end
    local container = h.make_main_container(tpl_args, frame, 'infobox')
    end
   
 
    if tpl_args.inventory_icon ~= nil and tpl_args.class_id ~= 'DivinationCard' then
    if tpl_args._flags.has_deprecated_skill_parameters then
         container:tag('span')
        cats[#cats+1] = i18n.categories.deprecated_skill_parameters
            :addClass('images')
            :wikitext(string.format(
                '[[%s|%sx%spx]]',
                tpl_args.inventory_icon,
                cfg.image_size_full * tpl_args.size_x,
                cfg.image_size_full * tpl_args.size_y
            ))
     end
     end
    return cats
end
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
local p = {}
--
-- Template:Item
--
function p.item(frame)
    local t = os.clock() -- Start time (for logging)
      
      
     --
     local tpl_args = getArgs(frame, {
     -- Secondary infobox
        parentFirst = true
     --
     })
     frame = m_util.misc.get_frame(frame)
      
      
     tpl_args.extra_infobox = mw.html.create('span')
     tpl_args._flags = {}
         :attr( 'class', 'item-box -' .. tpl_args.frame_type)
    tpl_args._base_item_args = {}
          
    tpl_args._base_implicit_mods = {}
     h.add_to_container_from_map(tpl_args, frame, tpl_args.extra_infobox, c.extra_display_groups)
    tpl_args._defined_implicit_mods = {}
    tpl_args._mods = {}
    for _, k in ipairs({'', '_random'}) do
         for _, prefix in ipairs({'', '_implicit', '_explicit'}) do
            tpl_args[k .. prefix .. '_stats'] = {}
         end
     end
    tpl_args._subobjects = {}
    tpl_args._properties = {}
    tpl_args._errors = {}
      
      
     --  
     -- Item configuration
     -- Output
     tpl_args._item_config = s.get_item_config(tpl_args, frame)
     --
 
     h.build_cargo_data(tpl_args, frame, tpl_args._item_config.tables)
      
      
    local infobox = mw.html.create('span')
     -- Using general purpose function to handle release and removal versions
    infobox
     m_util.args.version(tpl_args, {frame=frame, set_properties=true})
        :attr('class', 'infobox-page-container')
        :node(container)
        :node(tpl_args.extra_infobox)
       
     -- skill_screenshot is set in skill module
     if tpl_args.skill_screenshot then
        infobox:wikitext(string.format('<br>[[%s|300px]]', tpl_args.skill_screenshot))
    end
      
      
     local out = tostring(infobox)
     -- Must validate some argument early. It is required for future things
     tpl_args.html_extra = out
     h.process_arguments(tpl_args, frame, tpl_args._item_config.args)
      
      
     -- ------------------------------------------------------------------------
     -- Base item
     -- Category handling
     s.process_base_item(tpl_args, frame)
    -- ------------------------------------------------------------------------
      
      
     local cats = {}
     -- Prophecies are considered currency items, but they have their own schema
     if tpl_args.rarity_id == 'unique' then
     if tpl_args._flags.is_prophecy then
        cats[#cats+1] = string.format(i18n.categories.unique_affix, tpl_args.class)
         h.process_arguments(tpl_args, frame, cfg.prophecy_args)
    elseif tpl_args._flags.is_prophecy then
         cats[#cats+1] = i18n.categories.prophecies
    elseif tpl_args._flags.is_blight_item then
        cats[#cats+1] = i18n.categories.blight_item
    elseif tpl_args.is_talisman then
        cats[#cats+1] = i18n.categories.talismans
    elseif tpl_args.is_essence then
        cats[#cats+1] = i18n.categories.essences
    elseif tpl_args.class_id == 'Map' then
        cats[#cats+1] = string.format('%s %s', tpl_args.map_series, tpl_args.class)
    else
        cats[#cats+1] = tpl_args.class
     end
     end
      
      
     if tpl_args.rarity_id ~= 'normal' or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
     -- Mods
        cats[#cats+1] = i18n.categories.derived_items
    s.process_mods(tpl_args, frame)
     else
 
        cats[#cats+1] = i18n.categories.base_items
    -- Stats
    s.process_stats(tpl_args, frame)
 
     -- Calculate and handle weapon dps
    if cfg.class_groups.weapons.keys[tpl_args.class_id] then
        s.process_weapon_dps(tpl_args, frame)
     end
     end
      
      
     for _, attr in ipairs(m_game.constants.attribute_order) do
     -- Late argument processing
        if tpl_args[attr .. '_percent'] then
    h.process_arguments(tpl_args, frame, tpl_args._item_config.late_args)
            cats[#cats+1] = string.format('%s %s', m_game.constants.attributes[attr].long_upper, tpl_args.class)
        end
    end
      
      
     local affix
     -- Handle upgrade from restrictions/info
     if tpl_args.class_id == 'Active Skill Gem' or tpl_args.class_id == 'Support Skill Gem' then
     h.process_upgraded_from(tpl_args, frame)
        affix = i18n.categories.gem_tag_affix
    end
    if affix ~= nil then
        for _, tag in ipairs(tpl_args.gem_tags) do
            cats[#cats+1] = string.format(affix, tag)
        end
    end
 
    if #tpl_args._defined_implicit_mods > 0 and tpl_args.rarity_id ~= 'normal' then
        cats[#cats+1] = i18n.categories.implicit_modifier_override
    end
      
      
     if #tpl_args.alternate_art_inventory_icons > 0 then
     -- Quest reward info
        cats[#cats+1] = i18n.categories.alternate_artwork
    s.process_quest_rewards(tpl_args, frame)
    end
      
      
     -- TODO: add maintenance categories
     -- ------------------------------------------------------------------------
    -- Infobox handling
    -- ------------------------------------------------------------------------
    -- Store the infobox so it can be accessed with ease on other pages
    tpl_args.html = tostring(h.make_main_container(tpl_args, frame, 'inline'))
    local container = h.make_main_container(tpl_args, frame, 'infobox')
      
      
     if tpl_args.release_version == nil then
     if tpl_args.inventory_icon ~= nil and tpl_args.class_id ~= 'DivinationCard' then
         cats[#cats+1] = i18n.categories.missing_release_version
         container:tag('span')
    end
            :addClass('images')
   
            :wikitext(string.format(
    if tpl_args._flags.text_modifier and not tpl_args.suppress_improper_modifiers_category then
                '[[%s|%sx%spx]]',
        cats[#cats+1] = i18n.categories.improper_modifiers
                tpl_args.inventory_icon,
                cfg.image_size_full * tpl_args.size_x,
                cfg.image_size_full * tpl_args.size_y
            ))
     end
     end
   
    --
    -- Secondary infobox
    --
   
    tpl_args.extra_infobox = mw.html.create('span')
        :attr( 'class', 'item-box -' .. tpl_args.frame_type)
       
    h.add_to_container_from_map(tpl_args, frame, tpl_args.extra_infobox, c.extra_display_groups)
      
      
     --  
     --  
     for _, k in ipairs({'broken_upgraded_from_reference', 'duplicate_upgraded_from_reference', 'duplicate_query_area_ids', 'sell_prices_override'}) do
     -- Output
         if tpl_args._flags[k] then
    --
            cats[#cats+1] = i18n.categories[k]
   
         end
    local infobox = mw.html.create('span')
     end
    infobox
 
        :attr('class', 'infobox-page-container')
     if tpl_args._flags.has_deprecated_skill_parameters then
         :node(container)
         cats[#cats+1] = i18n.categories.deprecated_skill_parameters
        :node(tpl_args.extra_infobox)
          
     -- skill_screenshot is set in skill module
     if tpl_args.skill_screenshot then
         infobox:wikitext(string.format('<br>[[%s|300px]]', tpl_args.skill_screenshot))
     end
     end
   
    local out = tostring(infobox)
    tpl_args.html_extra = out
   
    cats = s.get_categories(tpl_args, frame)
      
      
     out = out .. m_util.misc.add_category(cats, {ignore_blacklist=tpl_args.debug})
     out = out .. m_util.misc.add_category(cats, {ignore_blacklist=tpl_args.debug})
Line 2,812: Line 2,812:
     -- Store cargo data
     -- Store cargo data
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
     -- Map argument values for cargo storage
     -- Map argument values for cargo storage
     for _, table_name in ipairs(c.item_classes[tpl_args.class_id].tables) do
     for _, table_name in ipairs(tpl_args._item_config.tables) do
         tpl_args._subobjects[table_name] = {
         tpl_args._subobjects[table_name] = {
             _table = table_name,
             _table = table_name,
         }
         }
     end
     end
     if tpl_args.debug then
     if tpl_args.debug then
         mw.log('Start logging cargo_data.')
         mw.log('Start logging tpl_args.')
         mw.logObject(cargo_data)
         mw.logObject(tpl_args)
         mw.log('Stop logging cargo_data.')
         mw.log('Stop logging tpl_args.')
    end
         mw.log('Start logging core.map.')
    if tpl_args.debug then
         mw.logObject(core.map)
         mw.log('Start logging core.map.')
         mw.log('Stop logging core.map.')
         mw.logObject(core.map)
     end
         mw.log('Stop logging core.map.')
     for k, v in pairs(tpl_args) do
     end
         local data = core.map[k]
     for k, data in pairs(cargo_data) do
        if data ~= nil then
         if tpl_args[k] ~= nil then
             if data.table ~= nil and data.field ~= nil then
             if data.table == nil then
                 if data.type == 'Integer' then
                 error(string.format('Missing table for field "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.field, k, mw.dumpObject(tpl_args[k]), mw.dumpObject(data)))
                    v = tonumber(string.format("%.0f", v))
             elseif data.field == nil then
                    if v ~= tpl_args[k] then
                 error(string.format('Missing field for table "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.table, k, mw.dumpObject(tpl_args[k]), mw.dumpObject(data)))
                        mw.logObject(string.format('Float value "%s" for integer field "%s.%s"', tpl_args[k], data.table, data.field))
             else
                    end
                local value = tpl_args[k]
                end
                if data.type == 'Integer' then
                tpl_args._subobjects[data.table][data.field] = v
                    value = tonumber(string.format("%.0f", value))
             elseif data.table == nil and data.field ~= nil then
                    if value ~= tpl_args[k] then
                 error(string.format('Missing table for field "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.field, k, mw.dumpObject(v), mw.dumpObject(data)))
                        mw.logObject(string.format('Float value "%s" for integer field "%s.%s"', tpl_args[k], data.table, data.field))
             elseif data.table ~= nil and data.field == nil then
                    end
                error(string.format('Missing field for table "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.table, k, mw.dumpObject(v), mw.dumpObject(data)))
                end
                tpl_args._subobjects[data.table][data.field] = value
             end
             end
         end
         end
     end
     end
    --[[for k, v in pairs(tpl_args) do
        local data = core.map[k]
        if data ~= nil then
            if data.table ~= nil and data.field ~= nil then
                if data.type == 'Integer' then
                    v = tonumber(string.format("%.0f", v))
                    if v ~= tpl_args[k] then
                        mw.logObject(string.format('Float value "%s" for integer field "%s.%s"', tpl_args[k], data.table, data.field))
                    end
                end
                tpl_args._subobjects[data.table][data.field] = v
            elseif data.table == nil and data.field ~= nil then
                error(string.format('Missing table for field "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.field, k, mw.dumpObject(v), mw.dumpObject(data)))
            elseif data.table ~= nil and data.field == nil then
                error(string.format('Missing field for table "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.table, k, mw.dumpObject(v), mw.dumpObject(data)))
            end
        end
    end--]]
      
      
     -- Don't actually save data in testing mode
     -- Don't actually save data in testing mode

Revision as of 08:24, 14 July 2021

This is the module sandbox page for Module:Item2 (diff).

See also the companion subpage for test cases (run).

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


This module is used on 13,000+ pages.

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

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

The module implements {{item}}.

Overview

This module is responsible for creating item boxes, various item lists, item links and other item-related tasks. In the process a lot of the input data is verified and also added as semantic property to pages; as such, any templates deriving from this module should not be used on user pages other then for temporary testing purposes.

This template is also backed by an export script in PyPoE which can be used to export item data from the game files which then can be used on the wiki. Use the export when possible.


-------------------------------------------------------------------------------
-- 
--                                Module:Item2
-- 
-- This module implements Template:Item and Template:Itembox
-------------------------------------------------------------------------------

-- ----------------------------------------------------------------------------
-- TODO
-- ----------------------------------------------------------------------------
-- Items
-- -----
-- Check if abyss jewels actually have radius modifiers
-- 
-- Aggregate ids from skill id from modifiers
-- 
--
-- unique items:
--  3D art
--  supporter attribution
--
-- Maps: 
--  Area level can be retrieved eventually
--
-- Essence: 
--  type column
--  monster modifier info
--
-- random modifier
-- -> sell price must consider random mods
--
-- ----------
-- Item class
-- ----------
--
-- remove the ul if name_list is not provided

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

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

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

-- Lazy loading
local f_skill -- require('Module:Skill')._skill
local f_item_link -- require('Module:Item link').item_link
local f_query_area_info -- require('Module:Area').query_area_info
local f_process_upgraded_from -- require('Module:Item2/upgrade').process_upgraded_from
local f_build_cargo_data -- require('Module:Item2/cargo').build_cargo_data

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

local i18n = cfg.i18n

local core = use_sandbox and require('Module:Item2/core/sandbox') or require('Module:Item2/core')

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

local h = {}

-- Lazy loading for Module:Skill
function h.skill(tpl_args, frame)
    if not f_skill then
        f_skill = use_sandbox and require('Module:Skill/sandbox')._skill or require('Module:Skill')._skill
    end
    return f_skill(args)
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:Item link
function h.query_area_info(args)
    if not f_query_area_info then
        f_query_area_info = require('Module:Area').query_area_info
    end
    return f_query_area_info(args)
end

-- Lazy loading for Module:Item2/cargo
function h.build_cargo_data(tpl_args, frame, tables)
    if not f_build_cargo_data then
        f_build_cargo_data = use_sandbox and require('Module:Item2/cargo/sandbox').build_cargo_data or require('Module:Item2/cargo').build_cargo_data
    end
    return f_build_cargo_data(tpl_args, frame, tables)
end

-- Lazy loading for Module:Item2/upgrade
function h.process_upgraded_from(tpl_args, frame)
    if not f_process_upgraded_from then
        f_process_upgraded_from = use_sandbox and require('Module:Item2/upgrade/sandbox').process_upgraded_from or require('Module:Item2/upgrade').process_upgraded_from
    end
    return f_process_upgraded_from(tpl_args, frame)
end

function h.validate_mod(tpl_args, frame, args)
    -- args:
    --  key   - implict or explicit
    --  i
    --  value
    local prefix = args.key .. args.i
    local value = tpl_args[prefix]
    local is_implicit = args.key == 'implicit'
    local out = {
        result=nil,
        -- commited to cargo at a later point
        id=nil,
        stat_text=nil,
        is_implicit=is_implicit,
        is_random=nil,
    }
    
    local mods_table = is_implicit and tpl_args._defined_implicit_mods or tpl_args._mods
    if value ~= nil then
        out.id = value
        out.stat_text = tpl_args[prefix .. '_text']
        out.is_random = false
        table.insert(mods_table, out)
        
        return true
    elseif tpl_args[prefix .. '_random_list'] then
        tpl_args._flags.random_mods = true
        value = m_util.string.split(tpl_args[prefix ..  '_random_list'], ',%s*')
        for _, mod_id in ipairs(value) do
            table.insert(mods_table, {
                result = nil,
                id = mod_id,
                stat_text = tpl_args[prefix .. '_text'] or string.format(i18n.tooltips.random_mod, args.i),
                is_implicit = is_implicit,
                is_random = true,
            })
        end
        
        return true
    elseif tpl_args[prefix .. '_text'] then
        value = tpl_args[prefix .. '_text']
        tpl_args._flags.text_modifier = true
        out.result = value
        out.stat_text = value
        out.is_random = false
        table.insert(mods_table, out)
        
        return true
    end
    
    return false
end

function h.handle_range_args(tpl_args, frame, argument_key, field, value, fmt_options)
    fmt_options = mw.clone(fmt_options)
    fmt_options.return_color = true
    local html, colour = m_util.html.format_value(tpl_args, frame, value, fmt_options)
    tpl_args[argument_key .. '_html'] = html
    tpl_args[field .. '_html'] = html
    tpl_args[field .. '_range_colour'] = colour
    
    fmt_options = mw.clone(fmt_options)
    fmt_options.no_color = true
    tpl_args[field .. '_range_text'] = m_util.html.format_value(tpl_args, frame, value, fmt_options)
end

h.stat = {}
function h.stat.add(value, stat_cached) 
    value.min = value.min + stat_cached.min
    value.max = value.max + stat_cached.max
end

function h.stat.more(value, stat_cached)
    value.min = value.min * (1 + stat_cached.min / 100)
    value.max = value.max * (1 + stat_cached.max / 100)
end

function h.stat.more_inverse(value, stat_cached)
    value.min = value.min / (1 + stat_cached.min / 100)
    value.max = value.max / (1 + stat_cached.max / 100)
end

--
-- Processing
--

function h.process_arguments(tpl_args, frame, params)
    tpl_args._processed_args = tpl_args._processed_args or {}
    for _, k in ipairs(params) do
        local data = core.map[k]
        if data == nil then
            if tpl_args.debug then
                error(string.format(i18n.debug.invalid_argument_key, k))
            end
        else
            if data.inherit ~= false then
                table.insert(tpl_args._base_item_args, k)
            end
            if data.func ~= nil then
                tpl_args[k] = data.func(tpl_args, frame, tpl_args[k])
            end
            if tpl_args[k] == nil then
                if tpl_args.class_id and tpl_args._item_config.defaults ~= nil and tpl_args._item_config.defaults[k] ~= nil then
                    -- Item defaults
                    tpl_args[k] = tpl_args._item_config.defaults[k]
                elseif data.default ~= nil then
                    -- Generic defaults
                    if type(data.default) == 'function' then
                        tpl_args[k] = data.default(tpl_args, frame)
                    else
                        tpl_args[k] = data.default
                    end
                end
            end
        end
        table.insert(tpl_args._processed_args, k)
    end
end

--
-- Display
--

function h.strip_random_stats(tpl_args, frame, stat_text, container_type)
    if tpl_args._flags.random_mods and container_type == 'inline' then
        repeat
            local text = string.match(stat_text, '<th class="mw%-customtoggle%-31">(.+)</th>')
            if text ~= nil then
                stat_text = string.gsub(stat_text, '<table class="random%-modifier%-stats.+</table>', text, 1)
            end
        until text == nil
    end
    return stat_text
end

function h.add_to_container_from_map(tpl_args, frame, container, mapping, container_type)
    local statcont = mw.html.create('span')
    statcont
        :attr('class', 'item-stats')
        :done()
    local count = 0 -- Number of groups in container
    for _, group in ipairs(mapping) do
        local lines = {}
        if group.func == nil then
            for _, line in ipairs(group) do
                local show = true
                if container_type == 'inline' and line.inline == false then -- TODO: This is not used currently. Need to address what is hidden in inline view.
                    show = false
                elseif line.show == false then
                    show = false
                elseif type(line.show) == 'function' then
                    show = line.show(tpl_args, frame, container_type)
                end
                if show then
                    lines[#lines+1] = line.func(tpl_args, frame, container_type)
                end
            end
        else
            lines = group.func(tpl_args, frame, container_type)
        end
        if #lines > 0 then
            count = count + 1
            local heading = ''
            if group.heading == nil then
            elseif type(group.heading) == 'function' then
                heading = group.heading()
            else
                heading = string.format('<em class="header">%s</em><br>', group.heading)
            end
            statcont
                :tag('span')
                :attr('class', 'group ' .. (group.class or ''))
                :wikitext(heading .. table.concat(lines, '<br>'))
                :done()
        end
    end

     -- Add groups to container
    if count > 0 then
        container:node(statcont)
    end
end

function h.make_main_container(tpl_args, frame, container_type)
    local container = mw.html.create('span')
        :attr('class', 'item-box -' .. tpl_args.frame_type)
    
    if tpl_args.class_id == 'DivinationCard' then
        container
            :tag('span')
                :attr('class', 'divicard-wrapper')
                :tag('span')
                    :attr('class', 'divicard-art')
                    :wikitext( '[[' .. tpl_args.card_art .. '|link=|alt=]]' )
                    :done()
                :tag('span')
                    :attr('class', 'divicard-frame')
                    :wikitext( '[[File:Divination card frame.png|link=|alt=]]' )
                    :done()
                :tag('span')
                    :attr('class', 'divicard-header')
                    :wikitext(tpl_args.name)
                    :done()
                :tag('span')
                    :attr('class', 'divicard-stack')
                    :wikitext(tpl_args.stack_size)
                    :done()
                :tag('span')
                    :attr('class', 'divicard-reward')
                    :tag('span')
                        :wikitext(tpl_args.description)
                        :done()
                    :done()
                :tag('span')
                    :attr('class', 'divicard-flavour text-color -flavour')
                    :tag('span')
                        :wikitext(tpl_args.flavour_text)
                        :done()
                    :done()
                :done()
        --TODO Extras?
    else
        local header_css
        local line_type
        if tpl_args.base_item and tpl_args.rarity_id ~= 'normal' then
            line_type = 'double'
        else
            line_type = 'single'
        end
        
        local name_line = tpl_args.name
        if tpl_args.base_item and not tpl_args._flags.is_prophecy then
            name_line = name_line .. '<br>' .. tpl_args.base_item
        end

        -- Symbols - These are displayed in the item box header to indicate certain flags and/or item influences
        local symbols
        local influences = tpl_args.influences or {}
        if tpl_args.is_replica then
            symbols = {'replica', 'replica'}
            if #influences > 0 then
                symbols[2] = influences[1]
            end
        elseif #influences > 0 then
            symbols = {influences[1], influences[1]}
            if #influences > 1 then
                symbols[2] = influences[2]
            end
        elseif tpl_args.is_fractured then
            symbols = {'fractured', 'fractured'}
        elseif tpl_args.is_synthesised then
            symbols = {'synthesised', 'synthesised'}
        elseif tpl_args.is_veiled then
            symbols = {'veiled', 'veiled'}
        end
        
        container
            :tag('span')
                :attr( 'class', 'header -' .. line_type )
                :tag('span')
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[1] or '') )
                    :done()
                :wikitext(name_line)
                :tag('span')
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[2] or '') )
                    :done()
            :done()
            
        h.add_to_container_from_map(tpl_args, frame, container, c.item_infobox_groups, container_type)
    end
    
    if tpl_args.skill_icon ~= nil then
        container:wikitext(string.format('[[%s]]', tpl_args.skill_icon))
    end
    
    return container
end

-- 
-- Factory
-- 

h.factory = {}

function h.factory.args_present(...)
    local args = {...}
    return function (tpl_args)
        for _, k in ipairs(args) do
            if tpl_args[k] == nil then
                return false
            elseif type(tpl_args[k]) == 'table' and #tpl_args[k] == 0 then
                return false
            end
        end
        return true
    end
end

function h.factory.display_raw_value(key)
    return function(tpl_args, frame)
        return tpl_args[key]
    end
end

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

-- ----------------------------------------------------------------------------
-- Additional configuration
-- ----------------------------------------------------------------------------

local c = {}

-- helper to loop over the range variables easier
c.range_map = {
    min = {
        var = '_range_minimum',
    },
    max = {
        var = '_range_maximum',
    },
    avg = {
        var = '_range_average',
    },
}

--
-- Contents here are meant to resemble the ingame infobox of items
--
c.item_infobox_groups = {
    -- [n]: 
    --  class: Additional css class added to group tag
    --  heading: Group heading text (used for extras)
    --  lines: 
    --   [n]: 
    --    show: Show line if this function returns true; Always show if boolean true. Default: Always show
    --    func: Function that returns line text
    {
        -- Cosmetic item type
        {
            show = h.factory.args_present('cosmetic_type'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'cosmetic_type',
                        fmt = '%s',
                        color = 'default'
                    },
                },
            },
        },
        -- Weapon type
        {
            show = function (tpl_args, frame)
                if tpl_args.class_id == nil then 
                    return false 
                end
                return cfg.class_groups.weapons.keys[tpl_args.class_id] ~= nil
            end,
            func = function (tpl_args, frame)
                local v = i18n.item_class_map[tpl_args.class_id]
                return m_util.html.format_value(tpl_args, frame, {min=v, max=v}, {color = 'default'})
            end,
        },
        -- Hideout item type
        {
            show = function (tpl_args, frame)
                return tpl_args.class_id == 'HideoutDoodad'
            end,
            func = function (tpl_args, frame)
                return i18n.item_class_map[tpl_args.class_id]
            end,
        },
        {
            show = h.factory.args_present('gem_tags'),
            func = function (tpl_args, frame)
                local out = {}
                for i, tag in ipairs(tpl_args.gem_tags) do
                    out[#out+1] = string.format(i18n.gem_tag_category, tag, tag)
                end
                return table.concat(out, ', ')
            end,
        },
        {
            show = h.factory.args_present('support_gem_letter_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'support_gem_letter_html',
                    },
                },
                fmt = i18n.tooltips.support_icon,
            },
        },
        {
            show = function (tpl_args, frame)
                return tpl_args.skill_levels and cfg.class_groups.gems.keys[tpl_args.class_id]
            end,
            func = function (tpl_args, frame)
                local value = {
                    base = 1,
                    min = 1,
                    max = tpl_args.max_level,
                }
                local options = {
                    color = 'value',
                }
                return string.format(
                    i18n.tooltips.level,
                    m_util.html.format_value(tpl_args, frame, value, options)
                )
            end,
        },
        {
            show = function (tpl_args, frame)
                return tpl_args.skill_costs
            end,
            func = function (tpl_args, frame)
                local parts = {}
                if not tpl_args.skill_costs.has_spending_cost then
                    -- Try falling back to deprecated parameters
                    if not tpl_args.has_reservation_mana_cost then
                        parts[1] = {
                            key = 'mana_cost',
                            fmt = '%i',
                            color = false,
                            inline = string.format('%%s %s', m_game.constants.skill.cost_types.mana.long_upper),
                            inline_color = 'value',
                        }
                    end
                else
                    for i=1, #tpl_args.skill_costs do
                        if not tpl_args.skill_costs[i].is_reservation then -- Only get spending costs
                            local cost_type = tpl_args.skill_costs[i].type
                            parts[#parts+1] = {
                                key = {'costs', i, 'amount'},
                                fmt = '%i',
                                color = false,
                                inline = string.format(
                                    '%%s%s %s',
                                    string.find(cost_type, 'percent', 1, true) and '%%' or '',
                                    m_game.constants.skill.cost_types[cost_type].long_upper
                                ),
                                inline_color = 'value',
                            }
                        end
                    end
                end
                return core.factory.infobox_line{
                    type = 'gem',
                    parts = parts,
                    sep = ', ',
                    fmt = i18n.tooltips.cost,
                }(tpl_args, frame)
            end,
        },
        {
            show = function (tpl_args, frame)
                return tpl_args.skill_costs
            end,
            func = function (tpl_args, frame)
                if not tpl_args.skill_costs then
                    return
                end
                local parts = {}
                if not tpl_args.skill_costs.has_reservation_cost then
                    -- Try falling back to deprecated parameters
                    if tpl_args.has_reservation_mana_cost then
                        parts[1] = {
                            key = 'mana_cost',
                            fmt = '%i',
                            color = false,
                            inline = string.format(
                                '%%s%s %s',
                                tpl_args.has_percentage_mana_cost and '%%' or '',
                                m_game.constants.skill.cost_types.mana.long_upper
                            ),
                            inline_color = 'value',
                        }
                    end
                else
                    for i=1, #tpl_args.skill_costs do
                        if tpl_args.skill_costs[i].is_reservation then -- Only get reservation costs
                            local cost_type = tpl_args.skill_costs[i].type
                            parts[#parts+1] = {
                                key = {'costs', i, 'amount'},
                                fmt = '%i',
                                color = false,
                                inline = string.format(
                                    '%%s%s %s',
                                    string.find(cost_type, 'percent', 1, true) and '%%' or '',
                                    m_game.constants.skill.cost_types[cost_type].long_upper
                                ),
                                inline_color = 'value',
                            }
                        end
                    end
                end
                return core.factory.infobox_line{
                    type = 'gem',
                    parts = parts,
                    sep = ', ',
                    fmt = i18n.tooltips.reservation,
                }(tpl_args, frame)
            end,
        },
        {
            show = true, -- TODO: Show only if has mana_multiplier
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'mana_multiplier',
                        hide_default = 100,
                        fmt = '%i',
                        color = false,
                        inline = '%s%%',
                        inline_color = 'value',
                    },
                },
                fmt = i18n.tooltips.mana_multiplier,
            },
        },
        {
            show = true, -- TODO: Show only if has cooldown
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'cooldown', 
                        hide_default = 0,
                        fmt = '%.2f ' .. m_game.units.seconds.short_lower,
                    },
                },
                fmt = i18n.tooltips.cooldown_time,
            },
        },
        {
            -- TODO: Combine with cooldown. Multi-use non-vaal skills display uses together with cooldown time. E.g., Cooldown Time: 8.00 sec (3 uses)
            show = true,
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'stored_uses',
                        hide_default = 0,
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.stored_uses,
            },
        },
        {
            show = true, -- TODO: Show only if has vaal_souls_requirement
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'vaal_souls_requirement',
                        hide_default = 0,
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.vaal_souls_per_use,
            },
        },
        {
            show = true, -- TODO: Show only if has vaal_stored_uses
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'vaal_stored_uses',
                        hide_default = 0,
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.stored_uses, -- TODO: Singular or plural based on number
            },
        },
        {
            show = true, -- TODO: Show only if has vaal_soul_gain_prevention_time
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'vaal_soul_gain_prevention_time',
                        hide_default = 0,
                        -- Technically it rounds to nearest, but it is given in milliseconds in the data, 
                        fmt = '%i ' .. m_game.units.seconds.short_lower,
                    },
                },
                fmt = i18n.tooltips.vaal_soul_gain_prevention_time,
            },
        },
        {
            show = function (tpl_args, frame)
                return tpl_args.cast_time and tpl_args.gem_tags and (m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.spell.tag) or m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.warcry.tag))
            end,
            func = function (tpl_args, frame)
                return core.factory.infobox_line{
                    parts = {
                        {
                            key = 'cast_time',
                            fmt = function (tpl_args, frame, value)
                                if value.min == 0 then
                                    return i18n.tooltips.instant_cast_time
                                end
                                return '%.2f ' .. m_game.units.seconds.short_lower
                            end,
                        },
                    },
                    fmt = m_util.table.contains(tpl_args.gem_tags, m_game.constants.item.gem_tags.spell.tag) and i18n.tooltips.cast_time or i18n.tooltips.use_time,
                }(tpl_args, frame)
            end,
        },
        {
            show = true, -- TODO: Show only if has critical_strike_chance
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'critical_strike_chance',
                        hide_default = 0,
                        fmt = '%.2f%%',
                    },
                },
                fmt = i18n.tooltips.critical_strike_chance,
            },
        },
        {
            show = true, -- TODO: Show only if has attack_speed_multiplier
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'attack_speed_multiplier',
                        hide_default = 100,
                        fmt = '%i',
                        color = false,
                        inline = '%s%% ' .. i18n.tooltips.of_base_stat,
                        inline_color = 'value',
                    },
                },
                fmt = i18n.tooltips.attack_speed_multiplier,
            },
        },
        {
            show = true, -- TODO: Show only if has damage_multiplier
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'damage_multiplier',
                        hide_default = 100,
                        fmt = '%i',
                        color = false,
                        inline = '%s%% ' .. i18n.tooltips.of_base_stat,
                        inline_color = 'value',
                    },
                },
                fmt = i18n.tooltips.damage_multiplier,
            },
        },
        {
            show = true, -- TODO: Show only if has damage_effectiveness
            func = core.factory.infobox_line{
                type = 'gem',
                parts = {
                    {
                        key = 'damage_effectiveness',
                        hide_default = 100,
                        fmt = '%i',
                        color = false,
                        inline = '%s%%',
                        inline_color = 'value',
                    },
                },
                fmt = i18n.tooltips.damage_effectiveness,
            },
        },
        {
            show = h.factory.args_present('projectile_speed'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'projectile_speed',
                    },
                },
                fmt = i18n.tooltips.projectile_speed,
            },
        },
        {
            show = h.factory.args_present('radius'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'radius',
                        func = h.factory.descriptor_value{key='radius_description'},
                    },
                    {
                        key = 'radius_secondary',
                        func = h.factory.descriptor_value{key='radius_secondary_description'},
                    },
                    {
                        key = 'radius_tertiary',
                        func = h.factory.descriptor_value{key='radius_tertiary_description'},
                    },
                },
                sep = ' / ',
                fmt = i18n.tooltips.radius,
            },
        },
        -- Quality is before item stats, but after gem stuff and item class
        {
            show = h.factory.args_present('quality'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'quality',
                        fmt = '+%i%%',
                        color = 'mod',
                        hide_default = 0,
                    },
                },
                fmt = i18n.tooltips.quality,
            },
        },
        -- Weapon only
        {
            show = h.factory.args_present('physical_damage_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'physical_damage_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.physical_damage,
            },       
        },
        {
            show = true, -- Elemental Damage
            func = function (tpl_args, frame)
                local keys = {'fire_damage_html', 'cold_damage_html', 'lightning_damage_html'}
                local elements = {}
                for _, key in ipairs(keys) do
                    if tpl_args[key] then
                        elements[#elements+1] = tpl_args[key]
                    end
                end
                local text = table.concat(elements, ', ') -- returns empty string if elements is empty
                if text ~= '' then
                    return string.format(i18n.tooltips.elemental_damage, text)
                end
                return
            end,      
        },
        {
            show = h.factory.args_present('chaos_damage_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'chaos_damage_html',
                        fmt = '%s',
                        color = false, -- html already has color
                    },
                },
                fmt = i18n.tooltips.chaos_damage,
            },       
        },
        {
            show = h.factory.args_present('critical_strike_chance_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'critical_strike_chance_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.critical_strike_chance,
            },
        },
        {
            show = h.factory.args_present('attack_speed_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'attack_speed_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.attacks_per_second,
            },
        },
        {
            show = h.factory.args_present('weapon_range_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'weapon_range_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.weapon_range,
            },
        },
        -- Map only
        {
            show = h.factory.args_present('map_area_level'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'map_area_level',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.map_level,
            },
        },
        {
            show = h.factory.args_present('map_tier'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'map_tier',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.map_tier,
            },
        },
        {
            show = function (tpl_args, frame)
                return tpl_args.map_guild_character ~= nil and tpl_args.rarity_id == 'normal'
            end,
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'map_guild_character',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.map_guild_character,
            },
        },
        {
            show = function (tpl_args, frame)
                return tpl_args.unique_map_guild_character ~= nil and tpl_args.rarity_id == 'unique'
            end,
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'unique_map_guild_character',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.map_guild_character,
            },
        },
        {
            show = true,
            func = core.factory.infobox_line{
                type = 'stat',
                parts = {
                    {
                        key = 'map_item_drop_quantity_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                        hide_default = 0,
                    },
                },
                fmt = i18n.tooltips.item_quantity,
            },
        },
        {
            show = true,
            func = core.factory.infobox_line{
                type = 'stat',
                parts = {
                    {
                        key = 'map_item_drop_rarity_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                        hide_default = 0,
                    },
                },
                fmt = i18n.tooltips.item_rarity,
            },
        },
        {
            show = true,
            func = core.factory.infobox_line{
                type = 'stat',
                parts = {
                    {
                        key = 'map_pack_size_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                        hide_default = 0,
                    },
                },
                fmt = i18n.tooltips.monster_pack_size,
            },
        },
        -- Jewel Only
        {
            show = true,
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'item_limit',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.limited_to,
            },
        },
        {
            show = h.factory.args_present('jewel_radius_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'jewel_radius_html',
                        fmt = '%s',
                        color = false, -- html already has color
                    },
                },
                fmt = i18n.tooltips.radius,
            },
        },
        -- Flask only
        {
            show = h.factory.args_present('flask_mana_html', 'flask_duration_html'),
            --func = core.factory.display_flask('flask_mana'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'flask_mana_html',
                        fmt = '%s',
                    },
                    {
                        key = 'flask_duration_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.flask_mana_recovery,
            },
        },
        {
            show = h.factory.args_present('flask_life_html', 'flask_duration_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'flask_life_html',
                        fmt = '%s',
                    },
                    {
                        key = 'flask_duration_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.flask_life_recovery,
            },
        },
        {
            -- don't display for mana/life flasks
            show = function (tpl_args, frame)
                for _, k in ipairs({'flask_life_html', 'flask_mana_html'}) do
                    if tpl_args[k] ~= nil then
                        return false
                    end
                end
                return tpl_args['flask_duration_html'] ~= nil
            end,
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'flask_duration_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.flask_duration,
            },
        },
        {
            show = h.factory.args_present('charges_per_use_html', 'charges_max_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'charges_per_use_html',
                        fmt = '%s',
                    },
                    {
                        key = 'charges_max_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.flask_charges_per_use,
            },
        },
        {
            show = h.factory.args_present('buff_stat_text'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'buff_stat_text',
                        color = 'mod',
                    },
                },
            },
        },
        -- Armor only
        {
            show = h.factory.args_present('block_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'block_html',
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'block',
                    },
                },
                fmt = i18n.tooltips.chance_to_block,
            },
        },
        {
            show = h.factory.args_present('armour_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'armour_html',
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'armour',
                    },
                },
                fmt = i18n.tooltips.armour,
            },
        },
        {
            show = h.factory.args_present('evasion_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'evasion_html',
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'evasion',
                    },
                },
                fmt = i18n.tooltips.evasion,
            },
        },
        {
            show = h.factory.args_present('energy_shield_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'energy_shield_html',
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'energy_shield',
                    },
                },
                fmt = i18n.tooltips.energy_shield,
            },
        },
        {
            show = h.factory.args_present('movement_speed'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'movement_speed',
                        fmt = '%s%%',
                        hide_default = 0,
                    },
                },
                fmt = i18n.tooltips.movement_speed,
            },
        },
        -- Amulet only
        {
            show = h.factory.args_present('talisman_tier'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'talisman_tier',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.talisman_tier,
            },
        },
        -- Misc
        {
            show = h.factory.args_present('stack_size'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'stack_size',
                        fmt = '%i',
                        hide_default = 1,
                    },
                },
                fmt = i18n.tooltips.stack_size,
            },
        },
        -- Essence stuff
        {
            show = h.factory.args_present('essence_level'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'essence_level',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.essence_level,
            },
        },
        -- Blight items
        {
            show = h.factory.args_present('blight_item_tier'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'blight_item_tier',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.blight_item_tier,
            },
        },
        -- Harvest seeds (upper section)
        {
            show = h.factory.args_present('seed_tier'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'seed_tier',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.seed_tier,
            },
        },
        {
            show = h.factory.args_present('seed_tier'),
            func = function (tpl_args, value)
                return i18n.tooltips.seed_monster
            end,
        },
        {
            show = h.factory.args_present('seed_type_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'seed_type_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.seed_lifeforce_gained,
            },
        },
        {
            show = h.factory.args_present('seed_growth_cycles'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'seed_growth_cycles',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.seed_growth_cycles,
            },
        },
        -- Heist
        {
            show = h.factory.args_present('heist_required_npcs'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'heist_required_npcs',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.heist_required_npc,
            },
        },
    },
    -- Requirements
    {
        {
            show = h.factory.args_present('master', 'master_level_requirement'),
            func = function (tpl_args, frame)
                local data
                for i, rowdata in ipairs(m_game.constants.masters) do
                    if tpl_args.master == rowdata.full then
                        data = rowdata
                        break
                    end
                end
                
                return m_util.html.poe_color('default', i18n.tooltips.requires, string.format('[[%s|%s %s]]', data.full, data.short_upper, tpl_args.master_level_requirement))
            end
        },
        -- Instead of item level, show drop level if any
        {
            show = true, -- Requires...
            func = function (tpl_args, frame)
                local parts = {
                    {
                        key = 'required_level_final_html',
                        hide_default = 1,
                        hide_default_key = 'required_level_final',
                        inline = i18n.tooltips.level_inline,
                        inline_color = false,
                    },
                }
                for _, attr in ipairs(m_game.constants.attribute_order) do
                    parts[#parts+1] = {
                        key = string.format('required_%s_html', attr),
                        hide = function (tpl_args, frame, value)
                            local min = m_game.constants.characters.minimum_attributes[m_game.constants.attributes[attr].arg]
                            return value.min <= min and value.max <= min
                        end,
                        hide_key = string.format('required_%s', attr),
                        inline = ', %s ' .. m_game.constants.attributes[attr].short_upper,
                        inline_color = false,
                    }
                end
                local requirements = core.factory.infobox_line{parts = parts}(tpl_args, frame)
                if requirements == nil then -- return early
                    return
                end
                requirements = string.gsub(requirements, '^, ', '')
                return string.format(i18n.tooltips.requires, requirements)
            end,
        },
        {
            show = h.factory.args_present('heist_required_job', 'heist_required_job_level'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'heist_required_job_level',
                        fmt = '%s',
                    },
                    {
                        key = 'heist_required_job',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.heist_required_job,
            },
        },
    },
    -- Gem description
    {
        class = 'tc -gemdesc',
        {
            show = h.factory.args_present('gem_description'),
            func = h.factory.display_raw_value('gem_description'),
        },
    },
    -- Gem Quality Stats
    {
        class = 'tc -mod',
        {
            show = h.factory.args_present('skill_quality'),
            func = function (tpl_args, frame)
                local span = mw.html.create('span')
                span
                    :addClass('quality')
                    
                local span2 = span:tag('span')
                span2
                    :addClass('quality-header')
                    :tag('span')
                        :wikitext(m_util.html.poe_color('default', i18n.tooltips.gem_quality))
                    
                for i, quality_data in ipairs(tpl_args.skill_quality) do
                    local span_inner = span2:tag('span')
                    span_inner
                        :addClass('quality-section-select')
                        :addClass('quality-' .. i)
                        :wikitext(i)
                        :tag('span')
                            :wikitext(i18n.tooltips['gem_quality_' .. i])
                            
                    if i == 1 then
                        span_inner:addClass('quality-selected')
                    end
                end
                
                for i, quality_data in ipairs(tpl_args.skill_quality) do
                    local span_inner = span:tag('span')
                    span_inner
                        :addClass('quality-box')
                        :addClass('quality-' .. i)
                        :wikitext(quality_data.stat_text)
                     
                     if i == 1 then
                        span_inner:addClass('quality-selected')
                     end
                end
                
                return tostring(span)
            end,
        },
    },
    -- Gem Implicit Stats
    {
        class = 'tc -mod',
        {
            show = function (tpl_args, frame)
                return cfg.class_groups.gems.keys[tpl_args.class_id] and tpl_args.stat_text
            end,
            func = function (tpl_args, frame)
                local lines = {}
                lines[#lines+1] = tpl_args.stat_text
                for _, tag in ipairs(tpl_args.gem_tags) do
                    if tag == m_game.constants.item.gem_tags.vaal.tag then
                        lines[#lines+1] = i18n.tooltips.corrupted
                        break
                    end
                end
                return table.concat(lines, '<br>')
            end,
        },
    },
    -- Implicit Stats
    {
        class = 'tc -mod',
        func = function (tpl_args, frame, container_type)
            if tpl_args.implicit_stat_text then
                return {h.strip_random_stats(tpl_args, frame, tpl_args.implicit_stat_text, container_type)}
            else
                return {}
            end
        end,
    },
    -- Stats
    {
        class = 'tc -mod',
        func = function (tpl_args, frame, container_type)
            if tpl_args.explicit_stat_text then
                return {h.strip_random_stats(tpl_args, frame, tpl_args.explicit_stat_text, container_type)}
            else
                return {}
            end
        end,
    },
    -- Experience
    --[[{
        {
            show = h.factory.args_present('experience'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'experience',
                        fmt = '%i',
                    },
                },
            },
        },
    },]]--
    -- Harvest seeds (lower section)
    {
        class = 'tc -mod',
        {
            show = h.factory.args_present('seed_consumed_wild_lifeforce_percentage'),
            func = function (tpl_args, frame)
                if tpl_args.seed_consumed_wild_lifeforce_percentage > 0 then
                    return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_wild_lifeforce_percentage, m_util.html.poe_color('wild', m_game.seed_types.wild))
                end
            end
        },
        {
            show = h.factory.args_present('seed_consumed_vivid_lifeforce_percentage'),
            func = function (tpl_args, frame)
                if tpl_args.seed_consumed_vivid_lifeforce_percentage > 0 then
                    return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_vivid_lifeforce_percentage, m_util.html.poe_color('vivid', m_game.seed_types.vivid))
                end
            end
        },
        {
            show = h.factory.args_present('seed_consumed_primal_lifeforce_percentage'),
            func = function (tpl_args, frame)
                if tpl_args.seed_consumed_primal_lifeforce_percentage > 0 then
                    return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_primal_lifeforce_percentage, m_util.html.poe_color('primal', m_game.seed_types.primal))
                end
            end
        },
        {
            show = h.factory.args_present('seed_required_nearby_seed_tier', 'seed_type_html', 'seed_required_nearby_seed_amount'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'seed_required_nearby_seed_amount',
                        fmt = '%s',
                    },
                    {
                        key = 'seed_type_html',
                        fmt = '%s',
                    },
                    {
                        key = 'seed_required_nearby_seed_tier',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.seed_required_seeds,
                color = 'mod',
            },
        },
    },
    {
        class = 'tc -crafted',
        {
            show = h.factory.args_present('seed_effect'),
            func = h.factory.display_raw_value('seed_effect'),
        },
    },
    -- Description (currency, doodads)
    {
        class = 'tc -mod',
        {
            show = h.factory.args_present('description'),
            func = h.factory.display_raw_value('description'),
        },
        --[[{
            show = h.factory.args_present('plant_booster_additional_crafting_options'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'plant_booster_additional_crafting_options',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.plant_booster_additional_crafting_options,
            },
        },
        {
            show = h.factory.args_present('plant_booster_extra_chances'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'plant_booster_extra_chances',
                        fmt = '%s%%',
                    },
                },
                fmt = i18n.tooltips.plant_booster_extra_chances,
            },
        },
        {
            show = h.factory.args_present('plant_booster_lifeforce'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'plant_booster_lifeforce',
                        fmt = '%s%%',
                    },
                },
                fmt = i18n.tooltips.plant_booster_lifeforce,
            },
        },]]
    },
    {
        class = 'tc -crafted',
        {
            show = h.factory.args_present('incubator_effect'),
            func = h.factory.display_raw_value('incubator_effect'),
        },
    },
    -- Variations (for doodads)
    {
        class = 'tc -mod',
        {
            show = h.factory.args_present('variation_count'),
            func = function (tpl_args, frame)
                local txt
                if tpl_args.variation_count == 1 then
                    txt = i18n.tooltips.variation_singular
                else
                    txt = i18n.tooltips.variation_plural
                end
                return string.format('%i %s', tpl_args.variation_count, txt)
            end,
        },
    },
    -- Flavour Text
    {
        class = 'tc -flavour',
        {
            show = h.factory.args_present('flavour_text'),
            func = h.factory.display_raw_value('flavour_text'),
        },
    },
    -- Prophecy text
    {
        class = 'tc -value',
        {
            show = h.factory.args_present('prediction_text'),
            func = h.factory.display_raw_value('prediction_text'),
        },
    },
    -- Can not be traded or modified
    {
        class = 'tc -canttradeormodify',
        {
            show = h.factory.args_present('cannot_be_traded_or_modified'),
            func = function (tpl_args, frame)
                if tpl_args.cannot_be_traded_or_modified == true then
                    return i18n.tooltips.cannot_be_traded_or_modified
                end
            end,
        },
    },
    -- Help text
    {
        class = 'tc -help',
        {
            show = h.factory.args_present('help_text'),
            func = h.factory.display_raw_value('help_text'),
        },
    },
    -- Cost (i.e. vendor costs)
    {
        --class = '',
        {
            show = h.factory.args_present('master_favour_cost'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'master_favour_cost',
                        color = 'currency',
                    },
                },
                fmt = i18n.tooltips.favour_cost,
            },
        },
        {
            show = h.factory.args_present('seal_cost'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'seal_cost',
                        fmt = function (tpl_args, frame, value)
                            return '%dx ' .. h.item_link{metadata_id='Metadata/Items/Currency/CurrencySilverCoin', html=''}
                        end,
                        color = 'currency',
                    },
                },
                fmt = i18n.tooltips.seal_cost,
            },
        },
    },
}

--
-- This is meant to show additional information about the item in a separate infobox
--
c.extra_display_groups = {
    {
        heading = i18n.tooltips.extra_info,
        class = '',
        {
            show = h.factory.args_present('atlas_connections'),
            func = function (tpl_args, frame)
                local fields = {
                    [false] = {
                        value = '✗',
                        sort = 0,
                        class = 'table-cell-xmark',
                    },
                    [true] = {
                        value = '✓',
                        sort = 1,
                        class = 'table-cell-checkmark',
                    },
                }
                
                local tbl = mw.html.create('table')
                tbl
                    :attr('class', 'wikitable')
                    :attr('style', 'width:100%;')
                    :tag('tr')
                        :tag('th')
                            :attr('colspan', 6)
                            :attr('style', 'text-decoration: underline;')
                            :wikitext(i18n.tooltips.header_overall)
                            :done()
                        :done()
                    :tag('tr')
                        :tag('th')
                            :wikitext(i18n.tooltips.header_upgrades)
                            :done()
                        :tag('th')
                            :wikitext(0)
                            :done()
                        :tag('th')
                            :wikitext(1)
                            :done()
                        :tag('th')
                            :wikitext(2)
                            :done()
                        :tag('th')
                            :wikitext(3)
                            :done()
                        :tag('th')
                            :wikitext(4)
                            :done()
                        :done()
                    
                for _, vtype in ipairs({'tier', 'level'}) do
                    local tr = tbl:tag('tr')
                    tr
                        :tag('th')
                            :wikitext(i18n.tooltips['header_map_' .. vtype])
                            :done()
                    
                    for i=0,4 do
                        local value = tpl_args['atlas_map_tier' .. i]
                        if value == 0 then
                            value = fields[false].value
                        elseif vtype == 'level' then
                            value = value + 67
                        end
                        tr
                            :tag('td')
                                :wikitext(value)
                                :done()
                    end
                    tr:done()
                end
                
                tbl
                    :tag('tr')
                        :tag('th')
                            :attr('colspan', 6)
                            :attr('style', 'text-decoration: underline;')
                            :wikitext(i18n.tooltips.header_connections)
                            :done()
                        :done()
                
                -- sort alphabetically
                local sorted = {}
                for key, value in pairs(tpl_args.atlas_connections) do
                    sorted[#sorted+1] = key
                end
                table.sort(sorted)
                
                for _, key in ipairs(sorted) do
                    local tr = tbl:tag('tr')
                    tr
                        :tag('th')
                            :wikitext(key)
                            :done()
                    
                    for i=0,4 do
                        local field = fields[tpl_args.atlas_connections[key]['region' .. i]]
                        tr
                            :tag('td')
                                :attr('data-sort-value', field.sort)
                                :attr('class', field.class)
                                :wikitext(field.value)
                                :done()
                    end
                    tr:done()
                end
                
                return tostring(tbl)
            end
        },
    },
    -- Drop info
    {
        heading = i18n.tooltips.drop_restrictions,
        class = '',
        {
            show = h.factory.args_present('drop_enabled'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'drop_level',
                        fmt = '%i',
                        
                    },
                    {
                        key = 'drop_level_maximum',
                        fmt = '%i',
                        hide_default = 100,
                    },
                },
                sep = ' / ',
                fmt = i18n.tooltips.level,
            },
        },
        {
            show = function (tpl_args, frame)
                if tpl_args.drop_enabled == false then
                    return true
                end
                return false
            end,
            func = function (tpl_args, frame)
                local span = mw.html.create('span')
                span
                    :attr('class', 'infobox-disabled-drop')
                    :wikitext(i18n.tooltips.drop_disabled)
                    :done()
                return tostring(span)
            end,
        },
        {
            show = function (tpl_args, frame)
                if tpl_args.is_drop_restricted == true and tpl_args.drop_enabled ~= false then
                    return true
                end
                return false
            end,
            func = function (tpl_args, frame)
                local span = mw.html.create('span')
                span
                    :attr('class', 'infobox-restricted-drop')
                    :wikitext(i18n.tooltips.drop_restricted)
                    :done()
                return tostring(span)
            end,
        },
        {
            show = h.factory.args_present('drop_leagues'),
            func = function (tpl_args, frame)
                return string.format(i18n.tooltips.league_restriction, m_util.html.poe_color('value', table.concat(tpl_args.drop_leagues, ', ')))
            end
        },
        {
            show = h.factory.args_present('drop_areas_html'),
            func = h.factory.display_raw_value('drop_areas_html'),
        },
        {
            show = h.factory.args_present('drop_text'),
            func = h.factory.display_raw_value('drop_text'),
        },
    },
    {
        heading = i18n.tooltips.purchase_costs,
        {
            show = function (tpl_args, frame)
                for rarity, data in pairs(tpl_args.purchase_costs) do
                    if #data > 0 then
                        return true
                    end
                end
                return false
            end,
            func = function (tpl_args, frame)
                local tbl = mw.html.create('table')
                tbl
                    --:attr('class', 'wikitable')
                    :attr('style', 'width: 100%; margin-top: 0px;')
                    
                for _, rarity_id in ipairs(m_game.constants.rarity_order) do
                    local data = tpl_args.purchase_costs[rarity_id]
                    if #data > 0 then
                        local tr = tbl:tag('tr')
                        tr
                            :tag('td')
                                :wikitext(m_game.constants.rarities[rarity_id].long_upper)
                        local td = tr:tag('td')
                        for _, purchase_data in ipairs(data) do
                            td:wikitext(string.format('%dx [[%s]]<br />', purchase_data.amount, purchase_data.name))
                        end
                    end
                end
                
                return tostring(tbl)
            end,
        },
    },
    {
        heading = i18n.tooltips.sell_price,
        {
            show = h.factory.args_present('sell_price_order'),
            func = function (tpl_args, frame)
                local out = {}
                for _, item_name in ipairs(tpl_args.sell_price_order) do
                    out[#out+1] = string.format('%dx [[%s]]', tpl_args.sell_prices[item_name], item_name)
                end
                
                return table.concat(out, '<br />')
            end,
        },
    },
    -- Damage per second
    {
        heading = i18n.tooltips.damage_per_second,
        {
            show = h.factory.args_present('physical_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'physical_dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.physical_dps,
            },
        },
        {
            show = h.factory.args_present('fire_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'fire_dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.fire_dps,
            },
        },
        {
            show = h.factory.args_present('cold_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'cold_dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.cold_dps,
            },
        },
        {
            show = h.factory.args_present('lightning_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'lightning_dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.lightning_dps,
            },
        },
        {
            show = h.factory.args_present('chaos_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'chaos_dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.chaos_dps,
            },
        },
        {
            show = h.factory.args_present('elemental_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'elemental_dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.elemental_dps,
            },
        },
        {
            show = h.factory.args_present('poison_dps_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'poison_dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.poison_dps,
            },
        },
        {
            show = h.factory.args_present('dps_html'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'dps_html',
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.dps,
            },
        },
    },
    {
        heading = i18n.tooltips.misc,
        {
            show = h.factory.args_present('class'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'class',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.item_class,
            },
        },
        {
            show = h.factory.args_present('metadata_id'),
            func = function (tpl_args, frame)
                return core.factory.infobox_line{
                    parts = {
                        {
                            key = 'metadata_id',
                            fmt = '%s',
                            inline = m_util.html.abbr('%s', tpl_args.metadata_id),
                        },
                    },
                    fmt = tostring(
                        mw.html.create('span')
                            :addClass('u-truncate-line')
                            :wikitext(i18n.tooltips.metadata_id)
                    ),
                }(tpl_args, frame)
            end,
        },
    },
}

-- ----------------------------------------------------------------------------
-- Subroutines
-- ----------------------------------------------------------------------------

local s = {}

-- Subroutines for p.item. These exist to declutter the main function. Each one of these is only called once.

function s.get_item_config(tpl_args, frame)
    -- Returns primary item configuration, based on item class

    h.process_arguments(tpl_args, frame, {'class_id', 'class'})
    local config = {
        tables = cfg.tables,
        args = cfg.default_args,
        late_args = cfg.late_args,
        defaults = {},
    }
    local extend_keys = m_util.table.keys(config)
    for _, row in pairs(cfg.class_groups) do
        if row.keys[tpl_args.class_id] then
            for _, k in ipairs(extend_keys) do
                if row[k] then
                    for _, v in ipairs(row[k]) do
                        table.insert(config[k], v)
                    end
                end
            end
            break
        end
    end
    local class_specifics = cfg.class_specifics[tpl_args.class_id]
    if class_specifics then
        for _, k in ipairs(extend_keys) do
            if class_specifics[k] then
                for _, v in ipairs(class_specifics[k]) do
                    table.insert(config[k], v)
                end
            end
        end
    end
    return config
end

function s.process_quest_rewards(tpl_args, frame)
    local rid = 1
    local continue
    tpl_args.quest_rewards = {}
    tpl_args.vendor_rewards = {}
    repeat 
        continue = true
        local prefix = string.format('quest_reward%s_', rid)
        
        local input_args = {
            shared = {
                ['type'] = true, 
                ['quest'] = false, 
                ['quest_id'] = false, 
                ['act'] = true, 
                ['class_ids'] = false,
                },
            vendor = {
                ['npc'] = true,
            },
            quest = {
                ['sockets'] = false, 
                ['item_level'] = false, 
                ['rarity_id'] = false, 
                ['notes'] = false,
            },
        }
        
        local rdata = {}
        
        for key, is_required in pairs(input_args.shared) do
            rdata[key] = tpl_args[prefix .. key]
            if is_required then
                if rdata[key] == nil then
                    continue = false
                    break
                end
            end
        end
        
        if rdata.quest == nil or rdata.quest_id == nil then
            continue = false
        end
        
        if continue and rdata.type == 'vendor' or rdata.type == 'quest' then
            for key, is_required in pairs(input_args[rdata.type]) do
                rdata[key] = tpl_args[prefix .. key]
                if is_required then
                    if rdata[key] == nil then
                        continue = false
                        break
                    end
                end
            end
        else
            continue = false
        end
        
        if continue then
            rdata.classes = {}
            if rdata.class_ids ~= nil then
                rdata.class_ids = m_util.string.split(rdata.class_ids, ',%s*')
                for index, class_id in ipairs(rdata.class_ids) do
                    local class = m_game.constants.characters[class_id] 
                    if class == nil then
                        error(string.format('Class id %s is invalid', class_id))
                    else
                        rdata.class_ids[index] = class.str_id
                        rdata.classes[index] = class.name
                    end
                end
            end
            
            
            if rdata.item_level then
                rdata.item_level = m_util.cast.number(rdata.item_level)
            end
            
            if rdata.rarity_id then
                if m_game.constants.rarities[rdata.rarity_id] == nil then
                    error(string.format(i18n.errors.invalid_rarity_id, tostring(rdata.rarity_id)))
                end
            end
            
            rdata._table = rdata.type .. '_rewards'
            rdata.type = nil
            
            tpl_args[rdata._table] = rdata
            
            m_cargo.store(frame, rdata)
            
            -- TODO: Verify quests and quest ids?
        end
        
        rid = rid + 1
    until continue == false
end

function s.process_base_item(tpl_args, frame)
    local where
    if tpl_args.base_item_id ~= nil then
        where = string.format('items.metadata_id="%s"', tpl_args.base_item_id)
    elseif tpl_args.base_item_page ~= nil then
        where = string.format('items._pageName="%s"' , tpl_args.base_item_page)
    elseif tpl_args.base_item ~= nil then
        where = string.format('items.name="%s"' , tpl_args.base_item)
    elseif tpl_args.rarity_id ~= 'normal' then
        error(i18n.errors.missing_base_item)
    else
        return
    end
    
    if where ~= nil and tpl_args.rarity_id == 'normal' and not tpl_args._flags.is_prophecy then
        error(i18n.errors.missing_rarity)
    end
    
    where = string.format('%s AND items.class_id="%s" AND items.rarity_id="normal"', where, tpl_args.class_id)
    
    local join = {}
    
    for _, table_name in ipairs(tpl_args._item_config.tables) do
        if table_name ~= 'items' then
            join[#join+1] = string.format('items._pageID=%s._pageID', table_name)
        end
    end
    
    local fields = {
        'items._pageName',
        'items.name',
        'items.metadata_id',
    }
    
    for _, k in ipairs(tpl_args._base_item_args) do
        local data = core.map[k]
        if data.field ~= nil then
            fields[#fields+1] = string.format('%s.%s', data.table, data.field)
        end
    end
    
    local result = m_cargo.query(
        tpl_args._item_config.tables,
        fields,
        {
            where=where,
            join=table.concat(join, ','),
            groupBy='items._pageID',
        }
    )
    
    if #result > 1 then
        error(i18n.errors.duplicate_base_items)
        -- TODO be more explicit in the error?
    elseif #result == 0 then
        error(i18n.errors.base_item_not_found)
    end
    
    result = result[1]
    
    tpl_args.base_item_data = result
    h.process_arguments(tpl_args, frame, {'base_item', 'base_item_page', 'base_item_id'})

    --Copy values..
    for _, k in ipairs(tpl_args._base_item_args) do
        local data = core.map[k]
        if data.field ~= nil and data.func_fetch == nil then
            local value = result[string.format('%s.%s', data.table, data.field)]
            -- I can just use data.default since it will be nil if not provided (nil == nil). Neat! ;)
            if value ~= nil and (tpl_args[k] == data.default or type(data.default) == 'function') then
                tpl_args[k] = value
                if data.func ~= nil then
                    tpl_args[k] = data.func(tpl_args, frame, tpl_args[k])
                end
                if data.func_copy ~= nil then
                    data.func_copy(tpl_args, frame)
                end
            elseif value == nil and not data.debug_ignore_nil then
                if tpl_args.debug then
                    error(string.format(i18n.debug.base_item_field_not_found, data.table, data.field))
                end
            elseif tpl_args[k] ~= data.default then
                if tpl_args.debug then
                    error(string.format(i18n.debug.field_value_mismatch, k, tostring(tpl_args[k])))
                end
            end
        elseif data.func_fetch ~= nil then
            data.func_fetch(tpl_args, frame)
        end
    end
end

function s.process_mods(tpl_args, frame)
    for _, k in ipairs({'implicit', 'explicit'}) do
        local success = true
        local i = 1
        while success do
            success = h.validate_mod(tpl_args, frame, {key=k, i=i})
            i = i + 1
        end
    end

    -- If the item does not have its own implicit mods, fall back to the implicit mods on the base item.
    local implicit_mods = tpl_args._defined_implicit_mods
    if #implicit_mods == 0 then
        implicit_mods = tpl_args._base_implicit_mods
    end
    for _, v in ipairs(implicit_mods) do
        table.insert(tpl_args._mods, v)
    end
    if #tpl_args._mods > 0 then
        local mods = {}
        local mod_ids = {}
        local non_random_mod_ids = {}
        for _, mod_data in ipairs(tpl_args._mods) do
            if mod_data.result == nil then
                mods[mod_data.id] = mod_data
                mod_ids[#mod_ids+1] = mod_data.id
                if not mod_data.is_random then
                    table.insert(non_random_mod_ids, mod_data.id)
                end
            end
            tpl_args._subobjects[#tpl_args._subobjects+1] = {
                _table = 'item_mods',
                id = mod_data.id,
                text = mod_data.stat_text,
                is_implicit = mod_data.is_implicit,
                is_random = mod_data.is_random,
            }
        end
        
        local results = m_cargo.array_query{
            tables={'mods'},
            fields={'mods._pageName', 'mods.id', 'mods.required_level', 'mods.stat_text'},
            id_field='mods.id',
            id_array=mod_ids,
        }
        
        for _, data in ipairs(results) do
            local mod_data = mods[data['mods.id']]
            mod_data.result = data
            
            if mod_data.is_random == false then
                 -- update item level requirement
                local keys = {'required_level_final'}
                -- only update base item requirement if this is an implicit
                if mod_data.key == 'implicit' then
                    keys[#keys+1] = 'required_level'
                end
                
                for _, key in ipairs(keys) do
                    local req = math.floor(tonumber(data['mods.required_level']) * cfg.item_required_level_modifier_contribution)
                    if req > tpl_args[key] then
                        tpl_args[key] = req
                    end
                end
            end
        end
        
        -- fetch stats
        
        results = m_cargo.query(
            {'mods', 'mod_stats'},
            {'mods.id', 'mod_stats.id', 'mod_stats.min', 'mod_stats.max'},
            {
                join='mods._pageID=mod_stats._pageID',
                where=string.format('mod_stats.id IS NOT NULL AND mods.id IN ("%s")', table.concat(mod_ids, '", "')),
            }
        )
        for _, data in ipairs(results) do
            -- Stat subobject
            local mod_data = mods[data['mods.id']]
            if mod_data.result.stats == nil then
                mod_data.result.stats = {data, }
            else
                mod_data.result.stats[#mod_data.result.stats+1] = data
            end
        
            local id = data['mod_stats.id']
            local value = {
                min = tonumber(data['mod_stats.min']),
                max = tonumber(data['mod_stats.max']),
            }
            value.avg = (value.min+value.max)/2
            
            local prefix = ''
            if mod_data.is_random then
                prefix = '_random'
            end
            core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_stats')
            if mod_data.is_implicit then
                core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_implicit_stats')
            else
                core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_explicit_stats')
            end
        end
        
        if tpl_args._flags.sell_prices_override ~= true then
            -- fetch sell prices
            results = m_cargo.query(
                {'mods', 'mod_sell_prices'},
                {'mods.id', 'mod_sell_prices.amount', 'mod_sell_prices.name'},
                {
                    join='mods._pageID=mod_sell_prices._pageID',
                    -- must be non random mods to avoid accumulating sell prices of randomized modifiers
                    where=string.format('mod_sell_prices.amount IS NOT NULL AND mods.id IN ("%s")', table.concat(non_random_mod_ids, '", "')),
                }
            )
            
            for _, data in ipairs(results) do
                local mod_data = mods[data['mods.id']]
                if not mod_data.is_implicit then
                    local values = {
                        name = data['mod_sell_prices.name'],
                        amount = tonumber(data['mod_sell_prices.amount']), 
                    }
                    -- sell_prices is defined in tpl_args.sell_prices_override
                    tpl_args.sell_prices[values.name] = (tpl_args.sell_prices[values.name] or 0) + values.amount
                end
            end
         end
    end
    if tpl_args._flags.sell_prices_override ~= true then
        local missing_sell_price = true
        for _, _ in pairs(tpl_args.sell_prices) do
            missing_sell_price = false
            break
        end
        
        if missing_sell_price then
            tpl_args.sell_prices[i18n.tooltips.default_vendor_offer] = 1
        end
        
        -- Set sell price on page
        for name, amount in pairs(tpl_args.sell_prices) do
            -- sell_price_order is defined in tpl_args.sell_prices_override
            tpl_args.sell_price_order[#tpl_args.sell_price_order+1] = name
            tpl_args._subobjects[#tpl_args._subobjects+1] = {
                _table = 'item_sell_prices',
                amount = amount,
                name = name,
            }
        end
        table.sort(tpl_args.sell_price_order)
    end
end

function s.process_stats(tpl_args, frame)
    -- Add stats - this is for when mods are not set, but we still need stats to calcuate new armour values etc
    m_util.args.stats(tpl_args, {prefix='extra_'})
    for _, stat in ipairs(tpl_args.extra_stats) do
        if stat.value ~= nil then
            stat.min = stat.value
            stat.max = stat.value
            stat.avg = stat.value
        end
        core.stats_update(tpl_args, stat.id, stat, nil, '_stats')
        core.stats_update(tpl_args, stat.id, stat, nil, '_explicit_stats')
    end

    -- Transpose stats into cargo data
    for _, random_prefix in ipairs({'', '_random'}) do
        for _, type_prefix in ipairs({'', '_implicit', '_explicit'}) do
            for id, data in pairs(tpl_args[random_prefix .. type_prefix .. '_stats']) do
                local is_implicit
                if type_prefix == '_implicit' then
                    is_implicit = true
                elseif type_prefix == '_explicit' then
                    is_implicit = false
                end
                tpl_args._subobjects[#tpl_args._subobjects+1] = {
                    _table = 'item_stats',
                    id = id,
                    min = data.min,
                    max = data.max,
                    avg = data.avg,
                    is_implicit = is_implicit,
                    is_random = random_prefix == '_random',
                }
            end
        end
    end
    
    -- Handle extra stats (for gems)
    if cfg.class_groups.gems.keys[tpl_args.class_id] then
        h.skill(tpl_args, frame)
    end
    
    --
    -- Handle local stats increases/reductions/additions
    --
    
    local skip = {}
    
    -- general stats
    for k, data in pairs(core.stat_map) do
        local value = tpl_args[k]
        if value ~= nil and skip[k] == nil then
            value = {min=value, max=value, base=value}
            -- If stats are overriden we scan save some CPU time here
            local overridden = false
            if data.stats_override ~= nil then
                for stat_id, override_value in pairs(data.stats_override) do
                    local stat_value = tpl_args._stats[stat_id]
                    if stat_value ~= nil then
                        -- Use the value of stat
                        if override_value == true then
                            value.min = stat_value.min
                            value.max = stat_value.max
                            overridden = true
                        elseif stat_value ~= 0 then
                            value.min = override_value.min
                            value.max = override_value.max
                            overridden = true
                        end
                    end
                end
           end
                
           if overridden == false then
                -- The simple cases; this must be using ipairs as "add" must apply before
                for _, operator in ipairs({'add', 'more'}) do
                    local st = data['stats_' .. operator]
                    if st ~= nil then
                        for _, statid in ipairs(st) do
                            if tpl_args._stats[statid] ~= nil then
                                h.stat[operator](value, tpl_args._stats[statid])
                            end
                        end
                    end
                end
                
                -- For increased stats we need to add them up first
                for stat_key, stat_func in pairs({increased=h.stat.more, increased_inverse=h.stat.more_inverse}) do
                    local st = data['stats_' .. stat_key]
                    if st ~= nil then
                        local total_increase = {min=0, max=0}
                        for _, statid in ipairs(st) do
                            if tpl_args._stats[statid] ~= nil then
                                for var, current_value in pairs(total_increase) do
                                    total_increase[var] = current_value + tpl_args._stats[statid][var]
                                end
                            end
                        end
                        stat_func(value, total_increase)
                    end
                end
                
                if data.minimum ~= nil then
                    for _, key in ipairs({'min', 'max'}) do
                        if value[key] < data.minimum then
                            value[key] = data.minimum 
                        end
                    end
                end
            else

            end
            
            value.avg = (value.min + value.max) / 2
            
            -- don't add the properties unless we need to
            if (data.default ~= nil and (value.min ~= data.default or value.max ~= data.default)) or data.default == nil then
                for short_key, range_data in pairs(c.range_map) do
                    tpl_args[data.field .. range_data.var] = value[short_key]
                end
                
                -- process to HTML to use on list pages or other purposes
                h.handle_range_args(tpl_args, frame, k, data.field, value, data.html_fmt_options or {})
            end
            
            for short_key, range_data in pairs(c.range_map) do
                tpl_args[k .. range_data.var] = value[short_key]
            end
        end
    end
end

function s.process_weapon_dps(tpl_args, frame)
    for key, data in pairs(core.dps_map) do
        local damage = {
            min = {},
            max = {},
        }
        for var_type, value in pairs(damage) do
            -- covers the min/max/avg range
            for short_key, range_data in pairs(c.range_map) do
                value[short_key] = 0
                for _, damage_key in ipairs(data.damage_args) do
                    value[short_key] = value[short_key] + (tpl_args[string.format('%s_%s%s', damage_key, var_type, range_data.var)] or 0)
                end
            end
        end
        local value = {}
        for short_key, range_data in pairs(c.range_map) do
            local result = (damage.min[short_key] + damage.max[short_key]) / 2 * tpl_args[string.format('attack_speed%s', range_data.var)]
            value[short_key] = result
            tpl_args[string.format('%s%s', data.field, range_data.var)] = result
        end
        if value.avg > 0 then
            h.handle_range_args(tpl_args, frame, key, data.field, value, data.html_fmt_options or {})
        end
    end
end

function s.get_categories(tpl_args, frame)
    local cats = {}
    if tpl_args.rarity_id == 'unique' then
        cats[#cats+1] = string.format(i18n.categories.unique_affix, tpl_args.class)
    elseif tpl_args._flags.is_prophecy then
        cats[#cats+1] = i18n.categories.prophecies
    elseif tpl_args._flags.is_blight_item then
        cats[#cats+1] = i18n.categories.blight_item
    elseif tpl_args.is_talisman then
        cats[#cats+1] = i18n.categories.talismans
    elseif tpl_args.is_essence then
        cats[#cats+1] = i18n.categories.essences
    elseif tpl_args.class_id == 'Map' then
        cats[#cats+1] = string.format('%s %s', tpl_args.map_series, tpl_args.class)
    else
        cats[#cats+1] = tpl_args.class
    end
    
    if tpl_args.rarity_id ~= 'normal' or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
        cats[#cats+1] = i18n.categories.derived_items
    else
        cats[#cats+1] = i18n.categories.base_items
    end
    
    for _, attr in ipairs(m_game.constants.attribute_order) do
        if tpl_args[attr .. '_percent'] then
            cats[#cats+1] = string.format('%s %s', m_game.constants.attributes[attr].long_upper, tpl_args.class)
        end
    end
    
    local affix
    if tpl_args.class_id == 'Active Skill Gem' or tpl_args.class_id == 'Support Skill Gem' then
        affix = i18n.categories.gem_tag_affix
    end
    if affix ~= nil then
        for _, tag in ipairs(tpl_args.gem_tags) do
            cats[#cats+1] = string.format(affix, tag)
        end
    end

    if #tpl_args._defined_implicit_mods > 0 and tpl_args.rarity_id ~= 'normal' then
        cats[#cats+1] = i18n.categories.implicit_modifier_override
    end
    
    if #tpl_args.alternate_art_inventory_icons > 0 then
        cats[#cats+1] = i18n.categories.alternate_artwork
    end
    
    if tpl_args.release_version == nil then
        cats[#cats+1] = i18n.categories.missing_release_version
    end
    
    if tpl_args._flags.text_modifier and not tpl_args.suppress_improper_modifiers_category then
        cats[#cats+1] = i18n.categories.improper_modifiers
    end
    
    -- 
    for _, k in ipairs({'broken_upgraded_from_reference', 'duplicate_upgraded_from_reference', 'duplicate_query_area_ids', 'sell_prices_override'}) do
        if tpl_args._flags[k] then
            cats[#cats+1] = i18n.categories[k]
        end
    end

    if tpl_args._flags.has_deprecated_skill_parameters then
        cats[#cats+1] = i18n.categories.deprecated_skill_parameters
    end
    return cats
end

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

local p = {}

--
-- Template:Item
--
function p.item(frame)
    local t = os.clock() -- Start time (for logging)
    
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    tpl_args._flags = {}
    tpl_args._base_item_args = {}
    tpl_args._base_implicit_mods = {}
    tpl_args._defined_implicit_mods = {}
    tpl_args._mods = {}
    for _, k in ipairs({'', '_random'}) do
        for _, prefix in ipairs({'', '_implicit', '_explicit'}) do
            tpl_args[k .. prefix .. '_stats'] = {}
        end
    end
    tpl_args._subobjects = {}
    tpl_args._properties = {}
    tpl_args._errors = {}
    
    -- Item configuration
    tpl_args._item_config = s.get_item_config(tpl_args, frame)

    h.build_cargo_data(tpl_args, frame, tpl_args._item_config.tables)
    
    -- Using general purpose function to handle release and removal versions
    m_util.args.version(tpl_args, {frame=frame, set_properties=true})
    
    -- Must validate some argument early. It is required for future things
    h.process_arguments(tpl_args, frame, tpl_args._item_config.args)
    
    -- Base item
    s.process_base_item(tpl_args, frame)
    
    -- Prophecies are considered currency items, but they have their own schema
    if tpl_args._flags.is_prophecy then
        h.process_arguments(tpl_args, frame, cfg.prophecy_args)
    end
    
    -- Mods
    s.process_mods(tpl_args, frame)

    -- Stats
    s.process_stats(tpl_args, frame)

    -- Calculate and handle weapon dps
    if cfg.class_groups.weapons.keys[tpl_args.class_id] then
        s.process_weapon_dps(tpl_args, frame)
    end
    
    -- Late argument processing
    h.process_arguments(tpl_args, frame, tpl_args._item_config.late_args)
    
    -- Handle upgrade from restrictions/info
    h.process_upgraded_from(tpl_args, frame)
    
    -- Quest reward info
    s.process_quest_rewards(tpl_args, frame)
    
    -- ------------------------------------------------------------------------
    -- Infobox handling 
    -- ------------------------------------------------------------------------
    -- Store the infobox so it can be accessed with ease on other pages
    tpl_args.html = tostring(h.make_main_container(tpl_args, frame, 'inline'))
    local container = h.make_main_container(tpl_args, frame, 'infobox')
    
    if tpl_args.inventory_icon ~= nil and tpl_args.class_id ~= 'DivinationCard' then
        container:tag('span')
            :addClass('images')
            :wikitext(string.format(
                '[[%s|%sx%spx]]',
                tpl_args.inventory_icon,
                cfg.image_size_full * tpl_args.size_x,
                cfg.image_size_full * tpl_args.size_y
            ))
    end
    
    --
    -- Secondary infobox
    --
    
    tpl_args.extra_infobox = mw.html.create('span')
        :attr( 'class', 'item-box -' .. tpl_args.frame_type)
        
    h.add_to_container_from_map(tpl_args, frame, tpl_args.extra_infobox, c.extra_display_groups)
    
    -- 
    -- Output 
    --
    
    local infobox = mw.html.create('span')
    infobox
        :attr('class', 'infobox-page-container')
        :node(container)
        :node(tpl_args.extra_infobox)
        
    -- skill_screenshot is set in skill module
    if tpl_args.skill_screenshot then
        infobox:wikitext(string.format('<br>[[%s|300px]]', tpl_args.skill_screenshot))
    end
    
    local out = tostring(infobox)
    tpl_args.html_extra = out
    
    cats = s.get_categories(tpl_args, frame)
    
    out = out .. m_util.misc.add_category(cats, {ignore_blacklist=tpl_args.debug})
    
    --
    --  Misc
    --
     
    -- Also show the infobox for areas right away for maps, since they're both on the same page
    -- if tpl_args.map_series ~= i18n.misc.betrayal then
    local query_id
    if tpl_args.rarity_id == 'normal' and tpl_args.map_area_id ~= nil then
        query_id = tpl_args.map_area_id
    elseif tpl_args.rarity_id == 'unique' and tpl_args.unique_map_area_id ~= nil then
        query_id = tpl_args.unique_map_area_id
    end

    if query_id then
        out = out .. h.query_area_info{cats=true, where=string.format('areas.id="%s"', query_id)}
    end
    
    -- ------------------------------------------------------------------------
    -- Store cargo data
    -- ------------------------------------------------------------------------
    -- Map argument values for cargo storage
    for _, table_name in ipairs(tpl_args._item_config.tables) do
        tpl_args._subobjects[table_name] = {
            _table = table_name,
        }
    end
    if tpl_args.debug then
        mw.log('Start logging tpl_args.')
        mw.logObject(tpl_args)
        mw.log('Stop logging tpl_args.')
        mw.log('Start logging core.map.')
        mw.logObject(core.map)
        mw.log('Stop logging core.map.')
    end
    for k, v in pairs(tpl_args) do
        local data = core.map[k]
        if data ~= nil then
            if data.table ~= nil and data.field ~= nil then
                if data.type == 'Integer' then
                    v = tonumber(string.format("%.0f", v))
                    if v ~= tpl_args[k] then
                        mw.logObject(string.format('Float value "%s" for integer field "%s.%s"', tpl_args[k], data.table, data.field))
                    end
                end
                tpl_args._subobjects[data.table][data.field] = v
            elseif data.table == nil and data.field ~= nil then
                error(string.format('Missing table for field "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.field, k, mw.dumpObject(v), mw.dumpObject(data)))
            elseif data.table ~= nil and data.field == nil then
                error(string.format('Missing field for table "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.table, k, mw.dumpObject(v), mw.dumpObject(data)))
            end
        end
    end
    
    -- Don't actually save data in testing mode
    if not tpl_args.test then
        local attach = {}
        for _, data in pairs(tpl_args._subobjects) do
            if attach[data._table] == nil then
                local i = 0 
                for _, _ in pairs(data) do
                    i = i + 1
                    -- Don't attach to tables we don't store data to. _table is always present so we need to check for 2 or more entries.
                    if i > 1 then
                        attach[data._table] = true
                        -- Keeping this here in case it might be useful in the future.
                        -- Recreating table is very slow and doing anything while the queue is running just creates a big mess.
                        frame:expandTemplate{title=string.format('Template:Item/cargo/attach/%s', data._table), args={}}
                        break
                    end
                end
            end
            m_cargo.store(frame, data, {
                debug=tpl_args.debug,
                sep={
                    name_list='�',
                },
            })
        end
    end
    
    -- Show additional error messages in console to help fixing them
    if #tpl_args._errors > 0 then
        mw.logObject(table.concat(tpl_args._errors, '\n'))
    end
    
    if tpl_args.debug then
        mw.logObject(os.clock() - t)
    end
    return out
end

p.itembox = p.item -- Apparently this is still used somewhere???

-- 
-- Template:Itembox
-- 
function p.query_itembox(frame)
    --[[
    Queries for an item box.
    
    Examples
    --------
    =p.query_itembox{page='Beach Map (Betrayal)'}
    ]]
    
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    local results = m_cargo.query(
        {'items', 'maps', 'areas'}, 
        {'items.html_extra', 'maps.area_id', 'areas.id', 'areas.infobox_html'},
        {
            join='items._pageID=maps._pageID, maps.area_id=areas.id',
            where=string.format('items._pageName="%s"', tpl_args.page),
        }
    )
    
    if #results == 0 then
        error(i18n.errors.no_results_found)
    end
    
    local out = {
        results[1]['items.html_extra'], 
        results[1]['areas.infobox_html'],
    }
    
    return table.concat(out, '')
end

return p