Module:Item2: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
No edit summary
(No globals. Fixed several bugs that were hard to track down. Unused arguments no longer throw errors. Items can now be defined as not sellable to NPC vendors.)
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')
local getArgs = require('Module:Arguments').getArgs
local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_cargo = require('Module:Cargo')
local m_skill = require('Module:Skill')


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
local f_process_upgraded_from -- require('Module:Item2/upgrade').process_upgraded_from
local f_process_upgraded_from -- require('Module:Item2/upgrade').process_upgraded_from
local f_build_cargo_data -- require('Module:Item2/cargo').build_cargo_data
local f_append_schema -- require('Module:Item2/cargo').append_schema


-- The cfg table contains all localisable strings and configuration, to make it
-- The cfg table contains all localisable strings and configuration, to make it
Line 62: Line 61:
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')


-- Declare early to avoid errors
local c = {}
local c = {}
c.item_classes = {}


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 70: Line 69:


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(tpl_args, frame)
end


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


-- Lazy loading for Module:Item2/cargo
-- Lazy loading for Module:Item2/cargo
function h.build_cargo_data(tpl_args, frame, item_classes)
function h.append_schema(tpl_args, frame, tables)
     if not f_build_cargo_data then
     if not f_append_schema 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_append_schema = use_sandbox and require('Module:Item2/cargo/sandbox').append_schema or require('Module:Item2/cargo').append_schema
     end
     end
     return f_build_cargo_data(tpl_args, frame, item_classes)
     return f_append_schema(tpl_args, frame, tables)
end
end


Line 204: Line 211:
             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 218: Line 227:
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
     end
    return stat_text
end


     local class_specifics = cfg.class_specifics[tpl_args.class_id]
function h.add_to_container_from_map(tpl_args, frame, container, mapping, container_type)
     if class_specifics then
     local statcont = mw.html.create('span')
         for _, k in ipairs(item_classes_extend) do
     statcont
            if class_specifics[k] ~= nil then
        :attr('class', 'item-stats')
                for _, value in ipairs(class_specifics[k]) do
         :done()
                     table.insert(c.item_classes[tpl_args.class_id][k], value)
    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
             end
             end
        else
            lines = group.func(tpl_args, frame, container_type)
         end
         end
         if class_specifics.defaults ~= nil then
         if #lines > 0 then
             for key, value in pairs(class_specifics.defaults) do
            count = count + 1
                 c.item_classes[tpl_args.class_id].defaults[key] = value
            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
             end
      end
            statcont
                :tag('span')
                :attr('class', 'group ' .. (group.class or ''))
                :wikitext(heading .. table.concat(lines, '<br>'))
                :done()
        end
     end
     end
end


function h.process_mods(tpl_args, frame)
    -- Add groups to container
    -- If the item does not have its own implicit mods, fall back to the implicit mods on the base item.
     if count > 0 then
    local implicit_mods = tpl_args._defined_implicit_mods
         container:node(statcont)
     if #implicit_mods == 0 then
         implicit_mods = tpl_args._base_implicit_mods
     end
     end
     for _, v in ipairs(implicit_mods) do
end
         table.insert(tpl_args._mods, v)
 
     end
function h.make_main_container(tpl_args, frame, container_type)
     if #tpl_args._mods > 0 then
     local container = mw.html.create('span')
        local mods = {}
         :attr('class', 'item-box -' .. tpl_args.frame_type)
        local mod_ids = {}
      
         local non_random_mod_ids = {}
     if tpl_args.class_id == 'DivinationCard' then
        for _, mod_data in ipairs(tpl_args._mods) do
         container
            if mod_data.result == nil then
            :tag('span')
                 mods[mod_data.id] = mod_data
                 :attr('class', 'divicard-wrapper')
                 mod_ids[#mod_ids+1] = mod_data.id
                 :tag('span')
                if not mod_data.is_random then
                     :attr('class', 'divicard-art')
                     table.insert(non_random_mod_ids, mod_data.id)
                    :wikitext( '[[' .. tpl_args.card_art .. '|link=|alt=]]' )
                end
                    :done()
            end
                 :tag('span')
            tpl_args._subobjects[#tpl_args._subobjects+1] = {
                    :attr('class', 'divicard-frame')
                 _table = 'item_mods',
                    :wikitext( '[[File:Divination card frame.png|link=|alt=]]' )
                id = mod_data.id,
                    :done()
                text = mod_data.stat_text,
                :tag('span')
                is_implicit = mod_data.is_implicit,
                    :attr('class', 'divicard-header')
                is_random = mod_data.is_random,
                    :wikitext(tpl_args.name)
            }
                    :done()
        end
                :tag('span')
       
                    :attr('class', 'divicard-stack')
        local results = m_cargo.array_query{
                    :wikitext(tpl_args.stack_size)
            tables={'mods'},
                    :done()
            fields={'mods._pageName', 'mods.id', 'mods.required_level', 'mods.stat_text'},
                :tag('span')
            id_field='mods.id',
                    :attr('class', 'divicard-reward')
            id_array=mod_ids,
                    :tag('span')
        }
                        :wikitext(tpl_args.description)
       
                        :done()
        for _, data in ipairs(results) do
                    :done()
            local mod_data = mods[data['mods.id']]
                 :tag('span')
            mod_data.result = data
                    :attr('class', 'divicard-flavour text-color -flavour')
           
                     :tag('span')
            if mod_data.is_random == false then
                        :wikitext(tpl_args.flavour_text)
                -- update item level requirement
                        :done()
                 local keys = {'required_level_final'}
                     :done()
                -- only update base item requirement if this is an implicit
                :done()
                if mod_data.key == 'implicit' then
        --TODO Extras?
                     keys[#keys+1] = 'required_level'
    else
                end
        local header_css
               
        local line_type
                for _, key in ipairs(keys) do
        if tpl_args.base_item and tpl_args.rarity_id ~= 'normal' then
                     local req = math.floor(tonumber(data['mods.required_level']) * cfg.item_required_level_modifier_contribution)
            line_type = 'double'
                    if req > tpl_args[key] then
        else
                        tpl_args[key] = req
             line_type = 'single'
                    end
                end
             end
         end
         end
          
          
         -- fetch stats
        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
          
          
         results = m_cargo.query(
         container
             {'mods', 'mod_stats'},
             :tag('span')
            {'mods.id', 'mod_stats.id', 'mod_stats.min', 'mod_stats.max'},
                :attr( 'class', 'header -' .. line_type )
            {
                 :tag('span')
                 join='mods._pageID=mod_stats._pageID',
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[1] or '') )
                where=string.format('mod_stats.id IS NOT NULL AND mods.id IN ("%s")', table.concat(mod_ids, '", "')),
                    :done()
            }
                :wikitext(name_line)
        )
                :tag('span')
        for _, data in ipairs(results) do
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[2] or '') )
            -- Stat subobject
                    :done()
            local mod_data = mods[data['mods.id']]
             :done()
            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 = ''
        h.add_to_container_from_map(tpl_args, frame, container, c.item_infobox_groups, container_type)
            if mod_data.is_random then
    end
                prefix = '_random'
   
            end
    if tpl_args.skill_icon ~= nil then
            core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_stats')
        container:wikitext(string.format('[[%s]]', tpl_args.skill_icon))
            if mod_data.is_implicit then
    end
                core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_implicit_stats')
   
             else
    return container
                 core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_explicit_stats')
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
         end
         end
          
         return true
        if tpl_args._flags.sell_prices_override ~= true then
    end
            -- fetch sell prices
end
            results = m_cargo.query(
 
                {'mods', 'mod_sell_prices'},
function h.factory.display_raw_value(key)
                {'mods.id', 'mod_sell_prices.amount', 'mod_sell_prices.name'},
    return function(tpl_args, frame)
                {
        return tpl_args[key]
                    join='mods._pageID=mod_sell_prices._pageID',
    end
                    -- must be non random mods to avoid accumulating sell prices of randomized modifiers
end
                    where=string.format('mod_sell_prices.amount IS NOT NULL AND mods.id IN ("%s")', table.concat(non_random_mod_ids, '", "')),
 
                }
function h.factory.descriptor_value(args)
            )
    -- Arguments:
           
    --  key
            for _, data in ipairs(results) do
    --  tbl
                local mod_data = mods[data['mods.id']]
    args = args or {}
                if not mod_data.is_implicit then
    return function (tpl_args, frame, value)
                    local values = {
        args.tbl = args.tbl or tpl_args
                        name = data['mod_sell_prices.name'],
        if args.tbl[args.key] then
                        amount = tonumber(data['mod_sell_prices.amount']),
            value = m_util.html.abbr(value, args.tbl[args.key])
                    }
        end
                    -- sell_prices is defined in tpl_args.sell_prices_override
        return value
                    tpl_args.sell_prices[values.name] = (tpl_args.sell_prices[values.name] or 0) + values.amount
                end
            end
        end
     end
     end
    if tpl_args._flags.sell_prices_override ~= true then
end
        local missing_sell_price = true
 
        for _, _ in pairs(tpl_args.sell_prices) do
-- ----------------------------------------------------------------------------
            missing_sell_price = false
-- Additional configuration
            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 h.process_base_item(tpl_args, frame)
-- helper to loop over the range variables easier
    local where
c.range_map = {
     if tpl_args.base_item_id ~= nil then
     min = {
         where = string.format('items.metadata_id="%s"', tpl_args.base_item_id)
         var = '_range_minimum',
     elseif tpl_args.base_item_page ~= nil then
     },
        where = string.format('items._pageName="%s"' , tpl_args.base_item_page)
     max = {
     elseif tpl_args.base_item ~= nil then
         var = '_range_maximum',
         where = string.format('items.name="%s"' , tpl_args.base_item)
     },
     elseif tpl_args.rarity_id ~= 'normal' then
     avg = {
        error(i18n.errors.missing_base_item)
         var = '_range_average',
     else
     },
        return
}
    end
 
   
--
    if where ~= nil and tpl_args.rarity_id == 'normal' and not tpl_args._flags.is_prophecy then
-- Contents here are meant to resemble the ingame infobox of items
         error(i18n.errors.missing_rarity)
--
    end
c.item_infobox_groups = {
   
     -- [n]:
    where = string.format('%s AND items.class_id="%s" AND items.rarity_id="normal"', where, tpl_args.class_id)
     --  class: Additional css class added to group tag
      
     --  heading: Group heading text (used for extras)
    local join = {}
     --  lines:
   
     --  [n]:
    for _, table_name in ipairs(c.item_classes[tpl_args.class_id].tables) do
    --    show: Show line if this function returns true; Always show if boolean true. Default: Always show
        if table_name ~= 'items' then
     --    func: Function that returns line text
            join[#join+1] = string.format('items._pageID=%s._pageID', table_name)
     {
        end
         -- Cosmetic item type
     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(
         c.item_classes[tpl_args.class_id].tables,
        fields,
         {
         {
             where=where,
             show = h.factory.args_present('cosmetic_type'),
            join=table.concat(join, ','),
             func = core.factory.infobox_line{
             groupBy='items._pageID',
                parts = {
        }
                    {
    )
                        key = 'cosmetic_type',
   
                        fmt = '%s',
    if #result > 1 then
                        color = 'default'
        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
         -- Weapon type
   
         {
    result = result[1]
             show = function (tpl_args, frame)
   
                if tpl_args.class_id == nil then  
    tpl_args.base_item_data = result
                     return false
    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
                 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,
                end
        },
             elseif tpl_args[k] ~= data.default then
        -- Hideout item type
                 if tpl_args.debug then
        {
                    error(string.format(i18n.debug.field_value_mismatch, k, tostring(tpl_args[k])))
             show = function (tpl_args, frame)
                 end
                 return tpl_args.class_id == 'HideoutDoodad'
             end
            end,
         elseif data.func_fetch ~= nil then
            func = function (tpl_args, frame)
             data.func_fetch(tpl_args, frame)
                 return i18n.item_class_map[tpl_args.class_id]
        end
             end,
    end
         },
end
        {
 
             show = h.factory.args_present('gem_tags'),
function h.process_quest_rewards(tpl_args, frame)
            func = function (tpl_args, frame)
    local rid = 1
                local out = {}
    local continue
                for i, tag in ipairs(tpl_args.gem_tags) do
    tpl_args.quest_rewards = {}
                    out[#out+1] = string.format(i18n.gem_tag_category, tag, tag)
    tpl_args.vendor_rewards = {}
                end
    repeat
                return table.concat(out, ', ')
        continue = true
            end,
        local prefix = string.format('quest_reward%s_', rid)
         },
          
         {
         local input_args = {
             show = h.factory.args_present('support_gem_letter_html'),
             shared = {
            func = core.factory.infobox_line{
                ['type'] = true,  
                 parts = {
                ['quest'] = false,
                    {
                 ['quest_id'] = false,
                        key = 'support_gem_letter_html',
                ['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
                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
                 end
                 end
            end
                return core.factory.infobox_line{
        else
                    type = 'gem',
            continue = false
                    parts = parts,
        end
                    sep = ', ',
          
                    fmt = i18n.tooltips.cost,
         if continue then
                }(tpl_args, frame)
            rdata.classes = {}
            end,
             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
             show = function (tpl_args, frame)
                    local class = m_game.constants.characters[class_id]
                 return tpl_args.skill_costs
                    if class == nil then
            end,
                        error(string.format('Class id %s is invalid', class_id))
            func = function (tpl_args, frame)
                    else
                if not tpl_args.skill_costs then
                        rdata.class_ids[index] = class.str_id
                     return
                        rdata.classes[index] = class.name
                     end
                 end
                 end
            end
                local parts = {}
           
                if not tpl_args.skill_costs.has_reservation_cost then
           
                    -- Try falling back to deprecated parameters
            if rdata.item_level then
                    if tpl_args.has_reservation_mana_cost then
                rdata.item_level = m_util.cast.number(rdata.item_level)
                        parts[1] = {
            end
                            key = 'mana_cost',
           
                            fmt = '%i',
            if rdata.rarity_id then
                            color = false,
                if m_game.constants.rarities[rdata.rarity_id] == nil then
                            inline = string.format(
                    error(string.format(i18n.errors.invalid_rarity_id, tostring(rdata.rarity_id)))
                                '%%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
                 end
            end
                return core.factory.infobox_line{
           
                    type = 'gem',
            rdata._table = rdata.type .. '_rewards'
                    parts = parts,
            rdata.type = nil
                    sep = ', ',
           
                    fmt = i18n.tooltips.reservation,
            tpl_args[rdata._table] = rdata
                }(tpl_args, frame)
           
             end,
            m_cargo.store(frame, rdata)
        },
              
        {
            -- TODO: Verify quests and quest ids?
            show = true, -- TODO: Show only if has mana_multiplier
        end
            func = core.factory.infobox_line{
       
                type = 'gem',
        rid = rid + 1
                parts = {
    until continue == false
                    {
end
                        key = 'mana_multiplier',
 
                        hide_default = 100,
--
                        fmt = '%i',
-- Display
                        color = false,
--
                        inline = '%s%%',
 
                        inline_color = 'value',
function h.strip_random_stats(tpl_args, frame, stat_text, container_type)
                    },
    if tpl_args._flags.random_mods and container_type == 'inline' then
                },
        repeat
                 fmt = i18n.tooltips.mana_multiplier,
            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
             show = true, -- TODO: Show only if has cooldown
        until text == nil
            func = core.factory.infobox_line{
    end
                type = 'gem',
    return stat_text
                parts = {
end
                    {
 
                        key = 'cooldown',  
function h.add_to_container_from_map(tpl_args, frame, container, mapping, container_type)
                        hide_default = 0,
    local statcont = mw.html.create('span')
                        fmt = '%.2f ' .. m_game.units.seconds.short_lower,
    statcont
                    },
        :attr('class', 'item-stats')
                },
        :done()
                fmt = i18n.tooltips.cooldown_time,
    local count = 0 -- Number of groups in container
            },
    for _, group in ipairs(mapping) do
         },
         local lines = {}
         {
         if group.func == nil then
             -- TODO: Combine with cooldown. Multi-use non-vaal skills display uses together with cooldown time. E.g., Cooldown Time: 8.00 sec (3 uses)
             for _, line in ipairs(group) do
            show = true,
                local show = true
            func = core.factory.infobox_line{
                 if container_type == 'inline' and line.inline == false then -- TODO: This is not used currently. Need to address what is hidden in inline view.
                 type = 'gem',
                     show = false
                parts = {
                elseif line.show == false then
                     {
                    show = false
                        key = 'stored_uses',
                elseif type(line.show) == 'function' then
                        hide_default = 0,
                     show = line.show(tpl_args, frame, container_type)
                        fmt = '%i',
                 end
                     },
                 if show then
                 },
                    lines[#lines+1] = line.func(tpl_args, frame, container_type)
                 fmt = i18n.tooltips.stored_uses,
                end
             },
             end
         },
         else
         {
            lines = group.func(tpl_args, frame, container_type)
             show = true, -- TODO: Show only if has vaal_souls_requirement
         end
             func = core.factory.infobox_line{
        if #lines > 0 then
                type = 'gem',
             count = count + 1
                parts = {
             local heading = ''
                    {
            if group.heading == nil then
                        key = 'vaal_souls_requirement',
            elseif type(group.heading) == 'function' then
                        hide_default = 0,
                heading = group.heading()
                        fmt = '%i',
            else
                    },
                heading = string.format('<em class="header">%s</em><br>', group.heading)
                },
            end
                 fmt = i18n.tooltips.vaal_souls_per_use,
            statcont
            },
                 :tag('span')
        },
                :attr('class', 'group ' .. (group.class or ''))
        {
                :wikitext(heading .. table.concat(lines, '<br>'))
            show = true, -- TODO: Show only if has vaal_stored_uses
                :done()
            func = core.factory.infobox_line{
        end
                type = 'gem',
    end
                parts = {
 
                    {
    -- Add groups to container
                        key = 'vaal_stored_uses',
    if count > 0 then
                        hide_default = 0,
        container:node(statcont)
                        fmt = '%i',
    end
                    },
end
                },
 
                fmt = i18n.tooltips.stored_uses, -- TODO: Singular or plural based on number
function h.make_main_container(tpl_args, frame, container_type)
            },
    local container = mw.html.create('span')
        },
        :attr('class', 'item-box -' .. tpl_args.frame_type)
         {
   
             show = true, -- TODO: Show only if has vaal_soul_gain_prevention_time
    if tpl_args.class_id == 'DivinationCard' then
            func = core.factory.infobox_line{
         container
                 type = 'gem',
             :tag('span')
                 parts = {
                 :attr('class', 'divicard-wrapper')
                     {
                 :tag('span')
                        key = 'vaal_soul_gain_prevention_time',
                     :attr('class', 'divicard-art')
                        hide_default = 0,
                    :wikitext( '[[' .. tpl_args.card_art .. '|link=|alt=]]' )
                        -- Technically it rounds to nearest, but it is given in milliseconds in the data,
                     :done()
                        fmt = '%i ' .. m_game.units.seconds.short_lower,
                 :tag('span')
                     },
                    :attr('class', 'divicard-frame')
                 },
                    :wikitext( '[[File:Divination card frame.png|link=|alt=]]' )
                fmt = i18n.tooltips.vaal_soul_gain_prevention_time,
                    :done()
            },
                 :tag('span')
        },
                    :attr('class', 'divicard-header')
        {
                    :wikitext(tpl_args.name)
            show = function (tpl_args, frame)
                    :done()
                 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))
                :tag('span')
            end,
                    :attr('class', 'divicard-stack')
            func = function (tpl_args, frame)
                    :wikitext(tpl_args.stack_size)
                return core.factory.infobox_line{
                     :done()
                     parts = {
                :tag('span')
                        {
                    :attr('class', 'divicard-reward')
                            key = 'cast_time',
                    :tag('span')
                            fmt = function (tpl_args, frame, value)
                        :wikitext(tpl_args.description)
                                if value.min == 0 then
                        :done()
                                    return i18n.tooltips.instant_cast_time
                    :done()
                                end
                :tag('span')
                                return '%.2f ' .. m_game.units.seconds.short_lower
                     :attr('class', 'divicard-flavour text-color -flavour')
                            end,
                     :tag('span')
                        },
                        :wikitext(tpl_args.flavour_text)
                     },
                        :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,
                    :done()
                }(tpl_args, frame)
                :done()
            end,
         --TODO Extras?
        },
    else
         {
        local header_css
            show = true, -- TODO: Show only if has critical_strike_chance
        if tpl_args.base_item and tpl_args.rarity_id ~= 'normal' then
            func = core.factory.infobox_line{
            line_type = 'double'
                type = 'gem',
        else
                parts = {
            line_type = 'single'
                    {
        end
                        key = 'critical_strike_chance',
       
                        hide_default = 0,
        local name_line = tpl_args.name
                        fmt = '%.2f%%',
        if tpl_args.base_item and not tpl_args._flags.is_prophecy then
                    },
            name_line = name_line .. '<br>' .. tpl_args.base_item
                },
        end
                fmt = i18n.tooltips.critical_strike_chance,
 
            },
         -- 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 {}
            show = true, -- TODO: Show only if has attack_speed_multiplier
        if tpl_args.is_replica then
             func = core.factory.infobox_line{
             symbols = {'replica', 'replica'}
                type = 'gem',
            if #influences > 0 then
                parts = {
                symbols[2] = influences[1]
                    {
            end
                        key = 'attack_speed_multiplier',
        elseif #influences > 0 then
                        hide_default = 100,
            symbols = {influences[1], influences[1]}
                        fmt = '%i',
            if #influences > 1 then
                        color = false,
                symbols[2] = influences[2]
                        inline = '%s%% ' .. i18n.tooltips.of_base_stat,
            end
                        inline_color = 'value',
        elseif tpl_args.is_fractured then
                    },
            symbols = {'fractured', 'fractured'}
                },
        elseif tpl_args.is_synthesised then
                fmt = i18n.tooltips.attack_speed_multiplier,
            symbols = {'synthesised', 'synthesised'}
            },
        elseif tpl_args.is_veiled then
         },
            symbols = {'veiled', 'veiled'}
         {
        end
             show = true, -- TODO: Show only if has damage_multiplier
          
            func = core.factory.infobox_line{
         container
                 type = 'gem',
             :tag('span')
                 parts = {
                 :attr( 'class', 'header -' .. line_type )
                     {
                 :tag('span')
                        key = 'damage_multiplier',
                     :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[1] or '') )
                        hide_default = 100,
                     :done()
                        fmt = '%i',
                 :wikitext(name_line)
                        color = false,
                 :tag('span')
                        inline = '%s%% ' .. i18n.tooltips.of_base_stat,
                    :attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[2] or '') )
                        inline_color = 'value',
                    :done()
                     },
             :done()
                 },
           
                 fmt = i18n.tooltips.damage_multiplier,
         h.add_to_container_from_map(tpl_args, frame, container, c.item_infobox_groups, container_type)
             },
    end
        },
   
         {
    if tpl_args.skill_icon ~= nil then
            show = true, -- TODO: Show only if has damage_effectiveness
        container:wikitext(string.format('[[%s]]', tpl_args.skill_icon))
            func = core.factory.infobox_line{
    end
                type = 'gem',
   
                parts = {
    return container
                    {
end
                        key = 'damage_effectiveness',
 
                        hide_default = 100,
--
                        fmt = '%i',
-- Factory
                        color = false,
--
                        inline = '%s%%',
 
                        inline_color = 'value',
h.factory = {}
                    },
 
                },
function h.factory.args_present(...)
                fmt = i18n.tooltips.damage_effectiveness,
    local args = {...}
            },
    return function (tpl_args)
        },
         for _, k in ipairs(args) do
         {
             if tpl_args[k] == nil then
            show = h.factory.args_present('projectile_speed'),
                 return false
             func = core.factory.infobox_line{
            elseif type(tpl_args[k]) == 'table' and #tpl_args[k] == 0 then
                 parts = {
                 return false
                    {
             end
                        key = 'projectile_speed',
         end
                    },
         return true
                },
    end
                 fmt = i18n.tooltips.projectile_speed,
end
             },
 
         },
function h.factory.display_raw_value(key)
         {
    return function(tpl_args, frame)
            show = h.factory.args_present('radius'),
        return tpl_args[key]
            func = core.factory.infobox_line{
    end
                parts = {
end
                    {
 
                        key = 'radius',
function h.factory.descriptor_value(args)
                        func = h.factory.descriptor_value{key='radius_description'},
    -- Arguments:
                    },
    --  key
                    {
    --  tbl
                        key = 'radius_secondary',
    args = args or {}
                        func = h.factory.descriptor_value{key='radius_secondary_description'},
    return function (tpl_args, frame, value)
                    },
        args.tbl = args.tbl or tpl_args
                    {
        if args.tbl[args.key] then
                        key = 'radius_tertiary',
            value = m_util.html.abbr(value, args.tbl[args.key])
                        func = h.factory.descriptor_value{key='radius_tertiary_description'},
        end
                    },
         return value
                },
    end
                sep = ' / ',
end
                fmt = i18n.tooltips.radius,
 
            },
-- ----------------------------------------------------------------------------
         },
-- Additional configuration
        -- Quality is before item stats, but after gem stuff and item class
-- ----------------------------------------------------------------------------
        {
 
            show = h.factory.args_present('quality'),
-- helper to loop over the range variables easier
            func = core.factory.infobox_line{
c.range_map = {
                parts = {
    min = {
                    {
        var = '_range_minimum',
                        key = 'quality',
    },
                        fmt = '+%i%%',
    max = {
                        color = 'mod',
        var = '_range_maximum',
                        hide_default = 0,
    },
                    },
    avg = {
                },
        var = '_range_average',
                fmt = i18n.tooltips.quality,
    },
            },
}
        },
 
         -- Weapon only
--
         {
-- Contents here are meant to resemble the ingame infobox of items
             show = h.factory.args_present('physical_damage_html'),
--
             func = core.factory.infobox_line{
c.item_infobox_groups = {
                 parts = {
    -- [n]:
                     {
    --  class: Additional css class added to group tag
                         key = 'physical_damage_html',
    --  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',
                         fmt = '%s',
                        color = 'default'
                     },
                     },
                 },
                 },
             },
                fmt = i18n.tooltips.physical_damage,
             },      
         },
         },
        -- Weapon type
         {
         {
             show = function (tpl_args, frame)
             show = true, -- Elemental Damage
                 if tpl_args.class_id == nil then  
            func = function (tpl_args, frame)
                     return false
                 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
                 end
                 return cfg.class_groups.weapons.keys[tpl_args.class_id] ~= nil
                 return
            end,
             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)
             show = h.factory.args_present('chaos_damage_html'),
                 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 = 'chaos_damage_html',
             end,
                        fmt = '%s',
                        color = false, -- html already has color
                    },
                },
                 fmt = i18n.tooltips.chaos_damage,
             },      
         },
         },
         {
         {
             show = h.factory.args_present('gem_tags'),
             show = h.factory.args_present('critical_strike_chance_html'),
             func = function (tpl_args, frame)
             func = core.factory.infobox_line{
                 local out = {}
                 parts = {
                for i, tag in ipairs(tpl_args.gem_tags) do
                    {
                    out[#out+1] = string.format(i18n.gem_tag_category, tag, tag)
                        key = 'critical_strike_chance_html',
                 end
                        fmt = '%s',
                 return table.concat(out, ', ')
                    },
             end,
                 },
                 fmt = i18n.tooltips.critical_strike_chance,
             },
         },
         },
         {
         {
             show = h.factory.args_present('support_gem_letter_html'),
             show = h.factory.args_present('attack_speed_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'support_gem_letter_html',
                         key = 'attack_speed_html',
                        fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.support_icon,
                 fmt = i18n.tooltips.attacks_per_second,
             },
             },
         },
         },
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('weapon_range_html'),
                return tpl_args.skill_levels and cfg.class_groups.gems.keys[tpl_args.class_id]
             func = core.factory.infobox_line{
            end,
                 parts = {
             func = function (tpl_args, frame)
                     {
                 local value = {
                        key = 'weapon_range_html',
                     base = 1,
                        fmt = '%s',
                    min = 1,
                     },
                     max = tpl_args.max_level,
                 },
                 }
                 fmt = i18n.tooltips.weapon_range,
                 local options = {
             },
                    color = 'value',
                }
                return string.format(
                    i18n.tooltips.level,
                    m_util.html.format_value(tpl_args, frame, value, options)
                )
             end,
         },
         },
        -- Map only
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('map_area_level'),
                return tpl_args.skill_costs
             func = core.factory.infobox_line{
            end,
                 parts = {
             func = function (tpl_args, frame)
                     {
                 local parts = {}
                         key = 'map_area_level',
                if not tpl_args.skill_costs.has_spending_cost then
                        fmt = '%i',
                     -- Try falling back to deprecated parameters
                    },
                    if not tpl_args.has_reservation_mana_cost then
                },
                         parts[1] = {
                fmt = i18n.tooltips.map_level,
                            key = 'mana_cost',
            },
                            fmt = '%i',
        },
                            color = false,
        {
                            inline = string.format('%%s %s', m_game.constants.skill.cost_types.mana.long_upper),
            show = h.factory.args_present('map_tier'),
                            inline_color = 'value',
            func = core.factory.infobox_line{
                        }
                parts = {
                    end
                    {
                else
                        key = 'map_tier',
                    for i=1, #tpl_args.skill_costs do
                        fmt = '%i',
                        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] = {
                fmt = i18n.tooltips.map_tier,
                                key = {'costs', i, 'amount'},
            },
                                fmt = '%i',
        },
                                color = false,
        {
                                inline = string.format(
            show = function (tpl_args, frame)
                                    '%%s%s %s',
                 return tpl_args.map_guild_character ~= nil and tpl_args.rarity_id == 'normal'
                                    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,
             end,
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'map_guild_character',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.map_guild_character,
            },
         },
         },
         {
         {
             show = function (tpl_args, frame)
             show = function (tpl_args, frame)
                 return tpl_args.skill_costs
                 return tpl_args.unique_map_guild_character ~= nil and tpl_args.rarity_id == 'unique'
            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,
             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, -- TODO: Show only if has mana_multiplier
             show = true,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 type = 'gem',
                 type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'mana_multiplier',
                         key = 'map_item_drop_rarity_+%',
                        hide_default = 100,
                         fmt = '+%i%%',
                         fmt = '%i',
                         color = 'mod',
                         color = false,
                         hide_default = 0,
                        inline = '%s%%',
                         inline_color = 'value',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.mana_multiplier,
                 fmt = i18n.tooltips.item_rarity,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has cooldown
             show = true,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 type = 'gem',
                 type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'cooldown',  
                         key = 'map_pack_size_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                         hide_default = 0,
                         hide_default = 0,
                        fmt = '%.2f ' .. m_game.units.seconds.short_lower,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.cooldown_time,
                 fmt = i18n.tooltips.monster_pack_size,
             },
             },
         },
         },
        -- Jewel Only
         {
         {
            -- 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,
             show = true,
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'stored_uses',
                         key = 'item_limit',
                        hide_default = 0,
                         fmt = '%i',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.stored_uses,
                 fmt = i18n.tooltips.limited_to,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has vaal_souls_requirement
             show = h.factory.args_present('jewel_radius_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'vaal_souls_requirement',
                         key = 'jewel_radius_html',
                         hide_default = 0,
                         fmt = '%s',
                         fmt = '%i',
                         color = false, -- html already has color
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.vaal_souls_per_use,
                 fmt = i18n.tooltips.radius,
             },
             },
         },
         },
        -- Flask only
         {
         {
             show = true, -- TODO: Show only if has vaal_stored_uses
             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 = 'vaal_stored_uses',
                         key = 'flask_mana_html',
                         hide_default = 0,
                         fmt = '%s',
                         fmt = '%i',
                    },
                    {
                        key = 'flask_duration_html',
                         fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.stored_uses, -- TODO: Singular or plural based on number
                 fmt = i18n.tooltips.flask_mana_recovery,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has vaal_soul_gain_prevention_time
             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 = 'vaal_soul_gain_prevention_time',
                         key = 'flask_life_html',
                         hide_default = 0,
                         fmt = '%s',
                         -- Technically it rounds to nearest, but it is given in milliseconds in the data,  
                    },
                         fmt = '%i ' .. m_game.units.seconds.short_lower,
                    {
                         key = 'flask_duration_html',
                         fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.vaal_soul_gain_prevention_time,
                 fmt = i18n.tooltips.flask_life_recovery,
             },
             },
         },
         },
         {
         {
            -- don't display for mana/life flasks
             show = function (tpl_args, frame)
             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))
                 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,
             end,
             func = function (tpl_args, frame)
             func = core.factory.infobox_line{
                return core.factory.infobox_line{
                parts = {
                    parts = {
                    {
                        {
                        key = 'flask_duration_html',
                            key = 'cast_time',
                        fmt = '%s',
                            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.flask_duration,
            end,
            },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has critical_strike_chance
             show = h.factory.args_present('charges_per_use_html', 'charges_max_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'critical_strike_chance',
                         key = 'charges_per_use_html',
                         hide_default = 0,
                         fmt = '%s',
                         fmt = '%.2f%%',
                    },
                    {
                        key = 'charges_max_html',
                         fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.critical_strike_chance,
                 fmt = i18n.tooltips.flask_charges_per_use,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has attack_speed_multiplier
             show = h.factory.args_present('buff_stat_text'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'attack_speed_multiplier',
                         key = 'buff_stat_text',
                        hide_default = 100,
                         color = 'mod',
                        fmt = '%i',
                         color = false,
                        inline = '%s%% ' .. i18n.tooltips.of_base_stat,
                        inline_color = 'value',
                     },
                     },
                 },
                 },
                fmt = i18n.tooltips.attack_speed_multiplier,
             },
             },
         },
         },
        -- Armor only
         {
         {
             show = true, -- TODO: Show only if has damage_multiplier
             show = h.factory.args_present('block_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'damage_multiplier',
                         key = 'block_html',
                        hide_default = 100,
                         fmt = '%s',
                         fmt = '%i',
                         hide_default = 0,
                         color = false,
                         hide_default_key = 'block',
                        inline = '%s%% ' .. i18n.tooltips.of_base_stat,
                         inline_color = 'value',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.damage_multiplier,
                 fmt = i18n.tooltips.chance_to_block,
             },
             },
         },
         },
         {
         {
             show = true, -- TODO: Show only if has damage_effectiveness
             show = h.factory.args_present('armour_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'gem',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'damage_effectiveness',
                         key = 'armour_html',
                        hide_default = 100,
                         fmt = '%s',
                         fmt = '%i',
                         hide_default = 0,
                         color = false,
                         hide_default_key = 'armour',
                        inline = '%s%%',
                         inline_color = 'value',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.damage_effectiveness,
                 fmt = i18n.tooltips.armour,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('projectile_speed'),
             show = h.factory.args_present('evasion_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'projectile_speed',
                         key = 'evasion_html',
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'evasion',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.projectile_speed,
                 fmt = i18n.tooltips.evasion,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('radius'),
             show = h.factory.args_present('energy_shield_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'radius',
                         key = 'energy_shield_html',
                         func = h.factory.descriptor_value{key='radius_description'},
                         fmt = '%s',
                    },
                         hide_default = 0,
                    {
                         hide_default_key = 'energy_shield',
                         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.energy_shield,
                 fmt = i18n.tooltips.radius,
             },
             },
         },
         },
        -- Quality is before item stats, but after gem stuff and item class
         {
         {
             show = h.factory.args_present('quality'),
             show = h.factory.args_present('movement_speed'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'quality',
                         key = 'movement_speed',
                         fmt = '+%i%%',
                         fmt = '%s%%',
                        color = 'mod',
                         hide_default = 0,
                         hide_default = 0,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.quality,
                 fmt = i18n.tooltips.movement_speed,
             },
             },
         },
         },
         -- Weapon only
         -- Amulet only
         {
         {
             show = h.factory.args_present('physical_damage_html'),
             show = h.factory.args_present('talisman_tier'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'physical_damage_html',
                         key = 'talisman_tier',
                         fmt = '%s',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.physical_damage,
                 fmt = i18n.tooltips.talisman_tier,
             },      
             },
         },
         },
        -- Misc
         {
         {
            show = true, -- Elemental Damage
             show = h.factory.args_present('stack_size'),
            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 = 'stack_size',
                         fmt = '%s',
                         fmt = '%i',
                         color = false, -- html already has color
                         hide_default = 1,
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.chaos_damage,
                 fmt = i18n.tooltips.stack_size,
             },      
             },
         },
         },
        -- Essence stuff
         {
         {
             show = h.factory.args_present('critical_strike_chance_html'),
             show = h.factory.args_present('essence_level'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'critical_strike_chance_html',
                         key = 'essence_level',
                         fmt = '%s',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.critical_strike_chance,
                 fmt = i18n.tooltips.essence_level,
             },
             },
         },
         },
        -- Blight items
         {
         {
             show = h.factory.args_present('attack_speed_html'),
             show = h.factory.args_present('blight_item_tier'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'attack_speed_html',
                         key = 'blight_item_tier',
                         fmt = '%s',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.attacks_per_second,
                 fmt = i18n.tooltips.blight_item_tier,
             },
             },
         },
         },
        -- Harvest seeds (upper section)
         {
         {
             show = h.factory.args_present('weapon_range_html'),
             show = h.factory.args_present('seed_tier'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'weapon_range_html',
                         key = 'seed_tier',
                         fmt = '%s',
                         fmt = '%i',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.weapon_range,
                 fmt = i18n.tooltips.seed_tier,
             },
             },
         },
         },
        -- Map only
         {
         {
             show = h.factory.args_present('map_area_level'),
             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{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_area_level',
                         key = 'seed_type_html',
                         fmt = '%i',
                         fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.map_level,
                 fmt = i18n.tooltips.seed_lifeforce_gained,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('map_tier'),
             show = h.factory.args_present('seed_growth_cycles'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_tier',
                         key = 'seed_growth_cycles',
                        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 = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.map_guild_character,
                 fmt = i18n.tooltips.seed_growth_cycles,
             },
             },
         },
         },
        -- Heist
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('heist_required_npcs'),
                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 = 'heist_required_npcs',
                         fmt = '%s',
                         fmt = '%s',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.map_guild_character,
                 fmt = i18n.tooltips.heist_required_npc,
             },
             },
         },
         },
    },
    -- Requirements
    {
         {
         {
             show = true,
             show = h.factory.args_present('master', 'master_level_requirement'),
             func = core.factory.infobox_line{
             func = function (tpl_args, frame)
                 type = 'stat',
                 local data
                 parts = {
                 for i, rowdata in ipairs(m_game.constants.masters) do
                     {
                     if tpl_args.master == rowdata.full then
                        key = 'map_item_drop_quantity_+%',
                         data = rowdata
                        fmt = '+%i%%',
                         break
                         color = 'mod',
                     end
                         hide_default = 0,
                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))
                 fmt = i18n.tooltips.item_quantity,
             end
             },
         },
         },
        -- Instead of item level, show drop level if any
         {
         {
             show = true,
             show = true, -- Requires...
             func = core.factory.infobox_line{
             func = function (tpl_args, frame)
                type = 'stat',
                 local parts = {
                 parts = {
                     {
                     {
                         key = 'map_item_drop_rarity_+%',
                         key = 'required_level_final_html',
                         fmt = '+%i%%',
                         hide_default = 1,
                         color = 'mod',
                        hide_default_key = 'required_level_final',
                         hide_default = 0,
                         inline = i18n.tooltips.level_inline,
                         inline_color = false,
                     },
                     },
                 },
                 }
                 fmt = i18n.tooltips.item_rarity,
                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 = true,
             show = h.factory.args_present('heist_required_job', 'heist_required_job_level'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                type = 'stat',
                 parts = {
                 parts = {
                     {
                     {
                         key = 'map_pack_size_+%',
                         key = 'heist_required_job_level',
                         fmt = '+%i%%',
                         fmt = '%s',
                        color = 'mod',
                        hide_default = 0,
                     },
                     },
                },
                fmt = i18n.tooltips.monster_pack_size,
            },
        },
        -- Jewel Only
        {
            show = true,
            func = core.factory.infobox_line{
                parts = {
                     {
                     {
                         key = 'item_limit',
                         key = 'heist_required_job',
                        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',
                         fmt = '%s',
                        color = false, -- html already has color
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.radius,
                 fmt = i18n.tooltips.heist_required_job,
             },
             },
         },
         },
         -- Flask only
    },
    -- Gem description
    {
         class = 'tc -gemdesc',
         {
         {
             show = h.factory.args_present('flask_mana_html', 'flask_duration_html'),
             show = h.factory.args_present('gem_description'),
             --func = core.factory.display_flask('flask_mana'),
             func = h.factory.display_raw_value('gem_description'),
            func = core.factory.infobox_line{
                parts = {
                    {
                        key = 'flask_mana_html',
                        fmt = '%s',
                    },
                    {
                        key = 'flask_duration_html',
                        fmt = '%s',
                    },
                },
                fmt = i18n.tooltips.flask_mana_recovery,
            },
         },
         },
    },
    -- Gem Quality Stats
    {
        class = 'tc -mod',
         {
         {
             show = h.factory.args_present('flask_life_html', 'flask_duration_html'),
             show = h.factory.args_present('skill_quality'),
             func = core.factory.infobox_line{
             func = function (tpl_args, frame)
                 parts = {
                local span = mw.html.create('span')
                     {
                 span
                        key = 'flask_life_html',
                     :addClass('quality')
                        fmt = '%s',
                   
                     },
                local span2 = span:tag('span')
                     {
                span2
                        key = 'flask_duration_html',
                     :addClass('quality-header')
                         fmt = '%s',
                     :tag('span')
                     },
                         :wikitext(m_util.html.poe_color('default', i18n.tooltips.gem_quality))
                 },
                      
                fmt = i18n.tooltips.flask_life_recovery,
                 for i, quality_data in ipairs(tpl_args.skill_quality) do
            },
                    local span_inner = span2:tag('span')
        },
                    span_inner
        {
                        :addClass('quality-section-select')
            -- don't display for mana/life flasks
                        :addClass('quality-' .. i)
            show = function (tpl_args, frame)
                        :wikitext(i)
                for _, k in ipairs({'flask_life_html', 'flask_mana_html'}) do
                        :tag('span')
                     if tpl_args[k] ~= nil then
                            :wikitext(i18n.tooltips['gem_quality_' .. i])
                         return false
                           
                     if i == 1 then
                         span_inner:addClass('quality-selected')
                     end
                     end
                 end
                 end
                 return tpl_args['flask_duration_html'] ~= nil
                  
            end,
                for i, quality_data in ipairs(tpl_args.skill_quality) do
            func = core.factory.infobox_line{
                    local span_inner = span:tag('span')
                parts = {
                     span_inner
                     {
                         :addClass('quality-box')
                         key = 'flask_duration_html',
                        :addClass('quality-' .. i)
                         fmt = '%s',
                         :wikitext(quality_data.stat_text)
                    },
                   
                 },
                    if i == 1 then
                 fmt = i18n.tooltips.flask_duration,
                        span_inner:addClass('quality-selected')
             },
                    end
                end
                  
                 return tostring(span)
             end,
         },
         },
    },
    -- Gem Implicit Stats
    {
        class = 'tc -mod',
         {
         {
             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{
                parts = {
                    {
                        key = 'buff_stat_text',
                        color = 'mod',
                    },
                },
            },
        },
        -- Armor only
        {
            show = h.factory.args_present('block_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'block_html',
                         key = 'experience',
                         fmt = '%s',
                         fmt = '%i',
                        hide_default = 0,
                        hide_default_key = 'block',
                     },
                     },
                 },
                 },
                fmt = i18n.tooltips.chance_to_block,
             },
             },
         },
         },
    },]]--
    -- Harvest seeds (lower section)
    {
        class = 'tc -mod',
         {
         {
             show = h.factory.args_present('armour_html'),
             show = h.factory.args_present('seed_consumed_wild_lifeforce_percentage'),
             func = core.factory.infobox_line{
             func = function (tpl_args, frame)
                 parts = {
                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))
                        key = 'armour_html',
                 end
                        fmt = '%s',
            end
                        hide_default = 0,
        },
                        hide_default_key = 'armour',
        {
                    },
            show = h.factory.args_present('seed_consumed_vivid_lifeforce_percentage'),
                 },
            func = function (tpl_args, frame)
                fmt = i18n.tooltips.armour,
                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('evasion_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 = 'evasion_html',
                         key = 'seed_required_nearby_seed_amount',
                        fmt = '%s',
                    },
                    {
                        key = 'seed_type_html',
                        fmt = '%s',
                    },
                    {
                        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,
             },
             },
        },]]
    },
    {
        class = 'tc -crafted',
        {
            show = h.factory.args_present('incubator_effect'),
            func = h.factory.display_raw_value('incubator_effect'),
         },
         },
         -- Misc
    },
    -- Variations (for doodads)
    {
         class = 'tc -mod',
         {
         {
             show = h.factory.args_present('stack_size'),
             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 = 'stack_size',
                     txt = i18n.tooltips.variation_singular
                        fmt = '%i',
                 else
                        hide_default = 1,
                    txt = i18n.tooltips.variation_plural
                     },
                end
                 },
                return string.format('%i %s', tpl_args.variation_count, txt)
                fmt = i18n.tooltips.stack_size,
             end,
             },
         },
         },
         -- Essence stuff
    },
    -- Flavour Text
    {
         class = 'tc -flavour',
         {
         {
             show = h.factory.args_present('essence_level'),
             show = h.factory.args_present('flavour_text'),
             func = core.factory.infobox_line{
             func = h.factory.display_raw_value('flavour_text'),
                parts = {
                    {
                        key = 'essence_level',
                        fmt = '%i',
                    },
                },
                fmt = i18n.tooltips.essence_level,
            },
         },
         },
         -- Blight items
    },
    -- Prophecy text
    {
         class = 'tc -value',
         {
         {
             show = h.factory.args_present('blight_item_tier'),
             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{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'blight_item_tier',
                         key = 'master_favour_cost',
                         fmt = '%i',
                         color = 'currency',
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.blight_item_tier,
                 fmt = i18n.tooltips.favour_cost,
             },
             },
         },
         },
        -- Harvest seeds (upper section)
         {
         {
             show = h.factory.args_present('seed_tier'),
             show = h.factory.args_present('seal_cost'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'seed_tier',
                         key = 'seal_cost',
                         fmt = '%i',
                         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_tier,
                 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('seed_tier'),
             show = h.factory.args_present('atlas_connections'),
             func = function (tpl_args, value)
             func = function (tpl_args, frame)
                 return i18n.tooltips.seed_monster
                 local fields = {
            end,
                    [false] = {
        },
                        value = '',
        {
                         sort = 0,
            show = h.factory.args_present('seed_type_html'),
                         class = 'table-cell-xmark',
            func = core.factory.infobox_line{
                parts = {
                    {
                         key = 'seed_type_html',
                         fmt = '%s',
                     },
                     },
                },
                    [true] = {
                fmt = i18n.tooltips.seed_lifeforce_gained,
                        value = '',
            },
                         sort = 1,
        },
                         class = 'table-cell-checkmark',
        {
            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,
                  
            },
                local tbl = mw.html.create('table')
        },
                tbl
        -- Heist
                    :attr('class', 'wikitable')
        {
                    :attr('style', 'width:100%;')
            show = h.factory.args_present('heist_required_npcs'),
                    :tag('tr')
            func = core.factory.infobox_line{
                        :tag('th')
                parts = {
                            :attr('colspan', 6)
                     {
                            :attr('style', 'text-decoration: underline;')
                        key = 'heist_required_npcs',
                            :wikitext(i18n.tooltips.header_overall)
                         fmt = '%s',
                            :done()
                    },
                        :done()
                },
                     :tag('tr')
                fmt = i18n.tooltips.heist_required_npc,
                         :tag('th')
            },
                            :wikitext(i18n.tooltips.header_upgrades)
        },
                            :done()
    },
                        :tag('th')
    -- Requirements
                            :wikitext(0)
    {
                            :done()
        {
                        :tag('th')
            show = h.factory.args_present('master', 'master_level_requirement'),
                            :wikitext(1)
            func = function (tpl_args, frame)
                            :done()
                local data
                        :tag('th')
                 for i, rowdata in ipairs(m_game.constants.masters) do
                            :wikitext(2)
                    if tpl_args.master == rowdata.full then
                            :done()
                         data = rowdata
                        :tag('th')
                         break
                            :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
                     end
                    tr:done()
                 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))
                 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
             end
         },
         },
        -- Instead of item level, show drop level if any
    },
    -- Drop info
    {
        heading = i18n.tooltips.drop_restrictions,
        class = '',
         {
         {
             show = true, -- Requires...
             show = h.factory.args_present('drop_enabled'),
             func = function (tpl_args, frame)
             func = core.factory.infobox_line{
                 local parts = {
                 parts = {
                    {
                        key = 'drop_level',
                        fmt = '%i',
                       
                    },
                     {
                     {
                         key = 'required_level_final_html',
                         key = 'drop_level_maximum',
                         hide_default = 1,
                         fmt = '%i',
                        hide_default_key = 'required_level_final',
                         hide_default = 100,
                         inline = i18n.tooltips.level_inline,
                        inline_color = false,
                     },
                     },
                 }
                 },
                 for _, attr in ipairs(m_game.constants.attribute_order) do
                 sep = ' / ',
                    parts[#parts+1] = {
                fmt = i18n.tooltips.level,
                        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
            show = function (tpl_args, frame)
                        end,
                if tpl_args.drop_enabled == false then
                        hide_key = string.format('required_%s', attr),
                     return true
                        inline = ', %s ' .. m_game.constants.attributes[attr].short_upper,
                        inline_color = false,
                     }
                 end
                 end
                 local requirements = core.factory.infobox_line{parts = parts}(tpl_args, frame)
                 return false
                 if requirements == nil then -- return early
            end,
                    return
            func = function (tpl_args, frame)
                 end
                 local span = mw.html.create('span')
                requirements = string.gsub(requirements, '^, ', '')
                 span
                return string.format(i18n.tooltips.requires, requirements)
                    :attr('class', 'infobox-disabled-drop')
                    :wikitext(i18n.tooltips.drop_disabled)
                    :done()
                return tostring(span)
             end,
             end,
         },
         },
         {
         {
             show = h.factory.args_present('heist_required_job', 'heist_required_job_level'),
             show = function (tpl_args, frame)
            func = core.factory.infobox_line{
                if tpl_args.is_drop_restricted == true and tpl_args.drop_enabled ~= false then
                 parts = {
                    return true
                    {
                end
                        key = 'heist_required_job_level',
                 return false
                        fmt = '%s',
            end,
                    },
            func = function (tpl_args, frame)
                     {
                local span = mw.html.create('span')
                        key = 'heist_required_job',
                span
                        fmt = '%s',
                     :attr('class', 'infobox-restricted-drop')
                     },
                     :wikitext(i18n.tooltips.drop_restricted)
                },
                    :done()
                fmt = i18n.tooltips.heist_required_job,
                return tostring(span)
             },
             end,
         },
         },
    },
    -- Gem description
    {
        class = 'tc -gemdesc',
         {
         {
             show = h.factory.args_present('gem_description'),
             show = h.factory.args_present('drop_leagues'),
             func = h.factory.display_raw_value('gem_description'),
             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
         },
         },
    },
    -- Gem Quality Stats
    {
        class = 'tc -mod',
         {
         {
             show = h.factory.args_present('skill_quality'),
             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'),
        },
    },
    {
        -- Vendor prices
        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)
             func = function (tpl_args, frame)
                 local span = mw.html.create('span')
                 local tbl = mw.html.create('table')
                 span
                 tbl
                     :addClass('quality')
                     --:attr('class', 'wikitable')
                    :attr('style', 'width: 100%; margin-top: 0px;')
                      
                      
                local span2 = span:tag('span')
                 for _, rarity_id in ipairs(m_game.constants.rarity_order) do
                span2
                     local data = tpl_args.purchase_costs[rarity_id]
                    :addClass('quality-header')
                     if #data > 0 then
                    :tag('span')
                         local tr = tbl:tag('tr')
                        :wikitext(m_util.html.poe_color('default', i18n.tooltips.gem_quality))
                         tr
                   
                            :tag('td')
                 for i, quality_data in ipairs(tpl_args.skill_quality) do
                                :wikitext(m_game.constants.rarities[rarity_id].long_upper)
                     local span_inner = span2:tag('span')
                         local td = tr:tag('td')
                     span_inner
                        for _, purchase_data in ipairs(data) do
                         :addClass('quality-section-select')
                             td:wikitext(string.format('%dx [[%s]]<br />', purchase_data.amount, purchase_data.name))
                         :addClass('quality-' .. i)
                         end
                        :wikitext(i)
                         :tag('span')
                             :wikitext(i18n.tooltips['gem_quality_' .. i])
                           
                    if i == 1 then
                         span_inner:addClass('quality-selected')
                     end
                     end
                 end
                 end
                  
                  
                for i, quality_data in ipairs(tpl_args.skill_quality) do
                 return tostring(tbl)
                    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,
         },
         },
     },
     },
    -- Gem Implicit Stats
     {
     {
         class = 'tc -mod',
         -- Vendor offer
        heading = i18n.tooltips.sell_price,
         {
         {
             show = function (tpl_args, frame)
             show = h.factory.args_present('sell_price_order'),
                return cfg.class_groups.gems.keys[tpl_args.class_id] and tpl_args.stat_text
            end,
             func = function (tpl_args, frame)
             func = function (tpl_args, frame)
                 lines = {}
                 local out = {}
                 lines[#lines+1] = tpl_args.stat_text
                 if #tpl_args.sell_price_order > 0 then
                for _, tag in ipairs(tpl_args.gem_tags) do
                    for _, item_name in ipairs(tpl_args.sell_price_order) do
                    if tag == m_game.constants.item.gem_tags.vaal.tag then
                         out[#out+1] = string.format('%dx [[%s]]', tpl_args.sell_prices[item_name], item_name)
                         lines[#lines+1] = i18n.tooltips.corrupted
                        break
                     end
                     end
                 end
                 end
                 return table.concat(lines, '<br>')
                 return table.concat(out, '<br />')
             end,
             end,
         },
         },
     },
     },
     -- Implicit Stats
     -- Damage per second
     {
     {
         class = 'tc -mod',
         heading = i18n.tooltips.damage_per_second,
         func = function (tpl_args, frame, container_type)
         {
             if tpl_args.implicit_stat_text then
             show = h.factory.args_present('physical_dps_html'),
                return {h.strip_random_stats(tpl_args, frame, tpl_args.implicit_stat_text, container_type)}
             func = core.factory.infobox_line{
             else
                 parts = {
                 return {}
                    {
            end
                        key = 'physical_dps_html',
        end,
                        fmt = '%s',
    },
                        color = false, -- the html already contains the colour
    -- Stats
                    },
    {
                },
        class = 'tc -mod',
                 fmt = i18n.tooltips.physical_dps,
        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 = h.factory.args_present('fire_dps_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'experience',
                         key = 'fire_dps_html',
                         fmt = '%i',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                 },
                 },
                fmt = i18n.tooltips.fire_dps,
             },
             },
         },
         },
    },]]--
    -- Harvest seeds (lower section)
    {
        class = 'tc -mod',
         {
         {
             show = h.factory.args_present('seed_consumed_wild_lifeforce_percentage'),
             show = h.factory.args_present('cold_dps_html'),
             func = function (tpl_args, frame)
             func = core.factory.infobox_line{
                 if tpl_args.seed_consumed_wild_lifeforce_percentage > 0 then
                 parts = {
                     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
                        key = 'cold_dps_html',
             end
                        fmt = '%s',
                        color = false, -- the html already contains the colour
                    },
                },
                fmt = i18n.tooltips.cold_dps,
             },
         },
         },
         {
         {
             show = h.factory.args_present('seed_consumed_vivid_lifeforce_percentage'),
             show = h.factory.args_present('lightning_dps_html'),
            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{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'seed_required_nearby_seed_amount',
                         key = 'lightning_dps_html',
                        fmt = '%s',
                    },
                    {
                        key = 'seed_type_html',
                         fmt = '%s',
                         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 = 'seed_required_nearby_seed_tier',
                         key = 'chaos_dps_html',
                         fmt = '%s',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.seed_required_seeds,
                 fmt = i18n.tooltips.chaos_dps,
                color = 'mod',
             },
             },
         },
         },
    },
    {
        class = 'tc -crafted',
         {
         {
             show = h.factory.args_present('seed_effect'),
             show = h.factory.args_present('elemental_dps_html'),
            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 = 'elemental_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.elemental_dps,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('plant_booster_extra_chances'),
             show = h.factory.args_present('poison_dps_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'plant_booster_extra_chances',
                         key = 'poison_dps_html',
                         fmt = '%s%%',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.plant_booster_extra_chances,
                 fmt = i18n.tooltips.poison_dps,
             },
             },
         },
         },
         {
         {
             show = h.factory.args_present('plant_booster_lifeforce'),
             show = h.factory.args_present('dps_html'),
             func = core.factory.infobox_line{
             func = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
                         key = 'plant_booster_lifeforce',
                         key = 'dps_html',
                         fmt = '%s%%',
                         fmt = '%s',
                        color = false, -- the html already contains the colour
                     },
                     },
                 },
                 },
                 fmt = i18n.tooltips.plant_booster_lifeforce,
                 fmt = i18n.tooltips.dps,
             },
             },
        },]]
    },
    {
        class = 'tc -crafted',
        {
            show = h.factory.args_present('incubator_effect'),
            func = h.factory.display_raw_value('incubator_effect'),
         },
         },
     },
     },
    -- Variations (for doodads)
     {
     {
         class = 'tc -mod',
         heading = i18n.tooltips.misc,
         {
         {
             show = h.factory.args_present('variation_count'),
             show = h.factory.args_present('class'),
             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 = 'class',
                else
                        fmt = '%s',
                    txt = i18n.tooltips.variation_plural
                     },
                end
                },
                 return string.format('%i %s', tpl_args.variation_count, txt)
                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,
             end,
         },
         },
     },
     },
    -- Flavour Text
}
    {
 
        class = 'tc -flavour',
-- ----------------------------------------------------------------------------
        {
-- Subroutines
            show = h.factory.args_present('flavour_text'),
-- ----------------------------------------------------------------------------
            func = h.factory.display_raw_value('flavour_text'),
 
         },
local s = {}
    },
 
    -- Prophecy text
-- Subroutines for p.item. These exist to declutter the main function. Each one of these is only called once.
    {
 
         class = 'tc -value',
function s.get_item_config(tpl_args, frame)
        {
    -- Returns primary item configuration, based on item class
            show = h.factory.args_present('prediction_text'),
 
            func = h.factory.display_raw_value('prediction_text'),
    h.process_arguments(tpl_args, frame, {'class_id'})
         },
    local config = {
     },
         tables = {},
     -- Can not be traded or modified
        args = {},
     {
        late_args = {},
         class = 'tc -canttradeormodify',
         defaults = {},
        {
    }
             show = h.factory.args_present('cannot_be_traded_or_modified'),
    for _, v in ipairs(cfg.tables) do
             func = function (tpl_args, frame)
        table.insert(config.tables, v)
                if tpl_args.cannot_be_traded_or_modified == true then
    end
                     return i18n.tooltips.cannot_be_traded_or_modified
    for _, v in ipairs(cfg.default_args) do
        table.insert(config.args, v)
    end
    for _, v in ipairs(cfg.late_args) do
         table.insert(config.late_args, v)
     end
     local extend_keys = {'tables', 'args', 'late_args'} -- We will do defaults separately to preserve order of args
     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
             if row['defaults'] then
                for k, v in pairs(row['defaults']) do
                    config['defaults'][k] = v
                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
         },
         end
    },
         if class_specifics['defaults'] then
    -- Help text
             for k, v in pairs(class_specifics['defaults']) do
    {
                config['defaults'][k] = v
         class = 'tc -help',
             end
        {
        end
             show = h.factory.args_present('help_text'),
    end
             func = h.factory.display_raw_value('help_text'),
    return config
        },
end
     },
 
     -- Cost (i.e. vendor costs)
function s.process_quest_rewards(tpl_args, frame)
     {
    local rid = 1
         --class = '',
    local continue
         {
     tpl_args.quest_rewards = {}
            show = h.factory.args_present('master_favour_cost'),
     tpl_args.vendor_rewards = {}
             func = core.factory.infobox_line{
     repeat
                 parts = {
         continue = true
                    {
         local prefix = string.format('quest_reward%s_', rid)
                        key = 'master_favour_cost',
       
                        color = 'currency',
        local input_args = {
                    },
             shared = {
                 ['type'] = true,
                ['quest'] = false,
                ['quest_id'] = false,  
                ['act'] = true,  
                ['class_ids'] = false,
                 },
                 },
                 fmt = i18n.tooltips.favour_cost,
            vendor = {
                ['npc'] = true,
            },
            quest = {
                ['sockets'] = false,
                ['item_level'] = false,
                ['rarity_id'] = false,
                 ['notes'] = false,
             },
             },
         },
         }
         {
          
            show = h.factory.args_present('seal_cost'),
        local rdata = {}
             func = core.factory.infobox_line{
       
                 parts = {
        for key, is_required in pairs(input_args.shared) do
                     {
             rdata[key] = tpl_args[prefix .. key]
                        key = 'seal_cost',
            if is_required then
                        fmt = function (tpl_args, frame, value)
                 if rdata[key] == nil then
                            return '%dx ' .. h.item_link{metadata_id='Metadata/Items/Currency/CurrencySilverCoin', html=''}
                    continue = false
                        end,
                     break
                        color = 'currency',
                end
                    },
            end
                 },
        end
                fmt = i18n.tooltips.seal_cost,
       
            },
        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
-- This is meant to show additional information about the item in a separate infobox
                 rdata[key] = tpl_args[prefix .. key]
--
                if is_required then
c.extra_display_groups = {
                    if rdata[key] == nil then
    {
                        continue = false
         heading = i18n.tooltips.extra_info,
                        break
         class = '',
                    end
        {
                end
             show = h.factory.args_present('atlas_connections'),
            end
            func = function (tpl_args, frame)
        else
                local fields = {
            continue = false
                    [false] = {
        end
                        value = '✗',
          
                        sort = 0,
         if continue then
                         class = 'table-cell-xmark',
            rdata.classes = {}
                     },
             if rdata.class_ids ~= nil then
                    [true] = {
                rdata.class_ids = m_util.string.split(rdata.class_ids, ',%s*')
                         value = '✓',
                for index, class_id in ipairs(rdata.class_ids) do
                        sort = 1,
                    local class = m_game.constants.characters[class_id]  
                        class = 'table-cell-checkmark',
                    if class == nil then
                     },
                         error(string.format('Class id %s is invalid', class_id))
                 }
                     else
               
                        rdata.class_ids[index] = class.str_id
                 local tbl = mw.html.create('table')
                         rdata.classes[index] = class.name
                 tbl
                     end
                     :attr('class', 'wikitable')
                 end
                    :attr('style', 'width:100%;')
            end
                    :tag('tr')
           
                        :tag('th')
           
                            :attr('colspan', 6)
            if rdata.item_level then
                            :attr('style', 'text-decoration: underline;')
                 rdata.item_level = m_util.cast.number(rdata.item_level)
                            :wikitext(i18n.tooltips.header_overall)
            end
                            :done()
           
                        :done()
            if rdata.rarity_id then
                    :tag('tr')
                 if m_game.constants.rarities[rdata.rarity_id] == nil then
                        :tag('th')
                     error(string.format(i18n.errors.invalid_rarity_id, tostring(rdata.rarity_id)))
                            :wikitext(i18n.tooltips.header_upgrades)
                end
                            :done()
            end
                        :tag('th')
           
                            :wikitext(0)
            rdata._table = rdata.type .. '_rewards'
                            :done()
            rdata.type = nil
                        :tag('th')
           
                            :wikitext(1)
            tpl_args[rdata._table] = rdata
                            :done()
           
                        :tag('th')
            m_cargo.store(frame, rdata)
                            :wikitext(2)
           
                            :done()
            -- TODO: Verify quests and quest ids?
                        :tag('th')
        end
                            :wikitext(3)
       
                            :done()
        rid = rid + 1
                        :tag('th')
    until continue == false
                            :wikitext(4)
end
                            :done()
 
                        :done()
function s.process_base_item(tpl_args, frame)
                   
    local where
                for _, vtype in ipairs({'tier', 'level'}) do
    if tpl_args.base_item_id ~= nil then
                    local tr = tbl:tag('tr')
        where = string.format('items.metadata_id="%s"', tpl_args.base_item_id)
                    tr
    elseif tpl_args.base_item_page ~= nil then
                        :tag('th')
        where = string.format('items._pageName="%s"' , tpl_args.base_item_page)
                            :wikitext(i18n.tooltips['header_map_' .. vtype])
    elseif tpl_args.base_item ~= nil then
                            :done()
        where = string.format('items.name="%s"' , tpl_args.base_item)
                   
    elseif tpl_args.rarity_id ~= 'normal' then
                    for i=0,4 do
        error(i18n.errors.missing_base_item)
                        local value = tpl_args['atlas_map_tier' .. i]
    else
                        if value == 0 then
        return
                            value = fields[false].value
    end
                        elseif vtype == 'level' then
   
                            value = value + 67
    if where ~= nil and tpl_args.rarity_id == 'normal' and not tpl_args._flags.is_prophecy then
                        end
        error(i18n.errors.missing_rarity)
                        tr
    end
                            :tag('td')
   
                                :wikitext(value)
    where = string.format('%s AND items.class_id="%s" AND items.rarity_id="normal"', where, tpl_args.class_id)
                                :done()
   
                     end
    local join = {}
                     tr:done()
   
                end
    for _, table_name in ipairs(tpl_args._item_config.tables) do
                  
        if table_name ~= 'items' then
                tbl
            join[#join+1] = string.format('items._pageID=%s._pageID', table_name)
                     :tag('tr')
        end
                        :tag('th')
    end
                            :attr('colspan', 6)
   
                            :attr('style', 'text-decoration: underline;')
    local fields = {
                            :wikitext(i18n.tooltips.header_connections)
        'items._pageName',
                            :done()
        'items.name',
                        :done()
        'items.metadata_id',
               
    }
                -- sort alphabetically
   
                local sorted = {}
    for _, k in ipairs(tpl_args._base_item_args) do
                for key, value in pairs(tpl_args.atlas_connections) do
        local data = core.map[k]
                    sorted[#sorted+1] = key
        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)]
            if value ~= nil then
                local default = type(data.default) == 'function' and data.default(tpl_args, frame) or data.default
                -- Inherit value from base item if derived item value is not equal to the default value.
                -- This verbose comparison is needed since two empty tables are not considered equal to one another.
                if tpl_args[k] == data.default or (type(tpl_args[k]) == 'table' and #tpl_args[k] == 0 and type(data.default) == 'table' and #data.default == 0) 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, tpl_args[k])
                    end
                 elseif tpl_args.debug then
                     mw.log(string.format(i18n.debug.field_value_mismatch, k, tostring(tpl_args[k])))
                 end
                 end
                 table.sort(sorted)
            elseif tpl_args.debug then
               
                 mw.log(string.format(i18n.debug.base_item_field_not_found, data.table, data.field))
                for _, key in ipairs(sorted) do
            end
                    local tr = tbl:tag('tr')
        elseif data.func_fetch ~= nil then
                    tr
            data.func_fetch(tpl_args, frame)
                        :tag('th')
        end
                            :wikitext(key)
    end
                            :done()
end
                   
 
                    for i=0,4 do
function s.process_mods(tpl_args, frame)
                        local field = fields[tpl_args.atlas_connections[key]['region' .. i]]
    for _, k in ipairs({'implicit', 'explicit'}) do
                        tr
        local success = true
                            :tag('td')
        local i = 1
                                :attr('data-sort-value', field.sort)
        while success do
                                :attr('class', field.class)
            success = h.validate_mod(tpl_args, frame, {key=k, i=i})
                                :wikitext(field.value)
            i = i + 1
                                :done()
        end
                    end
    end
                     tr:done()
 
    -- 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
               
                return tostring(tbl)
             end
             end
        },
            tpl_args._subobjects[#tpl_args._subobjects+1] = {
    },
                _table = 'item_mods',
    -- Drop info
                id = mod_data.id,
    {
                text = mod_data.stat_text,
        heading = i18n.tooltips.drop_restrictions,
                is_implicit = mod_data.is_implicit,
         class = '',
                is_random = mod_data.is_random,
         {
            }
             show = h.factory.args_present('drop_enabled'),
        end
             func = core.factory.infobox_line{
          
                parts = {
         local results = m_cargo.array_query{
                    {
             tables={'mods'},
                        key = 'drop_level',
             fields={'mods._pageName', 'mods.id', 'mods.required_level', 'mods.stat_text'},
                        fmt = '%i',
            id_field='mods.id',
                       
            id_array=mod_ids,
                    },
        }
                    {
       
                        key = 'drop_level_maximum',
        for _, data in ipairs(results) do
                        fmt = '%i',
            local mod_data = mods[data['mods.id']]
                        hide_default = 100,
            mod_data.result = data
                    },
           
                 },
            if mod_data.is_random == false then
                 sep = ' / ',
                -- update item level requirement
                 fmt = i18n.tooltips.level,
                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'
            show = function (tpl_args, frame)
                 end
                if tpl_args.drop_enabled == false then
               
                     return true
                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
                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
             end
         },
         end
         {
       
            show = h.factory.args_present('drop_areas_html'),
         -- fetch stats
             func = h.factory.display_raw_value('drop_areas_html'),
        results = m_cargo.query(
        },
            {'mods', 'mod_stats'},
        {
             {'mods.id', 'mod_stats.id', 'mod_stats.min', 'mod_stats.max'},
            show = h.factory.args_present('drop_text'),
            {
            func = h.factory.display_raw_value('drop_text'),
                join='mods._pageID=mod_stats._pageID',
         },
                where=string.format('mod_stats.id IS NOT NULL AND mods.id IN ("%s")', table.concat(mod_ids, '", "')),
    },
            }
    {
         )
        heading = i18n.tooltips.purchase_costs,
        for _, data in ipairs(results) do
         {
            -- Stat subobject
            show = function (tpl_args, frame)
            local mod_data = mods[data['mods.id']]
                 for rarity, data in pairs(tpl_args.purchase_costs) do
            if mod_data.result.stats == nil then
                    if #data > 0 then
                mod_data.result.stats = {data, }
                        return true
            else
                    end
                mod_data.result.stats[#mod_data.result.stats+1] = data
                end
            end
                return false
          
             end,
            local id = data['mod_stats.id']
             func = function (tpl_args, frame)
            local value = {
                 local tbl = mw.html.create('table')
                min = tonumber(data['mod_stats.min']),
                tbl
                 max = tonumber(data['mod_stats.max']),
                    --:attr('class', 'wikitable')
            }
                     :attr('style', 'width: 100%; margin-top: 0px;')
            value.avg = (value.min+value.max)/2
                   
           
                for _, rarity_id in ipairs(m_game.constants.rarity_order) do
            local prefix = ''
                    local data = tpl_args.purchase_costs[rarity_id]
            if mod_data.is_random then
                    if #data > 0 then
                prefix = '_random'
                        local tr = tbl:tag('tr')
            end
                         tr
             core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_stats')
                            :tag('td')
             if mod_data.is_implicit then
                                :wikitext(m_game.constants.rarities[rarity_id].long_upper)
                core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_implicit_stats')
                         local td = tr:tag('td')
            else
                        for _, purchase_data in ipairs(data) do
                 core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_explicit_stats')
                            td:wikitext(string.format('%dx [[%s]]<br />', purchase_data.amount, purchase_data.name))
            end
                        end
        end
                    end
       
        if tpl_args.is_sellable == true and 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 core.map.sell_prices_override
                    tpl_args.sell_prices[values.name] = (tpl_args.sell_prices[values.name] or 0) + values.amount
                 end
                 end
               
             end
                return tostring(tbl)
        end
             end,
     end
        },
     if tpl_args.is_sellable == true and tpl_args._flags.sell_prices_override ~= true then
     },
         local missing_sell_price = true
     {
        for _, _ in pairs(tpl_args.sell_prices) do
        heading = i18n.tooltips.sell_price,
             missing_sell_price = false
         {
            break
            show = h.factory.args_present('sell_price_order'),
        end
             func = function (tpl_args, frame)
       
                local out = {}
        if missing_sell_price then
                for _, item_name in ipairs(tpl_args.sell_price_order) do
            tpl_args.sell_prices[i18n.tooltips.default_vendor_offer] = 1
                    out[#out+1] = string.format('%dx [[%s]]', tpl_args.sell_prices[item_name], item_name)
        end
                 end
       
                  
        -- Set sell price on page
                return table.concat(out, '<br />')
        for name, amount in pairs(tpl_args.sell_prices) do
            end,
            -- sell_price_order is defined in core.map.sell_prices_override
        },
            tpl_args.sell_price_order[#tpl_args.sell_price_order+1] = name
    },
            tpl_args._subobjects[#tpl_args._subobjects+1] = {
     -- Damage per second
                _table = 'item_sell_prices',
     {
                 amount = amount,
         heading = i18n.tooltips.damage_per_second,
                 name = name,
        {
            }
             show = h.factory.args_present('physical_dps_html'),
        end
            func = core.factory.infobox_line{
        table.sort(tpl_args.sell_price_order)
                parts = {
    end
                    {
end
                        key = 'physical_dps_html',
 
                        fmt = '%s',
function s.process_stats(tpl_args, frame)
                        color = false, -- the html already contains the colour
     -- 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
                fmt = i18n.tooltips.physical_dps,
         if stat.value ~= nil then
            },
            stat.min = stat.value
        },
            stat.max = stat.value
        {
             stat.avg = stat.value
            show = h.factory.args_present('fire_dps_html'),
        end
            func = core.factory.infobox_line{
        core.stats_update(tpl_args, stat.id, stat, nil, '_stats')
                 parts = {
        core.stats_update(tpl_args, stat.id, stat, nil, '_explicit_stats')
                     {
    end
                        key = 'fire_dps_html',
 
                        fmt = '%s',
    -- Transpose stats into cargo data
                        color = false, -- the html already contains the colour
    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
                 fmt = i18n.tooltips.fire_dps,
                local is_implicit
            },
                 if type_prefix == '_implicit' then
        },
                     is_implicit = true
        {
                elseif type_prefix == '_explicit' then
            show = h.factory.args_present('cold_dps_html'),
                    is_implicit = false
            func = core.factory.infobox_line{
                 end
                parts = {
                 tpl_args._subobjects[#tpl_args._subobjects+1] = {
                     {
                    _table = 'item_stats',
                        key = 'cold_dps_html',
                    id = id,
                        fmt = '%s',
                    min = data.min,
                        color = false, -- the html already contains the colour
                     max = data.max,
                     },
                    avg = data.avg,
                 },
                    is_implicit = is_implicit,
                fmt = i18n.tooltips.cold_dps,
                     is_random = random_prefix == '_random',
             },
                 }
         },
             end
        {
         end
            show = h.factory.args_present('lightning_dps_html'),
    end
            func = core.factory.infobox_line{
   
                parts = {
    -- Handle extra stats (for gems)
                    {
    if cfg.class_groups.gems.keys[tpl_args.class_id] then
                        key = 'lightning_dps_html',
        h.skill(tpl_args, frame)
                        fmt = '%s',
    end
                        color = false, -- the html already contains the colour
   
                    },
    --
                },
    -- Handle local stats increases/reductions/additions
                fmt = i18n.tooltips.lightning_dps,
    --
            },
   
         },
    local skip = {}
         {
   
             show = h.factory.args_present('chaos_dps_html'),
    -- general stats
             func = core.factory.infobox_line{
    for k, data in pairs(core.stat_map) do
                 parts = {
         local value = tpl_args[k]
                     {
         if value ~= nil and skip[k] == nil then
                        key = 'chaos_dps_html',
             value = {min=value, max=value, base=value}
                        fmt = '%s',
             -- If stats are overriden we scan save some CPU time here
                         color = false, -- the html already contains the colour
            local overridden = false
                    },
            if data.stats_override ~= nil then
                },
                 for stat_id, override_value in pairs(data.stats_override) do
                fmt = i18n.tooltips.chaos_dps,
                     local stat_value = tpl_args._stats[stat_id]
            },
                    if stat_value ~= nil then
        },
                         -- Use the value of stat
        {
                        if override_value == true then
            show = h.factory.args_present('elemental_dps_html'),
                            value.min = stat_value.min
            func = core.factory.infobox_line{
                            value.max = stat_value.max
                parts = {
                            overridden = true
                    {
                        elseif stat_value ~= 0 then
                        key = 'elemental_dps_html',
                            value.min = override_value.min
                        fmt = '%s',
                            value.max = override_value.max
                         color = false, -- the html already contains the colour
                            overridden = true
                     },
                         end
                 },
                     end
                 fmt = i18n.tooltips.elemental_dps,
                 end
            },
          end
        },
                  
        {
          if overridden == false then
            show = h.factory.args_present('poison_dps_html'),
                -- The simple cases; this must be using ipairs as "add" must apply before
            func = core.factory.infobox_line{
                for _, operator in ipairs({'add', 'more'}) do
                parts = {
                    local st = data['stats_' .. operator]
                    {
                    if st ~= nil then
                         key = 'poison_dps_html',
                         for _, statid in ipairs(st) do
                        fmt = '%s',
                            if tpl_args._stats[statid] ~= nil then
                         color = false, -- the html already contains the colour
                                h.stat[operator](value, tpl_args._stats[statid])
                     },
                            end
                 },
                         end
                 fmt = i18n.tooltips.poison_dps,
                     end
            },
                 end
        },
                  
        {
                -- For increased stats we need to add them up first
            show = h.factory.args_present('dps_html'),
                for stat_key, stat_func in pairs({increased=h.stat.more, increased_inverse=h.stat.more_inverse}) do
            func = core.factory.infobox_line{
                    local st = data['stats_' .. stat_key]
                parts = {
                     if st ~= nil then
                     {
                         local total_increase = {min=0, max=0}
                         key = 'dps_html',
                         for _, statid in ipairs(st) do
                         fmt = '%s',
                            if tpl_args._stats[statid] ~= nil then
                        color = false, -- the html already contains the colour
                                for var, current_value in pairs(total_increase) do
                    },
                                    total_increase[var] = current_value + tpl_args._stats[statid][var]
                },
                                end
                fmt = i18n.tooltips.dps,
                            end
            },
                        end
        },
                        stat_func(value, total_increase)
    },
                    end
    {
                end
        heading = i18n.tooltips.misc,
               
        {
                if data.minimum ~= nil then
            show = h.factory.args_present('class'),
                    for _, key in ipairs({'min', 'max'}) do
            func = core.factory.infobox_line{
                        if value[key] < data.minimum then
                parts = {
                            value[key] = data.minimum
                    {
                         end
                        key = 'class',
                     end
                         fmt = '%s',
                 end
                     },
            else
                 },
 
                fmt = i18n.tooltips.item_class,
             end
             },
           
        },
             value.avg = (value.min + value.max) / 2
        {
           
             show = h.factory.args_present('metadata_id'),
            -- don't add the properties unless we need to
             func = function (tpl_args, frame)
             if (data.default ~= nil and (value.min ~= data.default or value.max ~= data.default)) or data.default == nil then
                 return core.factory.infobox_line{
                 for short_key, range_data in pairs(c.range_map) do
                     parts = {
                     tpl_args[data.field .. range_data.var] = value[short_key]
                        {
                end
                            key = 'metadata_id',
               
                            fmt = '%s',
                -- process to HTML to use on list pages or other purposes
                            inline = m_util.html.abbr('%s', tpl_args.metadata_id),
                h.handle_range_args(tpl_args, frame, k, data.field, value, data.html_fmt_options or {})
                        },
            end
                    },
           
                    fmt = tostring(
            for short_key, range_data in pairs(c.range_map) do
                        mw.html.create('span')
                 tpl_args[k .. range_data.var] = value[short_key]
                            :addClass('u-truncate-line')
             end
                            :wikitext(i18n.tooltips.metadata_id)
         end
                    ),
     end
                 }(tpl_args, frame)
end
             end,
         },
     },
}


-- ----------------------------------------------------------------------------
function s.process_weapon_dps(tpl_args, frame)
-- Exported functions
     for key, data in pairs(core.dps_map) do
-- ----------------------------------------------------------------------------
        local damage = {
 
            min = {},
local p = {}
            max = {},
 
         }
--
        for var_type, value in pairs(damage) do
-- Template:Item
            -- covers the min/max/avg range
--
            for short_key, range_data in pairs(c.range_map) do
function p.item(frame)
                value[short_key] = 0
     local t = os.clock()
                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)
    local tpl_args = getArgs(frame, {
                end
         parentFirst = true
            end
    })
        end
    frame = m_util.misc.get_frame(frame)
        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)]
    -- Shared args
            value[short_key] = result
    --
             tpl_args[string.format('%s%s', data.field, range_data.var)] = result
   
        end
    tpl_args._flags = {}
        if value.avg > 0 then
    tpl_args._base_item_args = {}
            h.handle_range_args(tpl_args, frame, key, data.field, value, data.html_fmt_options or {})
    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
     end
     end
     tpl_args._subobjects = {}
end
     tpl_args._properties = {}
 
     tpl_args._errors = {}
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
      
      
     h.process_arguments(tpl_args, frame, {'class_id'})
     if tpl_args.rarity_id ~= 'normal' or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
    h.build_item_classes(tpl_args, frame)
        cats[#cats+1] = i18n.categories.derived_items
     h.build_cargo_data(tpl_args, frame, c.item_classes)
     else
        cats[#cats+1] = i18n.categories.base_items
    end
      
      
     -- Using general purpose function to handle release and removal versions
     for _, attr in ipairs(m_game.constants.attribute_order) do
    m_util.args.version(tpl_args, {frame=frame, set_properties=true})
        if tpl_args[attr .. '_percent'] then
   
            cats[#cats+1] = string.format('%s %s', m_game.constants.attributes[attr].long_upper, tpl_args.class)
    -- Must validate some argument early. It is required for future things
    h.process_arguments(tpl_args, frame, cfg.default_args)
    h.process_arguments(tpl_args, frame, c.item_classes[tpl_args.class_id].args)
   
    -- Base Item
    h.process_base_item(tpl_args, frame)
   
    -- Prophecy special snowflake
    if tpl_args._flags.is_prophecy then
        err = h.process_arguments(tpl_args, frame, cfg.prophecy_args)
        if err then
            return err
         end
         end
     end
     end
      
      
     -- Mods
     local affix
     for _, k in ipairs({'implicit', 'explicit'}) do
     if tpl_args.class_id == 'Active Skill Gem' or tpl_args.class_id == 'Support Skill Gem' then
         local success = true
         affix = i18n.categories.gem_tag_affix
        local i = 1
    end
         while success do
    if affix ~= nil then
             success = h.validate_mod(tpl_args, frame, {key=k, i=i})
         for _, tag in ipairs(tpl_args.gem_tags) do
            i = i + 1
             cats[#cats+1] = string.format(affix, tag)
         end
         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
     end
      
      
     h.process_mods(tpl_args, frame)
     if #tpl_args.alternate_art_inventory_icons > 0 then
       
         cats[#cats+1] = i18n.categories.alternate_artwork
    -- 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
     end
 
      
     -- Transpose stats into cargo data
     if tpl_args.release_version == nil then
     for _, random_prefix in ipairs({'', '_random'}) do
        cats[#cats+1] = i18n.categories.missing_release_version
        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
     end
      
      
     -- Handle extra stats (for gems)
     if tpl_args._flags.text_modifier and not tpl_args.suppress_improper_modifiers_category then
        cats[#cats+1] = i18n.categories.improper_modifiers
    end
      
      
     if cfg.class_groups.gems.keys[tpl_args.class_id] then
     --
         m_skill._skill(tpl_args, frame)
    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
     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, {
    -- Handle local stats increases/reductions/additions
         parentFirst = true
    --
    })
   
    frame = m_util.misc.get_frame(frame)
    local skip = {}
   
   
    tpl_args._flags = {}
    -- general stats
    tpl_args._base_item_args = {}
    for k, data in pairs(core.stat_map) do
    tpl_args._base_implicit_mods = {}
         local value = tpl_args[k]
    tpl_args._defined_implicit_mods = {}
       
    tpl_args._mods = {}
        if value ~= nil and skip[k] == nil then
    for _, k in ipairs({'', '_random'}) do
            value = {min=value, max=value, base=value}
        for _, prefix in ipairs({'', '_implicit', '_explicit'}) do
            -- If stats are overriden we scan save some CPU time here
            tpl_args[k .. prefix .. '_stats'] = {}
            local overridden = false
        end
            if data.stats_override ~= nil then
    end
                for stat_id, override_value in pairs(data.stats_override) do
    tpl_args._subobjects = {}
                    local stat_value = tpl_args._stats[stat_id]
    tpl_args._properties = {}
                    if stat_value ~= nil then
    tpl_args._errors = {}
                        -- Use the value of stat
   
                        if override_value == true then
    -- Item configuration
                            value.min = stat_value.min
    tpl_args._item_config = s.get_item_config(tpl_args, frame)
                            value.max = stat_value.max
 
                            overridden = true
    -- Add table names to core.map and append range fields.
                        elseif stat_value ~= 0 then
    h.append_schema(tpl_args, frame, tpl_args._item_config.tables)
                            value.min = override_value.min
   
                            value.max = override_value.max
    -- Using general purpose function to handle release and removal versions
                            overridden = true
    m_util.args.version(tpl_args, {frame=frame, set_properties=true})
                        end
   
                    end
    -- Must validate some argument early. It is required for future things
                end
    h.process_arguments(tpl_args, frame, tpl_args._item_config.args)
          end
   
               
    -- Base item
          if overridden == false then
    s.process_base_item(tpl_args, frame)
                -- The simple cases; this must be using ipairs as "add" must apply before
   
                for _, operator in ipairs({'add', 'more'}) do
    -- Prophecies are considered currency items, but they have their own schema
                    local st = data['stats_' .. operator]
    if tpl_args._flags.is_prophecy then
                    if st ~= nil then
        h.process_arguments(tpl_args, frame, cfg.prophecy_args)
                        for _, statid in ipairs(st) do
    end
                            if tpl_args._stats[statid] ~= nil then
   
                                h.stat[operator](value, tpl_args._stats[statid])
    -- Mods
                            end
    s.process_mods(tpl_args, frame)
                        end
 
                    end
    -- Stats
                end
    s.process_stats(tpl_args, frame)
               
 
                -- For increased stats we need to add them up first
    -- Calculate and handle weapon dps
                for stat_key, stat_func in pairs({increased=h.stat.more, increased_inverse=h.stat.more_inverse}) do
    if cfg.class_groups.weapons.keys[tpl_args.class_id] then
                    local st = data['stats_' .. stat_key]
        s.process_weapon_dps(tpl_args, frame)
                    if st ~= nil then
    end
                        local total_increase = {min=0, max=0}
   
                        for _, statid in ipairs(st) do
    -- Late argument processing
                            if tpl_args._stats[statid] ~= nil then
    h.process_arguments(tpl_args, frame, tpl_args._item_config.late_args)
                                for var, current_value in pairs(total_increase) do
   
                                    total_increase[var] = current_value + tpl_args._stats[statid][var]
    -- Handle upgrade from restrictions/info
                                end
    h.process_upgraded_from(tpl_args, frame)
                            end
   
                        end
    -- Quest reward info
                        stat_func(value, total_increase)
    s.process_quest_rewards(tpl_args, frame)
                    end
   
                end
    -- ------------------------------------------------------------------------
               
    -- Infobox handling
                if data.minimum ~= nil then
    -- ------------------------------------------------------------------------
                    for _, key in ipairs({'min', 'max'}) do
    -- Store the infobox so it can be accessed with ease on other pages
                        if value[key] < data.minimum then
    tpl_args.html = tostring(h.make_main_container(tpl_args, frame, 'inline'))
                            value[key] = data.minimum
    local container = h.make_main_container(tpl_args, frame, 'infobox')
                        end
   
                    end
    if tpl_args.inventory_icon ~= nil and tpl_args.class_id ~= 'DivinationCard' then
                end
        container:tag('span')
            else
             :addClass('images')
 
             :wikitext(string.format(
            end
                '[[%s|%sx%spx]]',
           
                tpl_args.inventory_icon,
            value.avg = (value.min + value.max) / 2
                 cfg.image_size_full * tpl_args.size_x,
           
                cfg.image_size_full * tpl_args.size_y
            -- 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
    end
                for short_key, range_data in pairs(c.range_map) do
   
                    tpl_args[data.field .. range_data.var] = value[short_key]
     --
                end
    -- Secondary infobox
               
     --
                -- 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 {})
    tpl_args.extra_infobox = mw.html.create('span')
             end
         :attr( 'class', 'item-box -' .. tpl_args.frame_type)
              
            for short_key, range_data in pairs(c.range_map) do
                 tpl_args[k .. range_data.var] = value[short_key]
             end
        end
     end
 
     -- calculate and handle weapon dps
     if cfg.class_groups.weapons.keys[tpl_args.class_id] then
         for key, data in pairs(core.dps_map) do
            local damage = {
                min = {},
                max = {},
            }
          
          
            for var_type, value in pairs(damage) do
    h.add_to_container_from_map(tpl_args, frame, tpl_args.extra_infobox, c.extra_display_groups)
                -- covers the min/max/avg range
   
                for short_key, range_data in pairs(c.range_map) do
    --
                    value[short_key] = 0
    -- Output
                    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
    local infobox = mw.html.create('span')
                end
    infobox
            end
        :attr('class', 'infobox-page-container')
 
        :node(container)
            local value = {}
        :node(tpl_args.extra_infobox)
            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)]
    -- skill_screenshot is set in skill module
                value[short_key] = result
    if tpl_args.skill_screenshot then
                tpl_args[string.format('%s%s', data.field, range_data.var)] = result
        infobox:wikitext(string.format('<br>[[%s|300px]]', tpl_args.skill_screenshot))
            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
     local out = tostring(infobox)
    h.process_arguments(tpl_args, frame, cfg.late_args) -- General late args
     tpl_args.html_extra = out
     h.process_arguments(tpl_args, frame, c.item_classes[tpl_args.class_id].late_args) -- Class-specific late args
      
      
     -- Handle upgrade from restrictions/info
     local cats = s.get_categories(tpl_args, frame)
    h.process_upgraded_from(tpl_args, frame)
      
      
     -- Quest reward info
     out = out .. m_util.misc.add_category(cats, {ignore_blacklist=tpl_args.debug})
    h.process_quest_rewards(tpl_args, frame)
      
      
     -- ------------------------------------------------------------------------
     --
     -- Infobox handling
     -- Misc
     -- ------------------------------------------------------------------------
     --
     -- 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'))
     -- Also show the infobox for areas right away for maps, since they're both on the same page
     local container = h.make_main_container(tpl_args, frame, 'infobox')
    -- 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
      
      
     if tpl_args.inventory_icon ~= nil and tpl_args.class_id ~= 'DivinationCard' then
     -- ------------------------------------------------------------------------
        container:tag('span')
    -- Store cargo data
            :addClass('images')
    -- ------------------------------------------------------------------------
            :wikitext(string.format(
    -- Map argument values for cargo storage
                '[[%s|%sx%spx]]',
    for _, table_name in ipairs(tpl_args._item_config.tables) do
                tpl_args.inventory_icon,
        tpl_args._subobjects[table_name] = {
                cfg.image_size_full * tpl_args.size_x,
             _table = table_name,
                cfg.image_size_full * tpl_args.size_y
        }
             ))
     end
     end
      
     for k, v in pairs(tpl_args) do
    --
        local data = core.map[k]
    -- Secondary infobox
        if data ~= nil then
    --
            if data.table ~= nil and data.field ~= nil then
   
                if data.type == 'Integer' then
    tpl_args.extra_infobox = mw.html.create('span')
                    v = tonumber(string.format("%.0f", v))
        :attr( 'class', 'item-box -' .. tpl_args.frame_type)
                    if v ~= tpl_args[k] then
       
                        mw.log(string.format('Float value "%s" for integer field "%s.%s"', tpl_args[k], data.table, data.field))
    h.add_to_container_from_map(tpl_args, frame, tpl_args.extra_infobox, c.extra_display_groups)
                    end
   
                end
    --
                tpl_args._subobjects[data.table][data.field] = v
    -- Output
            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)))
   
            elseif data.table == nil and data.field ~= nil then
    local infobox = mw.html.create('span')
                mw.log(string.format('Possibly redundant argument "%s", value:\n "%s"', k, mw.dumpObject(v)))
    infobox
            end
        :attr('class', 'infobox-page-container')
        end
        :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
     end
      
      
     local out = tostring(infobox)
     -- Don't store cargo data in testing mode
    tpl_args.html_extra = out
     if not tpl_args.test then
   
         local attach = {}
    -- ------------------------------------------------------------------------
         for _, data in pairs(tpl_args._subobjects) do
    -- Category handling
            if attach[data._table] == nil then
    -- ------------------------------------------------------------------------
                local i = 0
   
                for _, _ in pairs(data) do
    local cats = {}
                    i = i + 1
    if tpl_args.rarity_id == 'unique' then
                    -- 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.
        cats[#cats+1] = string.format(i18n.categories.unique_affix, tpl_args.class)
                    if i > 1 then
    elseif tpl_args._flags.is_prophecy then
                        attach[data._table] = true
        cats[#cats+1] = i18n.categories.prophecies
                        -- Keeping this here in case it might be useful in the future.
    elseif tpl_args._flags.is_blight_item then
                        -- Recreating table is very slow and doing anything while the queue is running just creates a big mess.
        cats[#cats+1] = i18n.categories.blight_item
                        frame:expandTemplate{title=string.format('Template:Item/cargo/attach/%s', data._table), args={}}
    elseif tpl_args.is_talisman then
                        break
        cats[#cats+1] = i18n.categories.talismans
                    end
    elseif tpl_args.is_essence then
                end
        cats[#cats+1] = i18n.categories.essences
            end
    elseif tpl_args.class_id == 'Map' then
            m_cargo.store(frame, data, {
        cats[#cats+1] = string.format('%s %s', tpl_args.map_series, tpl_args.class)
                debug=tpl_args.debug,
    else
                sep={
        cats[#cats+1] = tpl_args.class
                    name_list='',
    end
                },
   
            })
     if tpl_args.rarity_id ~= 'normal' or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
         end
         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
   
    -- TODO: add maintenance categories
   
    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
   
    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=yes, where=string.format('areas.id="%s"', query_id)}
     end
     end
      
      
     -- ------------------------------------------------------------------------
     -- Show additional error messages in console to help fixing them
    -- Store cargo data
     if #tpl_args._errors > 0 then
    -- ------------------------------------------------------------------------
         mw.log(table.concat(tpl_args._errors, '\n'))
    -- Map argument values for cargo storage
    for _, table_name in ipairs(c.item_classes[tpl_args.class_id].tables) do
        tpl_args._subobjects[table_name] = {
            _table = table_name,
        }
     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
     end
      
      
    -- Don't actually save data in testing mode
     if tpl_args.debug then
     if not tpl_args.test then
         mw.log(os.clock() - t)
         local attach = {}
        mw.log('Start logging tpl_args.')
        for _, data in pairs(tpl_args._subobjects) do
        mw.logObject(tpl_args)
            if attach[data._table] == nil then
        mw.log('Stop logging tpl_args.')
                local i = 0
        mw.log('Start logging core.map.')
                for _, _ in pairs(data) do
         mw.logObject(core.map)
                    i = i + 1
         mw.log('Stop logging core.map.')
                    -- 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
     end
   
     if tpl_args.test then
     if tpl_args.test then
         tpl_args.out = out
         tpl_args.out = out
         return tpl_args
         return tpl_args
    else
        mw.logObject(os.clock() - t)
        return out
     end
     end
    return out
end
end



Revision as of 18:12, 14 July 2021

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. de:Modul:Item2 ru:Модуль:Item2

-------------------------------------------------------------------------------
-- 
--                                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_append_schema -- require('Module:Item2/cargo').append_schema

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

-- Declare early to avoid errors
local c = {}

-- ----------------------------------------------------------------------------
-- 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(tpl_args, frame)
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.append_schema(tpl_args, frame, tables)
    if not f_append_schema then
        f_append_schema = use_sandbox and require('Module:Item2/cargo/sandbox').append_schema or require('Module:Item2/cargo').append_schema
    end
    return f_append_schema(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)
    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
    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
-- ----------------------------------------------------------------------------

-- 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'),
        },
    },
    {
        -- Vendor prices
        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,
        },
    },
    {
        -- Vendor offer
        heading = i18n.tooltips.sell_price,
        {
            show = h.factory.args_present('sell_price_order'),
            func = function (tpl_args, frame)
                local out = {}
                if #tpl_args.sell_price_order > 0 then
                    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
                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'})
    local config = {
        tables = {},
        args = {},
        late_args = {},
        defaults = {},
    }
    for _, v in ipairs(cfg.tables) do
        table.insert(config.tables, v)
    end
    for _, v in ipairs(cfg.default_args) do
        table.insert(config.args, v)
    end
    for _, v in ipairs(cfg.late_args) do
        table.insert(config.late_args, v)
    end
    local extend_keys = {'tables', 'args', 'late_args'} -- We will do defaults separately to preserve order of args
    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
            if row['defaults'] then
                for k, v in pairs(row['defaults']) do
                    config['defaults'][k] = v
                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
        if class_specifics['defaults'] then
            for k, v in pairs(class_specifics['defaults']) do
                config['defaults'][k] = v
            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)]
            if value ~= nil then
                local default = type(data.default) == 'function' and data.default(tpl_args, frame) or data.default
                -- Inherit value from base item if derived item value is not equal to the default value.
                -- This verbose comparison is needed since two empty tables are not considered equal to one another.
                if tpl_args[k] == data.default or (type(tpl_args[k]) == 'table' and #tpl_args[k] == 0 and type(data.default) == 'table' and #data.default == 0) 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, tpl_args[k])
                    end
                elseif tpl_args.debug then
                    mw.log(string.format(i18n.debug.field_value_mismatch, k, tostring(tpl_args[k])))
                end
            elseif tpl_args.debug then
                mw.log(string.format(i18n.debug.base_item_field_not_found, data.table, data.field))
            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.is_sellable == true and 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 core.map.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.is_sellable == true and 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 core.map.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)

    -- Add table names to core.map and append range fields.
    h.append_schema(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
    
    local 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
    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.log(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 field for table "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.table, k, mw.dumpObject(v), mw.dumpObject(data)))
            elseif data.table == nil and data.field ~= nil then
                mw.log(string.format('Possibly redundant argument "%s", value:\n "%s"', k, mw.dumpObject(v)))
            end
        end
    end
    
    -- Don't store cargo 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.log(table.concat(tpl_args._errors, '\n'))
    end
    
    if tpl_args.debug then
        mw.log(os.clock() - t)
        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
    if tpl_args.test then
        tpl_args.out = out
        return tpl_args
    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