Module:Item2/sandbox: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>OmegaK2
(Created page with "-- SMW reworked item module -- ---------------------------------------------------------------------------- -- TODO -- -------------------------------------------------------...")
 
>Weaverthree
(Updating sandbox to current code from parent page; no interesting history in sandbox)
Line 6: Line 6:
-- Items
-- Items
-- -----
-- -----
-- fix up / property support is_corrupted
--
--
-- DROP restriction improvements:
-- DROP restriction improvements:
Line 12: Line 11:
--  drop difficulty
--  drop difficulty
--  drop monster type(s)
--  drop monster type(s)
--  drop league restriction
--
--
-- divinatation card support
-- divination card support
--
-- talismans:
--  tier
--  category
--
--
-- unique items:
-- unique items:
--  3d art
--  3D art
--  supporter attribution
--  supporter attribution
--
--
-- potentially include quality bonus in calcuations
-- potentially include quality bonus in calculations
-- singular for weapon class in infoboxes
-- singular for weapon class in infoboxes
--
--
-- Maps:  
-- Maps:  
--  Area level can be retrived eventually
--  Area level can be retrieved eventually
--
--
-- Essence:  
-- Essence:  
Line 40: Line 34:
-- Item table
-- Item table
-- ----------
-- ----------
-- Jewel radius
-- Skills need proper range values in their tables
-- Jewel limit
--
-- Rework the range to use HTML properties instead of manually building ranges
-- ----------
-- ----------
-- Item class
-- Item class
Line 61: Line 54:


local p = {}
local p = {}
local v = {}
local g_frame, g_args
local c = {}
local c = {}
c.image_size = 39
c.image_size = 39
Line 72: Line 61:


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Other stuff
-- Strings
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- This section contains strings used by this module.
-- Add new strings here instead of in-code directly, this will help other
-- people to correct spelling mistakes easier and help with translation to
-- other PoE wikis.
--
-- TODO: Maybe move this out to a separate sub-page module
local i18n = {
    range = '(%s to %s)',
    inventory_icon = 'File:%s inventory icon.png',
    status_icon = 'File:%s status icon.png',
    skill_screenshot = 'File:%s skill screenshot.jpg',
    skill_icon = 'File:%s skill icon.png',
    divination_card_art = 'File:%s card art.png',
    gem_tag_category = '[[:Category:%s (gem tag)|%s]]',


local h = {}
    categories = {
        -- maintenance cats
        broken_item_links = '[[Category:Pages with broken item links]]',
        improper_modifiers = 'Items with improper modifiers',
        missing_release_version = 'Items without a release version',


function h.debug(func)
        -- regular cats
    if g_args.debug == nil then
        alternate_artwork = 'Items with alternate artwork',
        return
    end
    func()
end


function h.na_or_val(tr, value, func)
         -- misc
    if value == nil then
         gem_tag_affix = '%s (gem tag)',
         tr:wikitext(util.html.td.na())
         unique_affix = 'Unique %s',
    else
     },
         local raw_value = value
        if func ~= nil then
            value = func(value)
         end
        tr
            :tag('td')
                :attr('data-sort-value', raw_value)
                :wikitext(value)
                :done()
     end
end


-- helper to loop over the range variables easier
    stat_skip_patterns = {
h.range_map = {
        maps = {
    min = {
            '%d+%% increased Quantity of Items found in this Area',
        property = ' range minimum',
            '%d+%% increased Rarity of Items found in this Area',
        var = '_range_minimum',
            '%+%d+%% Monster pack size',
    },
            -- ranges
    max = {
            '%(%d+%-%d+%)%% increased Quantity of Items found in this Area',
        property = ' range maximum',
            '%(%d+%-%d+%)%% increased Rarity of Items found in this Area',
        var = '_range_maximum',
            '%+%(%d+%-%d+%)%% Monster pack size',
        },
        jewels = {
            'Limited to %d+ %(Hidden%)',
            'Jewel has a radius of %d+ %(Hidden%)',
        },
     },
     },
     avg = {
 
         property = ' range average',
 
         var = '_range_average',
     help_text_defaults = {
         active_gem = 'Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.',
        support_gem = 'This is a Support Gem. It does not grant a bonus to your character, but skills in sockets connected to it. Place into an item socket connected to a socket containing the Active Skill Gem you wish to augment. Right click to remove from a socket.',
        hideout_doodad = 'Right click on this item then left click on a location on the ground to create the object.',
         jewel = 'Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.',
     },
     },
}


function h.handle_range_args(argument_key, property, value, fmt_options)
    -- Used by the item table
    fmt_options = mw.clone(fmt_options)
    item_table = {
    fmt_options.return_color = true
        item = 'Item',
    local html, colour = h.format_value(value, fmt_options)
        skill_gem = 'Skill gem',
    g_args[argument_key .. '_html'] = html
 
    g_args._properties[property .. ' HTML'] = html
        physical_dps = util.html.abbr('pDPS', 'physical damage per second'),
    g_args._properties[property .. ' range colour'] = colour
        fire_dps = util.html.abbr('Fire DPS', 'fire damage per second'),
   
        cold_dps = util.html.abbr('Cold DPS', 'cold damage per second'),
    fmt_options = mw.clone(fmt_options)
        lightning_dps = util.html.abbr('Light. DPS', 'lightning damage per second'),
    fmt_options.no_color = true
        chaos_dps = util.html.abbr('Chaos DPS', 'chaos damage per second'),
    g_args._properties[property .. ' range text'] = h.format_value(value, fmt_options)
        elemental_dps = util.html.abbr('eDPS', 'elemental damage (i.e. fire/cold/lightning) per second'),
end
        poison_dps = util.html.abbr('Poison DPS', 'poison damage (i.e. physical/chaos) per second'),
        dps = util.html.abbr('DPS', 'total damage (i.e. physical/fire/cold/lightning/chaos) per second'),
        base_item = 'Base Item',
        item_class = 'Item Class',
        essence_tier = 'Essence<br>Tier',
        drop_level = 'Drop<br>Level',
        stack_size = 'Stack<br>Size',
        stack_size_currency_tab = util.html.abbr('Tab<br>Stack<br>Size', 'Stack size in the currency stash tab'),
        armour = util.html.abbr('AR', 'Armour'),
        evasion = util.html.abbr('EV', 'Evasion Rating'),
        energy_shield = util.html.abbr('ES', 'Energy Shield'),
        block = util.html.abbr('Block', 'Chance to Block'),
        damage = util.html.abbr('Damage', 'Colour coded damage'),
        attacks_per_second = util.html.abbr('APS', 'Attacks per second'),
        local_critical_strike_chance = util.html.abbr('Crit', 'Local weapon critical strike chance'),
        flask_life = util.html.abbr('Life', 'Life regenerated over the flask duration'),
        flask_mana = util.html.abbr('Mana', 'Mana regenerated over the flask duration'),
        flask_duration = 'Duration',
        flask_charges_per_use = util.html.abbr('Usage', 'Number of charges consumed on use'),
        flask_maximum_charges = util.html.abbr('Capacity', 'Maximum number of flask charges held'),
        item_limit = 'Limit',
        jewel_radius = 'Radius',
        map_tier = 'Map<br>Tier',
        map_level = 'Map<br>Level',
        map_guild_character = util.html.abbr('Char', 'Character for the guild tag'),
        buff_effects = 'Buff Effects',
        stats = 'Stats',
        effects = 'Effect(s)',
        flavour_text = 'Flavour Text',
        help_text = 'Help Text',
        buff_icon = 'Buff<br>Icon',


function h.stats_update(id, value, modid)
        -- Skills
    if g_args._stats[id] == nil then
        support_gem_letter = util.html.abbr('L', 'Support gem letter.'),
         value.references = {modid}
        skill_icon = 'Icon',
         g_args._stats[id] = value
        description = 'Description',
    else
         skill_critical_strike_chance = util.html.abbr('Crit', 'Critical Strike Chance'),
         if modid ~= nil then
         cast_time = util.html.abbr('Cast<br>Time', 'Casting time of the skill in seconds'),
            table.insert(g_args._stats[id].references, mod_data.result['Is Mod'])
         damage_effectiveness = util.html.abbr('Dmg.<br>Eff.', 'Damage Effectiveness'),
         end
        mana_cost_multiplier = util.html.abbr('MCM', 'Mana cost multiplier - missing values indicate it changes on gem level'),
         g_args._stats[id].min = g_args._stats[id].min + value.min
         mana_cost = util.html.abbr('Mana', 'Mana cost'),
         g_args._stats[id].max = g_args._stats[id].max + value.max
         reserves_mana_suffix = util.html.abbr('R', 'reserves mana'),
         g_args._stats[id].avg = g_args._stats[id].avg + value.avg
        vaal_souls_requirement = util.html.abbr('Souls', 'Vaal souls requirement in Normal/Cruel/Merciless difficulty'),
     end
         stored_uses = util.html.abbr('Uses', 'Maximum number of stored uses'),
end
        primary_radius = util.html.abbr('R1', 'Primary radius'),
         secondary_radius = util.html.abbr('R2', 'Secondary radius'),
        tertiary_radius = util.html.abbr('R3', 'Tertiary radius'),
     },


h.stat = {}
    -- Used by the item info box
function h.stat.add (value, stat_cached)  
    tooltips = {
    value.min = value.min + stat_cached.min
        corrupted = 'Corrupted',
    value.max = value.max + stat_cached.max
        support_icon = 'Icon: %s',
end
        radius = 'Radius: %s',
        mana_reserved = 'Mana Reserved: %s',
        mana_cost = 'Mana Cost: %s',
        mana_multiplier = 'Mana Multiplier: %s',
        vaal_souls_per_use = 'Souls per use: %s',
        stored_uses = 'Can store %s use(s)',
        cooldown_time = 'Cooldown Time: %s',
        cast_time = 'Cast Time: %s',
        critical_strike_chance = 'Critical Strike Chance: %s',
        damage_effectiveness = 'Damage Effectiveness: %s',
        projectile_speed = 'Projectile Speed: %s',
        physical_damage = 'Physical Damage: %s',
        elemental_damage = 'Elemental Damage:%s',
        chaos_damage = 'Chaos Damage: %s',
        attacks_per_second = 'Attacks per Second: %s',
        weapon_range = 'Weapon Range: %s',
        map_level = 'Map Level: %s',
        map_tier = 'Map Tier: %s',
        map_guild_character = util.html.abbr('Guild Character', 'When used in guild creation, this map can be used for the listed character') .. ': %s',
        item_quantity = 'Item Quantity: %s',
        item_rarity = 'Item Rarity: %s',
        monster_pack_size = 'Monster Pack Size: %s',
        limited_to = 'Limited to: %s',
        flask_mana_recovery = 'Recovers %s Mana over %s seconds',
        flask_life_recovery = 'Recovers %s Life over %s seconds',
        flask_duration = 'Lasts %s Seconds',
        flask_charges_per_use = 'Consumes %s of %s Charges on use',
        chance_to_block = 'Chance to Block: %s',
        armour = 'Armour: %s',
        evasion = 'Evasion: %s',
        energy_shield = 'Energy Shield: %s',
        talisman_tier = 'Talisman Tier: %s',
        stack_size = 'Stack Size: %s',
        essence_tier = 'Essence Tier: %s',
        requires = 'Requires %s',
        level_inline = 'Level %s',
        level = 'Level: %s',
        drop_restrictions = 'Acquisition',
        league_restriction = util.html.abbr('League(s):', 'Item can be obtained in relation to these league(s)') .. ' %s',
        drop_disabled = 'DROP DISABLED',
        gem_quality = 'Per 1% Quality:',
        variation_singular = 'Variation',
        variation_plural = 'Variations',
        favour_cost = 'Favour cost: %s',
        seal_cost = 'Seal cost: <br>%s',
    },


function h.stat.more (value, stat_cached)
    item_class_infobox = {
    value.min = value.min * (1 + stat_cached.min / 100)
        page = '[[Item class]]',
    value.max = value.max * (1 + stat_cached.max / 100)
        info = util.html.abbr('(?)', 'Item classes categorize items. Classes are often used to restrict items or skill gems to a specific class or by item filters'),
end
        also_referred_to_as = 'Also referred to as:',
    },


h.tbl = {}
    debug = {
h.tbl.display = {}
        base_item_property_not_found = 'Base item property not found: %s',
function h.tbl.display.na_or_val(tr, value, data)
        property_value_mismatch = 'Value for argument "%s" is set to something else then default: %s',
     return h.na_or_val(tr, value)
    },
end
 
    errors = {
        missing_base_item = 'Rarity is set to above normal, but base item is not set. A base item for rarities above normal is required!',
        missing_rarity = 'Base item parameter is set, but rarity is set to normal. A rarity above normal is required!',
        duplicate_base_items = 'More then one result found for the specified base item. Consider using base_item_page or base_item_id to narrow down the results.',
        smw_mod_count_duplicate_format = '%s (pages: %s)',
        smw_mod_count_mismatch = 'Number of mods found does not match number of queried mods. Not found: "%s", duplicates: "%s"',
        invalid_league = '%s is not a recognized league',
        invalid_argument = 'Argument %s to item template is invalid. Please check the documentation for acceptable values.',
        not_a_percentage = '%s must be a percentage (in range 0 to 100).',
        invalid_tag = '%s is not a valid tag',
        item_link_invalid_args = 'page, item_name or item_name_exact must be specified',
        item_link_no_results = 'No results found for search parameter "%s".',
        item_link_too_many_results = 'Too many results for search parameter "%s". Consider using page parameter instead.',
        item_link_alt_art_undefined = 'Image parameter was specified, but there is no alternate art defined on page "%s"',
        item_link_alt_art_invalid_index = 'Alternate art with index/name "%s" not found on page "%s"',
        generic_argument_parameter = 'Unrecognized %s parameter "%s"',
        invalid_item_class = 'Invalid item class',
        invalid_item_table_mode = 'Invalid mode for item table',
        non_unique_relic = 'Only unique items can be be relics',
     },
}


function h.tbl.display.seconds(tr, value, data)
-- ----------------------------------------------------------------------------
    return h.na_or_val(tr, value, function(value)
-- Other stuff
        return string.format('%ss', value)
-- ----------------------------------------------------------------------------
    end)
end


function h.tbl.display.percent(tr, value, data)
local h = {}
    return h.na_or_val(tr, value, function(value)
        return string.format('%s%%', value)
    end)
end


function h.tbl.display.wikilink(tr, value, data)
function h.debug(tpl_args, func)
     return h.na_or_val(tr, value, function(value)
     if tpl_args.debug ==  nil then
         return string.format('[[%s]]', value)
         return
     end)
     end
    func()
end
end


h.tbl.display.factory = {}
function h.na_or_val(tr, value, func)
function h.tbl.display.factory.range(args)
    if value == nil then
  -- args: table
         tr:wikitext(util.html.td.na())
  --  property
    else
  --  no_base: no base value given
         local raw_value = value
  --  options: table, see h.format_value
         if func ~= nil then
  args.options = args.options or {}
             value = func(value)
  args.options.fmt_range = '(%s to<br>%s)'
 
  return function (tr, value, data)
        local value = {}
        local chk_keys = {}
          
        for key, range_data in pairs(h.range_map) do
            value[key] = tonumber(data[string.format('?Has %s%s', args.property, range_data.property)])
            chk_keys[#chk_keys+1] = key
        end
          
         if args.no_base == nil then
             value.base = tonumber(data['?Has base ' .. args.property])
            chk_keys[#chk_keys+1] = 'base'
        end
       
        if util.table.has_one_value(value, chk_keys, nil) then
            tr:wikitext(util.html.td.na())
            return
         end
         end
       
        if args.no_base or value.avg == value.base then
            args.options.color = 'value'
        else
            args.options.color = 'mod'
        end
       
         tr
         tr
             :tag('td')
             :tag('td')
                 :attr('data-sort-value', value.avg)
                 :attr('data-sort-value', raw_value)
                 :wikitext(h.format_value(value, args.options))
                 :wikitext(value)
                 :done()
                 :done()
  end
    end
end
end


function h.format_value(value, options)
-- helper to loop over the range variables easier
     -- value: table
h.range_map = {
    --  min:
     min = {
    --  max:
        property = ' range minimum',
     -- options: table
        var = '_range_minimum',
     --  fmt: formatter to use for the value instead of valfmt
     },
    --  fmt_range: formatter to use for the range values. Default: (%s to %s)
     max = {
     --  before: add this string before the coloured string with colour gray
        property = ' range maximum',
     --  after: add this string after the coloured string with colour gray
        var = '_range_maximum',
    --  func: Function to adjust the value with before output
     },
    --  color: colour code for util.html.poe_color, overrides mod colour
     avg = {
     --  no_color: set to true to ingore colour entirely
        property = ' range average',
    --  return_color: also return colour
        var = '_range_average',
     },
}


     if options.no_color == nil then
function h.handle_range_args(tpl_args, frame, argument_key, property, value, fmt_options)
        if options.color then
     fmt_options = mw.clone(fmt_options)
            value.color = options.color
    fmt_options.return_color = true
        elseif value.base ~= value.min or value.base ~= value.max then
    local html, colour = h.format_value(tpl_args, frame, value, fmt_options)
            value.color = 'mod'
    tpl_args[argument_key .. '_html'] = html
        else
    tpl_args._properties[property .. ' HTML'] = html
            value.color = 'value'
    tpl_args._properties[property .. ' range colour'] = colour
        end
    end
      
      
     if options.func ~= nil then
     fmt_options = mw.clone(fmt_options)
        value.min = options.func(value.min)
    fmt_options.no_color = true
        value.max = options.func(value.max)
    tpl_args._properties[property .. ' range text'] = h.format_value(tpl_args, frame, value, fmt_options)
    end
end
   
 
     if options.fmt == nil then
function h.stats_update(tpl_args, id, value, modid, key)
         options.fmt = '%s'
     if tpl_args[key][id] == nil then
    elseif type(options.fmt) == 'function' then
         tpl_args[key][id] = {
        options.fmt = options.fmt()
            references = {modid},
    end
            min = value.min,
   
            max = value.max,
    if value.min == value.max then
            avg = value.avg,
        value.out = string.format(options.fmt, value.min)
        }
     else
     else
         value.out = string.format(string.format(options.fmt_range or "(%s to %s)", options.fmt, options.fmt), value.min, value.max)
         if modid ~= nil then
            table.insert(tpl_args[key][id].references, modid)
        end
        tpl_args[key][id].min = tpl_args[key][id].min + value.min
        tpl_args[key][id].max = tpl_args[key][id].max + value.max
        tpl_args[key][id].avg = tpl_args[key][id].avg + value.avg
     end
     end
   
end
    if options.no_color == nil then
 
        value.out = util.html.poe_color(value.color, value.out)
h.stat = {}
    end
function h.stat.add (value, stat_cached)
   
    value.min = value.min + stat_cached.min
     local before = ''
    value.max = value.max + stat_cached.max
     if type(options.before) == 'string' then
end
        before = util.html.poe_color('default', options.before)
 
     elseif type(options.before) == 'function' then
function h.stat.more (value, stat_cached)
        before = util.html.poe_color('default', options.before())
     value.min = value.min * (1 + stat_cached.min / 100)
    end
     value.max = value.max * (1 + stat_cached.max / 100)
   
end
    local after = ''
 
     if type(options.after) == 'string' then
function h.stat.more_inverse (value, stat_cached)
         after = util.html.poe_color('default', options.after)
     value.min = value.min / (1 + stat_cached.min / 100)
    elseif type(options.after) == 'function' then
    value.max = value.max / (1 + stat_cached.max / 100)
        after = util.html.poe_color('default', options.after())
end
    end
 
   
h.tbl = {}
    local return_color
 
    if options.return_color ~= nil then
function h.tbl.range_properties(property)
         return_color = value.color
     return function()
        local props = {}
         for _, prop in ipairs({'maximum', 'text', 'colour'}) do
            props[#props+1] = string.format('Has %s range %s', property, prop)
        end
         return props
     end
     end
    return before .. value.out .. after, return_color
end
end


-- ----------------------------------------------------------------------------
h.tbl.display = {}
-- core
function h.tbl.display.na_or_val(tr, value, data)
-- ----------------------------------------------------------------------------
    return h.na_or_val(tr, value)
end


local core = {}
function h.tbl.display.seconds(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('%ss', value)
    end)
end


function core.validate_mod(args)
function h.tbl.display.percent(tr, value, data)
     -- args:
     return h.na_or_val(tr, value, function(value)
     --  key  - implict or explicit
        return string.format('%s%%', value)
    --  i
     end)
    --  value
end
    local value = g_args[args.key .. args.i]
 
     local out = {
function h.tbl.display.wikilink(tr, value, data)
        result=nil,
     return h.na_or_val(tr, value, function(value)
        modid=nil,
         return string.format('[[%s]]', value)
         type=args.key,
     end)
     }
end
   
 
    if value ~= nil then
h.tbl.display.factory = {}
        table.insert(g_args.mods, value)
function h.tbl.display.factory.value(args)
        table.insert(g_args[args.key .. '_mods'], value)
    args.options = args.options or {}
        out.modid = value
 
        --out.result = nil
    return function(tr, data, properties)
        table.insert(g_args._mods, out)
         values = {}
         return true
       
    else
         for index, prop in ipairs(properties) do
         value = g_args[args.key .. args.i .. '_text']
            local value = data[prop]
         if value ~= nil then
            if args.options[index] and args.options[index].fmt then
            --table.insert(g_args._subobjects, {
                value = string.format(args.options[index].fmt, value)
            --    ['Is mod number'] = args.i,
            end
             --  ['Has mod text'] = value,
            values[#values+1] = value
            --})
        end
            out.result = value
       
            table.insert(g_args._mods, out)
       
            return true
         local td = tr:tag('td')
        td:attr('data-sort-value', table.concat(values, ', '))
        td:wikitext(table.concat(values, ', '))
        if args.colour then
             td:attr('class', 'tc -' .. args.colour)
         end
         end
     end
     end
   
    return false
end
end


function core.process_smw_mods()
function h.tbl.display.factory.range(args)
     if #g_args.mods == 0 then
     -- args: table
        return
    --  property
    return function (tr, data, properties)
        tr
            :tag('td')
                :attr('data-sort-value', data[string.format('Has %s range maximum', args.property)] or '0')
                :attr('class', 'tc -' .. (data[string.format('Has %s range colour', args.property)] or 'default'))
                :wikitext(data[string.format('Has %s range text', args.property)])
                :done()
     end
     end
      
end
     local mods = {}
 
     for _, mod_data in ipairs(g_args._mods) do
function h.format_value(tpl_args, frame, value, options)
         if mod_data.result == nil then
     -- value: table
             mods[mod_data.modid] = mod_data
     --  min:
    --  max:
    -- options: table
    --  fmt: formatter to use for the value instead of valfmt
    --  fmt_range: formatter to use for the range values. Default: (%s to %s)
    --  inline: Use this format string to insert value
    --  inline_color: colour to use for the inline value; false to disable colour
    --  func: Function to adjust the value with before output
     --  color: colour code for util.html.poe_color, overrides mod colour
    --  no_color: set to true to ingore colour entirely
    --  return_color: also return colour
 
    if options.no_color == nil then
         if options.color then
            value.color = options.color
        elseif value.base ~= value.min or value.base ~= value.max then
             value.color = 'mod'
        else
            value.color = 'value'
         end
         end
     end
     end
      
      
     local results = {}
     if options.func ~= nil then
    local result
        value.min = options.func(tpl_args, frame, value.min)
    local query
        value.max = options.func(tpl_args, frame, value.max)
    local number_of_queries = math.ceil(#g_args.mods / c.max_mod_params)
     end
     local pages = {}
      
      
     for query_number=1, number_of_queries do
     if options.fmt == nil then
         local offset = (query_number-1)*c.max_mod_params
        options.fmt = '%s'
         local mod_ids = {}
    elseif type(options.fmt) == 'function' then
         for j=(1+offset), (c.max_mod_params+offset) do
        options.fmt = options.fmt(tpl_args, frame)
             mod_ids[#mod_ids+1] = g_args.mods[j]
    end
   
    if value.min == value.max then
        value.out = string.format(options.fmt, value.min)
    else
         value.out = string.format(string.format(options.fmt_range or i18n.range, options.fmt, options.fmt), value.min, value.max)
    end
   
    if options.no_color == nil then
         value.out = util.html.poe_color(value.color, value.out)
    end
   
    local return_color
    if options.return_color ~= nil then
         return_color = value.color
    end
   
    local text = options.inline
   
    if type(text) == 'string' then
    elseif type(text) == 'function' then
        text = text(tpl_args, frame)
    else
        text = nil
    end
   
    if text and text ~= '' then
        local color
        if options.inline_color == nil then
            color = 'default'
        elseif options.inline_color ~= false then
            color = color.inline_color
        end
       
        if color ~= nil then
             text = util.html.poe_color(color, text)
         end
         end
   
        query = {
            string.format('[[Is mod::%s]]', table.concat(mod_ids, '||')),
            '?#=Page',
            '?Is mod#',
            '?Has mod group#',
            '?Has mod type#',
            '?Has stat text#',
            '?Has level requirement#',
        }
          
          
         result = util.smw.query(query, g_frame)
         return string.format(text, value.out), return_color
    end
      
      
        -- remap this as table with modids as key
    -- If we didn't return before, return here
        for _, row in ipairs(result) do
    return value.out, return_color
            results[#results+1] = row
end
           
 
            local mod_data = mods[row['Is mod']]
-- ----------------------------------------------------------------------------
            mod_data.result = row
-- core
           
-- ----------------------------------------------------------------------------
            -- needed for the query
 
            pages[#pages+1] = row[1]
local core = {}
            -- needed for the mapping stats to mods
            pages[row[1]] = mod_data
           
            -- 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(row['Has level requirement']) * 0.8)
                if req > g_args[key] then
                    g_args[key] = req
                end
            end
        end
    end
   
    -- TODO: Can items have mods twice? I dont think so, if they do it would need to be accounted for here
    if #results ~= #g_args.mods then
        local missing = {}
        for _, modid in ipairs(g_args.mods) do
            if mods[modid].result == nil then
                missing[#missing+1] = modid
            end
        end


        local duplicates = {}
function core.validate_mod(tpl_args, frame, args)
         for _, row in ipairs(results) do
    -- args:
            if duplicates[row['Is mod']] == nil then
    --  key  - implict or explicit
                duplicates[row['Is mod']] = {row['Page']}
    --  i
             else
    --  value
                table.insert(duplicates[row['Is mod']], row['Page'])
    local value = tpl_args[args.key .. args.i]
             end
    local out = {
        end
        result=nil,
       
        modid=nil,
        local text = {}
        type=args.key,
        for modid, pages in pairs(duplicates) do
    }
             if #pages <= 1 then
   
                duplicates[modid] = nil
    if value ~= nil then
             else
         table.insert(tpl_args.mods, value)
                text[#text+1] = string.format('%s (pages: %s)', modid, table.concat(pages, ', '))
        table.insert(tpl_args[args.key .. '_mods'], value)
             end
        out.modid = value
        --out.result = nil
        table.insert(tpl_args._mods, out)
        return true
    else
        value = tpl_args[args.key .. args.i .. '_text']
        if value ~= nil then
             tpl_args._flags.text_modifier = true
            --table.insert(tpl_args._subobjects, {
            --    ['Is mod number'] = args.i,
            --  ['Has mod text'] = value,
             --})
             out.result = value
             table.insert(tpl_args._mods, out)
             return true
         end
         end
       
        error(string.format('Number of mods found does not match number of queried mods. Not found: "%s", duplicates: "%s"', table.concat(missing, ', '), table.concat(text)))
     end
     end
      
      
     -- fetch stats
     return false
end
 
function core.process_smw_mods(tpl_args, frame)
    if #tpl_args.mods == 0 then
        return
    end
      
      
     number_of_queries = math.ceil(#pages / c.max_stat_params)
     local mods = {}
    for _, mod_data in ipairs(tpl_args._mods) do
        if mod_data.result == nil then
            mods[mod_data.modid] = mod_data
        end
    end
   
    local results = {}
    local result
    local query
    local number_of_queries = math.ceil(#tpl_args.mods / c.max_mod_params)
    local pages = {}
      
      
     for query_number=1, number_of_queries do
     for query_number=1, number_of_queries do
         local offset = (query_number-1)*c.max_stat_params
         local offset = (query_number-1)*c.max_mod_params
         local temp_pages = {}
         local mod_ids = {}
         for j=(1+offset), (c.max_stat_params+offset) do
         for j=(1+offset), (c.max_mod_params+offset) do
             temp_pages[#temp_pages+1] = pages[j]
             mod_ids[#mod_ids+1] = tpl_args.mods[j]
         end
         end
       
   
         query = {
         query = {
             string.format('[[-Has subobject::%s]] [[Has stat id::+]] [[Has minimum stat value::+]] [[Has maximum stat value::+]]', table.concat(temp_pages, '||')),
             string.format('[[Is mod::%s]]', table.concat(mod_ids, '||')),
             '?Is stat number#',
            '?#=Page',
             '?Has stat id#',
             '?Is mod#',
             '?Has minimum stat value#',
            '?Has mod group#',
             '?Has maximum stat value#',
             '?Has mod type#',
             '?Has stat text#',
             '?Has level requirement#',
         }
         }
          
          
         local stats = util.smw.query(query, g_frame)
         result = util.smw.query(query, frame)
       
   
         -- process and cache stats
         -- remap this as table with modids as key
         for _, stat in ipairs(stats) do
         for _, row in ipairs(result) do
             local mod_data = pages[util.string.split(stat[1], '#')[1]]
            results[#results+1] = row
             if mod_data.result.stats == nil then
           
                mod_data.result.stats = {stat, }
             local mod_data = mods[row['Is mod']]
             else
             mod_data.result = row
                mod_data.result.stats[#mod_data.result.stats+1] = stat
           
            -- needed for the query
            pages[#pages+1] = row[1]
            -- needed for the mapping stats to mods
            pages[row[1]] = mod_data
           
            -- 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
             end
       
            local id = stat['Has stat id']
            local value = {
                min = tonumber(stat['Has minimum stat value']),
                max = tonumber(stat['Has maximum stat value']),
            }
            value.avg = (value.min+value.max)/2
              
              
             h.stats_update(id, value, mod_data.result['Is Mod'])
             for _, key in ipairs(keys) do
                local req = math.floor(tonumber(row['Has level requirement']) * 0.8)
                if req > tpl_args[key] then
                    tpl_args[key] = req
                end
            end
         end
         end
    end
end
function core.process_base_item(args)
    local query = {}
    if g_args.base_item_id ~= nil then
        query[#query+1] = string.format('[[Has metadata id::%s]]', g_args.base_item_id)
    elseif g_args.base_item_page ~= nil then
        query[#query+1] = string.format('[[%s]]', g_args.base_item_page)
    elseif g_args.base_item ~= nil then
        query[#query+1] = string.format('[[Has name::%s]]', g_args.base_item)
    elseif g_args.rarity ~= 'Normal' then
        error(core.err{msg='Rarity is set to above normal, but base item is not set. A base item for rarities above normal is required!'})
    else
        return
     end
     end
      
      
     if #query > 1 and g_args.rarity == 'Normal' then
    -- TODO: Can items have mods twice? I dont think so, if they do it would need to be accounted for here
         error(core.err{msg='Base item parameter is set, but rarity is set to normal. A rarity above normal is required!'})
     if #results ~= #tpl_args.mods then
    end
         local missing = {}
   
        for _, modid in ipairs(tpl_args.mods) do
    query[#query+1] = string.format('[[Has item class::%s]]', g_args['class'])
            if mods[modid].result == nil then
    query[#query+1] = '[[Has rarity::Normal]]'
                missing[#missing+1] = modid
    query[#query+1] = '?Has implicit mod ids#'
            end
    query[#query+1] = '?Has metadata id'
        end
    query[#query+1] = '?Has name'
 
   
        local duplicates = {}
    for _, k in ipairs(g_args._base_item_args) do
        for _, row in ipairs(results) do
        if core.map[k].property ~= nil then
            if duplicates[row['Is mod']] == nil then
             query[#query+1] = string.format('?%s#', core.map[k].property)
                duplicates[row['Is mod']] = {row['Page']}
            else
                table.insert(duplicates[row['Is mod']], row['Page'])
            end
        end
       
        local text = {}
        for modid, pages in pairs(duplicates) do
            if #pages <= 1 then
                duplicates[modid] = nil
             else
                text[#text+1] = string.format(i18n.errors.smw_mod_count_duplicate_format, modid, table.concat(pages, ', '))
            end
         end
         end
       
        error(string.format(i18n.errors.smw_mod_count_mismatch, table.concat(missing, ', '), table.concat(text)))
     end
     end
      
      
     local result = util.smw.query(query, g_frame)
     -- fetch stats
      
      
     if #result > 1 then
     number_of_queries = math.ceil(#pages / c.max_stat_params)
        error(core.err{msg='More then one result found for the specified base item. Consider using base_item_page or base_item_id to narrow down the results.'})
        -- TODO be more explicit in the error?
    end
      
      
     result = result[1]
     for query_number=1, number_of_queries do
   
         local offset = (query_number-1)*c.max_stat_params
    g_args.base_item_data = result
         local temp_pages = {}
    core.process_arguments{array={'base_item', 'base_item_page', 'base_item_id'}}
        for j=(1+offset), (c.max_stat_params+offset) do
 
             temp_pages[#temp_pages+1] = pages[j]
    --Copy values..
    for _, k in ipairs(g_args._base_item_args) do
         local data = core.map[k]
         if data.property then
            local value = result[data.property]
            -- I can just use data.default since it will be nil if not provided. Neat! ;)
            if value ~= "" and g_args[k] == data.default then
                g_args[k] = value
                if data.property_func ~= nil then
                    data.property_func()
                elseif data.func ~= nil then
                    data.func()
                end
            elseif value == "" then
                h.debug(function ()
                    mw.logObject(string.format("Base item property not found: %s", data.property))
                end)
             elseif g_args[k] ~= data.default then
                h.debug(function ()
                    mw.logObject(string.format("Value for arg '%s' is set to something else then default: %s", k, tostring(g_args[k])))
                end)
            end
        elseif data.property_func ~= nil then
            -- property func is set, but not a property. Probably fetching subobjects here
            data.property_func()
         end
         end
    end
end
function core.process_arguments(args)
    for _, k in ipairs(args.array) do
        local data = core.map[k]
        table.insert(g_args._total_args, k)
        if data.no_copy == nil then
            table.insert(g_args._base_item_args, k)
        end
        if data.func ~= nil then
            local status, err = pcall(data.func)
            -- an error was raised, return the error string instead of the template
            if not status then
                return err
            end
        end
        if data.default ~= nil and g_args[k] == nil then
            g_args[k] = data.default
        end
    end
end
          
          
function core.process_mod_stats(args)
        query = {
    local lines = {}
            string.format('[[-Has subobject::%s]] [[Has stat id::+]] [[Has minimum stat value::+]] [[Has maximum stat value::+]]', table.concat(temp_pages, '||')),
   
            '?Is stat number#',
    local skip = core.class_specifics[g_args.class]
            '?Has stat id#',
    if skip then
            '?Has minimum stat value#',
         skip = skip.skip_stat_lines
            '?Has maximum stat value#',
    end
        }
   
       
    for _, modinfo in ipairs(g_args._mods) do
        local stats = util.smw.query(query, frame)
        if modinfo.type == args.type then
       
             if modinfo.modid == nil then
         -- process and cache stats
                 table.insert(lines, modinfo.result)
        for _, stat in ipairs(stats) do
            local mod_data = pages[util.string.split(stat[1], '#')[1]]
             if mod_data.result.stats == nil then
                 mod_data.result.stats = {stat, }
             else
             else
                 for _, line in ipairs(util.string.split(modinfo.result['Has stat text'], '<br>')) do
                 mod_data.result.stats[#mod_data.result.stats+1] = stat
                    if skip == nil then
            end
                        table.insert(lines, line)
       
                    else
            local id = stat['Has stat id']
                        local skipped = false
            local value = {
                        for _, pattern in ipairs(skip) do
                min = tonumber(stat['Has minimum stat value']),
                            if string.match(line, pattern) then
                max = tonumber(stat['Has maximum stat value']),
                                skipped = true
            }
                                break
            value.avg = (value.min+value.max)/2
                            end
           
                        end
            h.stats_update(tpl_args, id, value, mod_data.result['Is mod'], '_stats')
                        if not skipped then
            if mod_data.type ~= 'implicit' then
                            table.insert(lines, line)
                h.stats_update(tpl_args, id, value, mod_data.result['Is mod'], '_explicit_stats')
                        end
                    end
                end
             end
             end
         end
         end
    end
   
    if #lines == 0 then
        return
    else
        return table.concat(lines, '<br>')
     end
     end
end
end


function core.err(args)
function core.process_base_item(tpl_args, frame, args)
     local err = mw.html.create('div')
     local query = {}
    err
        :attr('style', 'font-color: red; font-weight: bold;')
        :wikitext(args.msg or ('Argument ' .. args.key .. ' to item template is invalid. Please check the documention for acceptable values.'))
        :done()
       
    return tostring(err)
end


--
    if tpl_args.base_item_id ~= nil then
-- function factory
        query[#query+1] = string.format('[[Has metadata id::%s]]', tpl_args.base_item_id)
--
     elseif tpl_args.base_item_page ~= nil then
core.factory = {}
        query[#query+1] = string.format('[[%s]]', tpl_args.base_item_page)
function core.factory.array_table_cast(k, args)
    elseif tpl_args.base_item ~= nil then
     -- Arguments:
        query[#query+1] = string.format('[[Has name::%s]]', tpl_args.base_item)
    --
    elseif tpl_args.rarity ~= 'Normal' then
    -- tbl
        error(core.err{msg=i18n.errors.missing_base_item})
    -- errmsg
    else
    return function ()
         return
        local elements
       
        if g_args[k] ~= nil then
            elements = util.string.split(g_args[k], ', ')
            for _, element in ipairs(elements) do
                local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
                if r == nil then
                    error(core.err{msg=string.format(args.errmsg, element)})
                end
            end
            g_args[k] = xtable:new(elements)
         end
     end
     end
end
   
 
     if #query > 1 and tpl_args.rarity == 'Normal' then
function core.factory.table_cast(k, args)
         error(core.err{msg=i18n.errors.missing_rarity})
     return function ()
        args.value = g_args[k]
        g_args[k] = util.table.find_in_nested_array(args)
         if g_args[k] == nil then
            error(core.err{key=k})
        end
     end
     end
end
   
 
    query[#query] = query[#query] .. string.format('[[Has item class::%s]] [[Has rarity::Normal]]', tpl_args['class'])
 
     query[#query+1] = '?Has implicit mod ids#'
function core.factory.number_cast(k)
    query[#query+1] = '?Has metadata id'
     return function ()
    query[#query+1] = '?Has name'
         g_args[k] = tonumber(g_args[k])
   
    for _, k in ipairs(tpl_args._base_item_args) do
         if core.map[k].property ~= nil then
            query[#query+1] = string.format('?%s#', core.map[k].property)
        end
    end
   
    local result = util.smw.query(query, frame)
   
    if #result > 1 then
        error(core.err{msg=i18n.errors.duplicate_base_items})
        -- TODO be more explicit in the error?
     end
     end
end
   
    result = result[1]
   
    tpl_args.base_item_data = result
    core.process_arguments(tpl_args, frame, {array={'base_item', 'base_item_page', 'base_item_id'}})


function core.factory.boolean_cast(k)
    --Copy values..
     return function()
     for _, k in ipairs(tpl_args._base_item_args) do
         if g_args[k] ~= nil then
         local data = core.map[k]
             g_args[k] = util.cast.boolean(g_args[k])
        if data.property then
            local value = result[data.property]
            -- I can just use data.default since it will be nil if not provided. Neat! ;)
            if value ~= "" and tpl_args[k] == data.default then
                tpl_args[k] = value
                if data.property_func ~= nil then
                    data.property_func(tpl_args, frame)
                elseif data.func ~= nil then
                    data.func(tpl_args, frame)
                end
             elseif value == "" then
                h.debug(tpl_args, function ()
                    mw.logObject(string.format(i18n.debug.base_item_property_not_found, data.property))
                end)
            elseif tpl_args[k] ~= data.default then
                h.debug(tpl_args, function ()
                    mw.logObject(string.format(i18n.debug.property_value_mismatch, k, tostring(tpl_args[k])))
                end)
            end
        elseif data.property_func ~= nil then
            -- property func is set, but not a property. Probably fetching subobjects here
            data.property_func(tpl_args, frame)
         end
         end
     end
     end
end
end


function core.factory.percentage(k)
function core.process_arguments(tpl_args, frame, args)
     return function ()
     for _, k in ipairs(args.array) do
         local v = tonumber(g_args[k])
         local data = core.map[k]
          
        table.insert(tpl_args._total_args, k)
         if v == nil then
        if data.no_copy == nil then
             return core.err{key=k}
            table.insert(tpl_args._base_item_args, k)
         end
         if data.func ~= nil then
             data.func(tpl_args, frame)
            --[[local status, err = pcall(data.func)
            -- an error was raised, return the error string instead of the template
            if not status then
                return err
            end
            ]]--
        end
        if data.default ~= nil and tpl_args[k] == nil then
            tpl_args[k] = data.default
         end
         end
    end
end
          
          
         if v < 0 or v > 100 then
function core.process_mod_stats(tpl_args, args)
             return core.err{msg=k .. ' must be in range 0-100.'}
    local lines = {}
   
    local skip = core.class_specifics[tpl_args.class]
    if skip then
        skip = skip.skip_stat_lines
    end
   
    for _, modinfo in ipairs(tpl_args._mods) do
         if modinfo.type == args.type then
             if modinfo.modid == nil then
                table.insert(lines, modinfo.result)
            else
                for _, line in ipairs(util.string.split(modinfo.result['Has stat text'], '<br>')) do
                    if line ~= '' then
                        if skip == nil then
                            table.insert(lines, line)
                        else
                            local skipped = false
                            for _, pattern in ipairs(skip) do
                                if string.match(line, pattern) then
                                    skipped = true
                                    break
                                end
                            end
                            if not skipped then
                                table.insert(lines, line)
                            end
                        end
                    end
                end
            end
         end
         end
          
    end
         g_args[k] = v
   
    if #lines == 0 then
         return
    else
         return table.concat(lines, '<br>')
     end
     end
end
end


function core.factory.display_value(args)
function core.process_upgraded_from(tpl_args, args)
     -- TODO: sane defaults for the prepend string "before" & after arguments
    return
     --
     -- TODO DISABLED
     -- args:
     --[[
     --  type: Type of the keys (nil = regular, gem = skill gems, stat = stats)
     local sets = {}
    --  options<Array>:
     local setid = 1
    --  key: key to use
    local set
    --  allow_zero: allow zero values
    repeat
    --  hide_default: hide the value if this is set
        local prefix = string.format('upgrade_from_set%s_', setid)
    --  hide_default_key: key to use if it isn't equal to the key parameter
        local groupid = 1
    --  -- from h.format_value --
        local group
    --  fmt: formatter to use for the value instead of valfmt
        set = {
    --  fmt_range: formatter to use for the range values. Default: (%s to %s)
            groups = {},
    --  before: add this string before the coloured string with colour gray
            optional = util.cast.boolean(tpl_args[prefix .. 'optional']),
    --  after: add this string after the coloured string with colour gray
            text = tpl_args[prefix .. 'text'],
    --  func: Function to adjust the value with before output
        }
    --  color: colour code for util.html.poe_color, overrides mod colour
        repeat
    --   no_color: set to true to ingore colour entirely
            local group_prefix = string.format('%sgroup%s_', prefix, index)
            group = {
                item = tpl_args[group_prefix .. 'item'],
                count = tonumber(tpl_args[group_prefix .. 'count']) or 1,
                notes = tpl_args[group_prefix .. 'notes'],
            }
           
            if group.item == nil then
                set.groups[#set.groups+1] = group
            end
           
            groupid = groupid + 1
        until group.item == nil end
       
        -- set was empty, can terminate safely
        if #set.groups == 0 then
            set = nil
        else
            setid = setid + 1
            sets[#sets+1] = set
        end
    until set == nil end
      
      
     for k, default in pairs({options = {}}) do
     if #sets == 0 then
        if args[k] == nil then
         return
            args[k] = default
         end
     end
     end
      
      
     return function ()
     tpl_args.upgrade_from_sets = sets
        local base_values = {}
   
         local temp_values = {}
   
         if args.type == 'gem' then
    -- TODO: Semantic properties, validate items
            if not core.class_groups.gems.keys[g_args.class] then
    ]]--
                return
end
            end
 
            for i, data in ipairs(args.options) do
function core.err(args)
                local value = g_args['static_' .. data.key]
    local err = mw.html.create('div')
                if value ~= nil then
    err
                    base_values[#base_values+1] = value
         :attr('style', 'font-color: red; font-weight: bold;')
                    temp_values[#temp_values+1] = {value={min=value,max=value}, index=i}
         :wikitext(args.msg or string.format(i18n.errors.invalid_argument, args.key))
                 else
        :done()
                    value = {
       
                        min=g_args[string.format('level1_%s', data.key)],
    return tostring(err)
                        max=g_args[string.format('level%s_%s', g_args.max_level, data.key)],
end
                    }
 
                    if value.min == nil or value.max == nil then
--
                     else
-- function factory
                        base_values[#base_values+1] = value.min
--
                        temp_values[#temp_values+1] = {value=value, index=i}
core.factory = {}
                    end
function core.factory.array_table_cast(k, args)
    -- Arguments:
    --
    -- tbl
    -- errmsg
    return function (tpl_args, frame)
        local elements
       
        if tpl_args[k] ~= nil then
            elements = util.string.split(tpl_args[k], ', ')
            for _, element in ipairs(elements) do
                 local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
                if r == nil then
                     error(core.err{msg=string.format(args.errmsg, element)})
                 end
                 end
             end
             end
         elseif args.type == 'stat' then
            tpl_args[k] = xtable:new(elements)
            for i, data in ipairs(args.options) do
         end
                local value = g_args._stats[data.key]
    end
                if value ~= nil then
end
                    base_values[i] = value.min
 
                    temp_values[#temp_values+1] = {value=value, index=i}
function core.factory.table_cast(k, args)
                end
    return function (tpl_args, frame)
            end
        args.value = tpl_args[k]
        else
        tpl_args[k] = util.table.find_in_nested_array(args)
            for i, data in ipairs(args.options) do
        if tpl_args[k] == nil then
                base_values[i] = g_args[data.key]
            error(core.err{key=k})
                local value = {}
        end
                if g_args[data.key .. '_range_minimum'] ~= nil then
    end
                    value.min = g_args[data.key .. '_range_minimum']
end
                    value.max = g_args[data.key .. '_range_maximum']
 
                 elseif g_args[data.key] ~= nil then
function core.factory.assoc_table_cast(k, args)
                     value.min = g_args[data.key]
    -- Arguments:
                    value.max = g_args[data.key]
    --
                end
    -- tbl
                if value.min == nil then
    -- errmsg
                else
    return function (tpl_args, frame)
                    temp_values[#temp_values+1] = {value=value, index=i}
        local elements
       
        if tpl_args[k] ~= nil then
            elements = util.string.split(tpl_args[k], ', ')
            for _, element in ipairs(elements) do
                 if args.tbl[element] == nil then
                     error(core.err{msg=string.format(args.errmsg, element)})
                 end
                 end
             end
             end
            tpl_args[k] = elements
        end
    end
end
function core.factory.number_cast(k)
    return function (tpl_args, frame)
        tpl_args[k] = tonumber(tpl_args[k])
    end
end
function core.factory.boolean_cast(k)
    return function(tpl_args, frame)
        if tpl_args[k] ~= nil then
            tpl_args[k] = util.cast.boolean(tpl_args[k])
         end
         end
    end
end
function core.factory.percentage(k)
    return function (tpl_args, frame)
        local v = tonumber(tpl_args[k])
          
          
         local final_values = {}
         if v == nil then
        for i, data in ipairs(temp_values) do
             return core.err{key=k}
            local opt = args.options[data.index]
            local insert = false
            if opt.hide_default == nil then
                insert = true
             elseif opt.hide_default_key == nil then
                local v = data.value
                if opt.hide_default ~= v.min and opt.hide_default ~= v.max then
                    insert = true
                end
            else
                local v = {
                    min = g_args[opt.hide_default_key .. '_range_minimum'],
                    max = g_args[opt.hide_default_key .. '_range_maximum'],
                }
                if v.min == nil or v.max == nil then
                    if opt.hide_default ~= g_args[opt.hide_default_key] then
                        insert = true
                    end
                elseif opt.hide_default ~= v.min and opt.hide_default ~= v.max then
                    insert = true
                end
            end
           
            if insert == true then
                table.insert(final_values, data)
            end
         end
         end
          
          
        -- all zeros = dont display and return early
         if v < 0 or v > 100 then
         if #final_values == 0 then
             return core.err{msg=string.format(i18n.errors.not_a_percentage, k)}
             return nil
         end
         end
          
          
         local out = {}
         tpl_args[k] = v
       
    end
        if args.before then
end
            out[#out+1] = util.html.poe_color('default', args.before)
 
        end
function core.factory.display_value(args)
       
    -- args:
        for i, data in ipairs(final_values) do
    --  type: Type of the keys (nil = regular, gem = skill gems, stat = stats)
            local value = data.value
    --  options<Array>:
            value.base = base_values[data.index]
    --  key: key to use
           
    --  allow_zero: allow zero values
            local options = args.options[data.index]
    --  hide_default: hide the value if this is set
           
    --  hide_default_key: key to use if it isn't equal to the key parameter
            if options.color == nil and args.type == 'gem' then
    --  -- from h.format_value --
                value.color = 'value'
    --  fmt: formatter to use for the value instead of valfmt
            end
    --  fmt_range: formatter to use for the range values. Default: (%s to %s)
           
    --  insert: insert results into this object
            out[#out+1] = h.format_value(value, options)
    --  func: Function to adjust the value with before output
        end
    --  color: colour code for util.html.poe_color, overrides mod colour
       
    --  no_color: set to true to ingore colour entirely
         if args.after then
   
             out[#out+1] = util.html.poe_color('default', args.after)
    for k, default in pairs({options = {}}) do
         if args[k] == nil then
             args[k] = default
         end
         end
       
        return table.concat(out, '')
     end
     end
end
   
 
    return function (tpl_args, frame)
function core.factory.display_value_only(key)
        local base_values = {}
    return function()
        local temp_values = {}
        return g_args[key]
        if args.type == 'gem' then
    end
            if not core.class_groups.gems.keys[tpl_args.class] then
end
                return
 
            end
function core.factory.descriptor_value(args)
            for i, data in ipairs(args.options) do
    -- Arguments:
                local value = tpl_args['static_' .. data.key]
    --  key
                if value ~= nil then
    --  tbl
                    base_values[#base_values+1] = value
    args = args or {}
                    temp_values[#temp_values+1] = {value={min=value,max=value}, index=i}
    return function (value)
                else
         args.tbl = args.tbl or g_args
                    value = {
        if args.tbl[args.key] then
                        min=tpl_args[string.format('level1_%s', data.key)],
            value = util.html.abbr(value, args.tbl[args.key])
                        max=tpl_args[string.format('level%s_%s', tpl_args.max_level, data.key)],
                    }
                    if value.min == nil or value.max == nil then
                    else
                        base_values[#base_values+1] = value.min
                        temp_values[#temp_values+1] = {value=value, index=i}
                    end
                end
            end
        elseif args.type == 'stat' then
            for i, data in ipairs(args.options) do
                local value = tpl_args._stats[data.key]
                if value ~= nil then
                    base_values[i] = value.min
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
         else
            for i, data in ipairs(args.options) do
                base_values[i] = tpl_args[data.key]
                local value = {}
                if tpl_args[data.key .. '_range_minimum'] ~= nil then
                    value.min = tpl_args[data.key .. '_range_minimum']
                    value.max = tpl_args[data.key .. '_range_maximum']
                elseif tpl_args[data.key] ~= nil then
                    value.min = tpl_args[data.key]
                    value.max = tpl_args[data.key]
                end
                if value.min == nil then
                else
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
         end
         end
         return value
          
    end
        local final_values = {}
end
        for i, data in ipairs(temp_values) do
 
            local opt = args.options[data.index]
function core.factory.damage_html(args)
            local insert = false
    return function()
            if opt.hide_default == nil then
        if args.key ~= '' then
                insert = true
          args.color = args.key
            elseif opt.hide_default_key == nil then
          args.no_color = true
                local v = data.value
          args.key = args.key .. '_'
                if opt.hide_default ~= v.min and opt.hide_default ~= v.max then
                    insert = true
                end
            else
                local v = {
                    min = tpl_args[opt.hide_default_key .. '_range_minimum'],
                    max = tpl_args[opt.hide_default_key .. '_range_maximum'],
                }
                if v.min == nil or v.max == nil then
                    if opt.hide_default ~= tpl_args[opt.hide_default_key] then
                        insert = true
                    end
                elseif opt.hide_default ~= v.min and opt.hide_default ~= v.max then
                    insert = true
                end
            end
           
            if insert == true then
                table.insert(final_values, data)
            end
         end
         end
        local keys = {
            min=args.key .. 'damage_min',
            max=args.key .. 'damage_max',
        }
        local value = {}
          
          
         for ktype, key in pairs(keys) do
         -- all zeros = dont display and return early
            value[ktype] = core.factory.display_value{options={ [1] = {
        if #final_values == 0 then
                key = key,
             return nil
                no_color = args.no_color,
                hide_default = 0
             }}}()
         end
         end
          
          
         if value.min and value.max then
         local out = {}
             value = value.min .. '&ndash;' .. value.max
       
             if args.color ~= nil then
        for i, data in ipairs(final_values) do
                 value = util.html.poe_color(args.color, value)
            local value = data.value
             value.base = base_values[data.index]
           
            local options = args.options[data.index]
           
             if options.color == nil and args.type == 'gem' then
                 value.color = 'value'
             end
             end
             g_args[(args.write_key or args.key) .. 'damage_html'] = value
              
            out[#out+1] = h.format_value(tpl_args, frame, value, options)
         end
         end
       
        if args.inline then
            return util.html.poe_color('default', string.format(args.inline, unpack(out)))
        else
            return table.concat(out, '')
        end
    end
end
function core.factory.display_value_only(key)
    return function(tpl_args, frame)
        return tpl_args[key]
     end
     end
end
end


--
function core.factory.descriptor_value(args)
-- argument mapping
    -- Arguments:
--
    --  key
-- format:
    --  tbl
-- g_args key = {
    args = args or {}
--  no_copy = true or nil          -- When loading an base item, dont copy this key  
    return function (tpl_args, frame, value)
--  property = 'prop',              -- Property associated with this key
        args.tbl = args.tbl or tpl_args
--  property_func = function or nil -- Function to unpack the property into a native lua value.  
        if args.tbl[args.key] then
--                                      If not specified, func is used.  
            value = util.html.abbr(value, args.tbl[args.key])
--                                      If neither is specified, value is copied as string
        end
--  func = function or nil          -- Function to unpack the argument into a native lua value and validate it.  
        return value
--                                      If not specified, value will not be set.
    end
--  default = object                -- Default value if the parameter is nil
end
-- }
 
core.map = {
function core.factory.damage_html(args)
     -- special params
    return function(tpl_args, frame)
     html = {
        if args.key ~= 'physical' then
         no_copy = true,
          args.color = args.key
         property = 'Has infobox HTML',
          args.no_color = true
         func = nil,
        end
     },
        local keys = {
     implicit_stat_text = {
            min=args.key .. '_damage_min',
         property = 'Has implicit stat text',
            max=args.key .. '_damage_max',
         func = function ()
        }
             g_args.implicit_stat_text = core.process_mod_stats{type='implicit'}
        local value = {}
         end,
       
     },
        for ktype, key in pairs(keys) do
     explicit_stat_text = {
            value[ktype] = core.factory.display_value{options={ [1] = {
         property = 'Has explicit stat text',
                key = key,
         func = function ()
                no_color = args.no_color,
             g_args.explicit_stat_text = core.process_mod_stats{type='explicit'}
                hide_default = 0
              
            }}}(tpl_args, frame)
             if g_args.is_talisman then
        end
                 g_args.explicit_stat_text = (g_args.explicit_stat_text or '') .. util.html.poe_color('corrupted', 'Corrupted')  
       
        if value.min and value.max then
            value = value.min .. '&ndash;' .. value.max
            if args.color ~= nil then
                value = util.html.poe_color(args.color, value)
            end
            tpl_args[args.key .. '_damage_html'] = value
        end
    end
end
 
function core.factory.copy_stats(args)
    return function(tpl_args, frame)
        if tpl_args._stats[args.stat_key] then
            if tpl_args._stats[args.stat_key].min ~= tpl_args._stats[args.stat_key].max then
                error('TODO: Missing support for range item limits')
            end
            tpl_args[args.key] = tpl_args._stats[args.stat_key].min
        end
    end
end
 
--
-- argument mapping
--
-- format:
-- tpl_args key = {
--  no_copy = true or nil          -- When loading an base item, dont copy this key  
--  property = 'prop',              -- Property associated with this key
--  property_func = function or nil -- Function to unpack the property into a native lua value.  
--                                      If not specified, func is used.  
--                                      If neither is specified, value is copied as string
--  func = function or nil          -- Function to unpack the argument into a native lua value and validate it.  
--                                      If not specified, value will not be set.
--  default = object                -- Default value if the parameter is nil
-- }
core.map = {
     -- special params
     html = {
         no_copy = true,
         property = 'Has infobox HTML',
         func = nil,
     },
     implicit_stat_text = {
         property = 'Has implicit stat text',
         func = function(tpl_args, frame)
             tpl_args.implicit_stat_text = core.process_mod_stats(tpl_args, {type='implicit'})
         end,
     },
     explicit_stat_text = {
         property = 'Has explicit stat text',
         func = function(tpl_args, frame)
             tpl_args.explicit_stat_text = core.process_mod_stats(tpl_args, {type='explicit'})
              
             if tpl_args.is_talisman or tpl_args.is_corrupted then
                if tpl_args.explicit_stat_text == nil or tpl_args.explicit_stat_text == '' then
                    tpl_args.explicit_stat_text = util.html.poe_color('corrupted', i18n.tooltips.corrupted)
                 else
                    tpl_args.explicit_stat_text = (tpl_args.explicit_stat_text or '') .. '<br>' .. util.html.poe_color('corrupted', i18n.tooltips.corrupted)
                end
             end
             end
         end,
         end,
Line 899: Line 1,200:
     stat_text = {
     stat_text = {
         property = 'Has stat text',
         property = 'Has stat text',
         func = function()
         func = function(tpl_args, frame)
             local sep = ''
             local sep = ''
             if g_args.implicit_stat_text and g_args.explicit_stat_text then
             if tpl_args.implicit_stat_text and tpl_args.explicit_stat_text then
                 sep = string.format('<span class="item-stat-separator -%s"></span>', g_args.frame_type)
                 sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
             end
             end
             local text = (g_args.implicit_stat_text or '') .. sep .. (g_args.explicit_stat_text or '')
             local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
              
              
             if string.len(text) > 0 then
             if string.len(text) > 0 then
                 g_args.stat_text = text
                 tpl_args.stat_text = text
             end
             end
         end,
         end,
Line 949: Line 1,250:
         property = 'Has maximum drop level',
         property = 'Has maximum drop level',
         func = core.factory.number_cast('drop_level_maximum'),
         func = core.factory.number_cast('drop_level_maximum'),
    },
    drop_leagues = {
        no_copy = true,
        property = 'Has league drop restrictions',
        func = core.factory.assoc_table_cast('drop_leagues', {tbl=m_game.constants.leagues, errmsg=i18n.errors.invalid_league}),
     },
     },
     required_level = {
     required_level = {
Line 957: Line 1,263:
     required_level_final = {
     required_level_final = {
         property = 'Has level requirement',
         property = 'Has level requirement',
         func = function ()
         func = function(tpl_args, frame)
             g_args.required_level_final = g_args.required_level
             tpl_args.required_level_final = tpl_args.required_level
         end,
         end,
         default = 1,
         default = 1,
Line 980: Line 1,286:
         no_copy = true,
         no_copy = true,
         property = 'Has inventory icon',
         property = 'Has inventory icon',
         func = function ()
         func = function(tpl_args, frame)
             g_args.inventory_icon_id = g_args.inventory_icon or g_args.name
             tpl_args.inventory_icon_id = tpl_args.inventory_icon or tpl_args.name
             g_args.inventory_icon = string.format('File:%s inventory icon.png', g_args.inventory_icon_id)  
             tpl_args.inventory_icon = string.format(i18n.inventory_icon, tpl_args.inventory_icon_id)  
         end,
         end,
     },
     },
     -- note: this must be called after inventory item to work correctly as it depends on g_args.inventory_icon_id being set
     -- note: this must be called after inventory item to work correctly as it depends on tpl_args.inventory_icon_id being set
     alternate_art_inventory_icons = {
     alternate_art_inventory_icons = {
         no_copy = true,
         no_copy = true,
         property = 'Has alternate inventory icons',
         property = 'Has alternate inventory icons',
         func = function ()
         func = function(tpl_args, frame)
             local icons = {}
             local icons = {}
             if g_args.alternate_art_inventory_icons ~= nil then
             if tpl_args.alternate_art_inventory_icons ~= nil then
                 local names = util.string.split(g_args.alternate_art_inventory_icons, ', ')
                 local names = util.string.split(tpl_args.alternate_art_inventory_icons, ', ')
                  
                  
                 for _, name in ipairs(names) do
                 for _, name in ipairs(names) do
                     icons[#icons+1] = string.format('File:%s %s inventory icon.png', g_args.inventory_icon_id, name)  
                     icons[#icons+1] = string.format(i18n.inventory_icon, string.format('%s %s', tpl_args.inventory_icon_id, name))
                 end
                 end
             end
             end
             g_args.alternate_art_inventory_icons = icons
             tpl_args.alternate_art_inventory_icons = icons
         end,
         end,
         default = {},
         default = {},
Line 1,004: Line 1,310:
     buff_icon = {
     buff_icon = {
         property = 'Has buff icon',
         property = 'Has buff icon',
         func = function()
         func = function(tpl_args, frame)
             g_args.buff_icon = string.format('File:%s status icon.png', g_args.name)  
             tpl_args.buff_icon = string.format(i18n.status_icon, tpl_args.name)  
         end,
         end,
     },
     },
Line 1,019: Line 1,325:
     tags = {
     tags = {
         property = 'Has tags',
         property = 'Has tags',
         property_func = function ()
         property_func = function(tpl_args, frame)
             g_args.tags = util.string.split(g_args.tags, '<MANY>')
             tpl_args.tags = util.string.split(tpl_args.tags, '<MANY>')
         end,
         end,
         func = core.factory.array_table_cast('tags', {
         func = core.factory.assoc_table_cast('tags', {
             tbl = m_game.constants.tags,
             tbl = m_game.constants.tags,
             errmsg = '%s is not a valid tag',
             errmsg = i18n.errors.invalid_tag,
         }),
         }),
     },
     },
Line 1,036: Line 1,342:
         property = 'Is corrupted',
         property = 'Is corrupted',
         func = core.factory.boolean_cast('is_corrupted'),  
         func = core.factory.boolean_cast('is_corrupted'),  
        default = false,
    },
    is_relic = {
        no_copy = true,
        property = 'Is relic',
        func = function(tpl_args, frame)
            core.factory.boolean_cast('is_relic')(tpl_args, frame)
            if tpl_args.is_relic == true and tpl_args.rarity ~= 'Unique' then
                error(i18n.errors.non_unique_relic)
            end
        end,
        default = false,
     },
     },
     --
     --
Line 1,044: Line 1,362:
         property = 'Is talisman',
         property = 'Is talisman',
         func = core.factory.boolean_cast('is_talisman'),
         func = core.factory.boolean_cast('is_talisman'),
        default = false,
    },
   
    talisman_tier = {
        property = 'Has talisman tier',
        func = core.factory.number_cast('talisman_tier'),
     },
     },
      
      
Line 1,073: Line 1,397:
     buff_values = {
     buff_values = {
         property = nil,
         property = nil,
         property_func = function ()
         property_func = function(tpl_args, frame)
             local results = util.smw.query({
             local results = util.smw.query({
                 string.format('[[-Has subobject::%s]] [[Is number::+]] [[Has buff value::+]]', g_args.base_item_page),
                 string.format('[[-Has subobject::%s]] [[Is number::+]] [[Has buff value::+]]', tpl_args.base_item_page),
                 '?Is number#',
                 '?Is number#',
                 '?Has buff value#',
                 '?Has buff value#',
             }, g_frame)
             }, frame)
             local values = {}
             local values = {}
             for _, row in ipairs(results) do
             for _, row in ipairs(results) do
Line 1,086: Line 1,410:
                 if i ~= nil then
                 if i ~= nil then
                     local key = 'buff_value' .. i
                     local key = 'buff_value' .. i
                     g_args[key] = value
                     tpl_args[key] = value
                     g_args._subobjects[key] = {
                     tpl_args._subobjects[key] = {
                         ['Is number'] = i,
                         ['Is number'] = i,
                         ['Has buff value'] = value,
                         ['Has buff value'] = value,
Line 1,094: Line 1,418:
             end
             end
              
              
             g_args.buff_values = values
             tpl_args.buff_values = values
         end,
         end,
         func = function ()
         func = function(tpl_args, frame)
             local values = {}
             local values = {}
             local i = 0
             local i = 0
Line 1,104: Line 1,428:
                 i = i + 1
                 i = i + 1
                 key = 'buff_value' .. i
                 key = 'buff_value' .. i
                 value = tonumber(g_args[key])
                 value = tonumber(tpl_args[key])
                 values[i] = value
                 values[i] = value
                 g_args[key] = value
                 tpl_args[key] = value
                 g_args._subobjects[key] = {
                 tpl_args._subobjects[key] = {
                     ['Is number'] = i,
                     ['Is number'] = i,
                     ['Has buff value'] = value,
                     ['Has buff value'] = value,
Line 1,113: Line 1,437:
             until values[i] == nil
             until values[i] == nil
              
              
             g_args.buff_values = values
             tpl_args.buff_values = values
         end,
         end,
     },
     },
Line 1,131: Line 1,455:
         func = core.factory.number_cast('attack_speed'),
         func = core.factory.number_cast('attack_speed'),
     },
     },
     damage_min = {
     physical_damage_min = {
         property = 'Has base minimum physical damage',
         property = 'Has base minimum physical damage',
         func = core.factory.number_cast('damage_min'),
         func = core.factory.number_cast('physical_damage_min'),
     },
     },
     damage_max = {
     physical_damage_max = {
         property = 'Has base maximum physical damage',
         property = 'Has base maximum physical damage',
         func = core.factory.number_cast('damage_max'),
         func = core.factory.number_cast('physical_damage_max'),
     },
     },
     range = {
     range = {
Line 1,183: Line 1,507:
     primary_attribute = {
     primary_attribute = {
         property = 'Has primary attribute',
         property = 'Has primary attribute',
         func = function()
         func = function(tpl_args, frame)
             for _, attr in ipairs(m_game.constants.attributes) do
             for _, attr in ipairs(m_game.constants.attributes) do
                 local val = g_args[attr.long_lower .. '_percent']  
                 local val = tpl_args[attr.long_lower .. '_percent']  
                 if val and val >= 60 then
                 if val and val >= 60 then
                     g_args['primary_attribute'] = attr.long_upper
                     tpl_args['primary_attribute'] = attr.long_upper
                     return
                     return
                 end
                 end
             end
             end
             g_args['primary_attribute'] = 'None'
             tpl_args['primary_attribute'] = 'None'
         end,
         end,
     },
     },
Line 1,199: Line 1,523:
         func = core.factory.array_table_cast('gem_tags', {
         func = core.factory.array_table_cast('gem_tags', {
             tbl = m_game.constants.item.gem_tags,
             tbl = m_game.constants.item.gem_tags,
             errmsg = '%s is not a valid tag',
             errmsg = i18n.errors.invalid_tag,
         }),
         }),
         default = {},
         default = {},
Line 1,209: Line 1,533:
     skill_screenshot = {
     skill_screenshot = {
         property = 'Has skill screenshot',
         property = 'Has skill screenshot',
         func = function ()
         func = function(tpl_args, frame)
             g_args.skill_screenshot = string.format('File:%s skill screenshot.jpg', g_args.name)
             tpl_args.skill_screenshot = string.format(i18n.skill_screenshot, tpl_args.name)
         end,
         end,
     },
     },
Line 1,216: Line 1,540:
     gem_icon = {
     gem_icon = {
         property = 'Has skill gem icon',
         property = 'Has skill gem icon',
         func = function ()
         func = function(tpl_args, frame)
             -- TODO readd support if needed.
             -- TODO readd support if needed.
             g_args.gem_icon = string.format('File:%s skill icon.png', g_args.name)  
             tpl_args.gem_icon = string.format(i18n.skill_icon, tpl_args.name)  
         end,
         end,
     },
     },
Line 1,224: Line 1,548:
     support_gem_letter_html = {
     support_gem_letter_html = {
         property = 'Has support gem letter HTML',
         property = 'Has support gem letter HTML',
         func = function ()
         func = function(tpl_args, frame)
             if g_args.support_gem_letter == nil then
             if tpl_args.support_gem_letter == nil then
                 return
                 return
             end
             end
Line 1,238: Line 1,562:
             for k, v in pairs(css_map) do
             for k, v in pairs(css_map) do
                 k = string.format('%s_percent', k)
                 k = string.format('%s_percent', k)
                 if g_args[k] and g_args[k] > 50 then
                 if tpl_args[k] and tpl_args[k] > 50 then
                     id = v
                     id = v
                     break
                     break
Line 1,248: Line 1,572:
                 container
                 container
                     :attr('class', string.format('support-gem-id-%s', id))
                     :attr('class', string.format('support-gem-id-%s', id))
                     :wikitext(g_args.support_gem_letter)
                     :wikitext(tpl_args.support_gem_letter)
                     :done()
                     :done()
                 g_args.support_gem_letter_html = tostring(container)
                 tpl_args.support_gem_letter_html = tostring(container)
             end
             end
         end,
         end,
Line 1,260: Line 1,584:
     },
     },
     map_guild_character = {
     map_guild_character = {
        no_copy = true,
         property = 'Has map guild character',
         property = 'Has map guild character',
         func = nil,
         func = nil,
     },
     },
     map_area_id = {
     map_area_id = {
        no_copy = true,
         property = 'Has map area id',
         property = 'Has map area id',
         func = nil, -- TODO: Validate against a query?
         func = nil, -- TODO: Validate against a query?
     },
     },
     map_area_level = {
     map_area_level = {
        no_copy = true,
         property = 'Has map area level',
         property = 'Has map area level',
         func = core.factory.number_cast('map_area_level'),
         func = core.factory.number_cast('map_area_level'),
     },
     },
     unique_map_guild_character = {
     unique_map_guild_character = {
         property = 'Has unqiue map guild character',
         property = 'Has unique map guild character',
        property_func = function(tpl_args, frame)
            tpl_args.map_guild_character = tpl_args.unique_map_guild_character
            tpl_args.unique_map_guild_character = nil
        end,
         func = nil,
         func = nil,
     },
     },
Line 1,278: Line 1,609:
         property = 'Has unique map area id',
         property = 'Has unique map area id',
         func = nil, -- TODO: Validate against a query?
         func = nil, -- TODO: Validate against a query?
        property_func = function(tpl_args, frame)
            tpl_args.map_area_id = tpl_args.unique_map_area_id
            tpl_args.unique_map_area_id = nil
        end,
     },
     },
     unique_map_area_level = {
     unique_map_area_level = {
         property = 'Has unique map area level',
         property = 'Has unique map area level',
         func = core.factory.number_cast('unique_map_area_level'),
         func = core.factory.number_cast('unique_map_area_level'),
        property_func = function(tpl_args, frame)
            tpl_args.map_area_level = tpl_args.unique_map_area_level
            tpl_args.unique_map_area_level = nil
        end,
     },
     },
     --
     --
Line 1,360: Line 1,699:
         property = 'Has prophecy seal cost in merciless difficulty',
         property = 'Has prophecy seal cost in merciless difficulty',
         func = core.factory.number_cast('seal_cost_merciless'),
         func = core.factory.number_cast('seal_cost_merciless'),
    },
    -- Divination cards
    card_art = {
        proprety = 'Has divination card art',
        func = function(tpl_args, frame)
            tpl_args.card_art = string.format(i18n.divination_card_art, tpl_args.name)
        end,
    },
    card_reward = {
        property = 'Has divinication card reward',
        func = nil,
     },
     },
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
Line 1,369: Line 1,719:
         no_copy = true,
         no_copy = true,
         property = 'Has base item',
         property = 'Has base item',
         func = function ()
         func = function(tpl_args, frame)
             g_args.base_item = g_args.base_item_data['Has name']
             tpl_args.base_item = tpl_args.base_item_data['Has name']
         end,
         end,
     },
     },
Line 1,376: Line 1,726:
         no_copy = true,
         no_copy = true,
         property = 'Has base item metadata id',
         property = 'Has base item metadata id',
         func = function()
         func = function(tpl_args, frame)
             g_args.base_item_id = g_args.base_item_data['Has metadata id']
             tpl_args.base_item_id = tpl_args.base_item_data['Has metadata id']
         end,
         end,
     },
     },
Line 1,383: Line 1,733:
         no_copy = true,
         no_copy = true,
         property = 'Has base item wiki page',
         property = 'Has base item wiki page',
         func = function()
         func = function(tpl_args, frame)
             g_args.base_item_page = g_args.base_item_data[1]
             tpl_args.base_item_page = tpl_args.base_item_data[1]
         end,
         end,
     },
     },
Line 1,391: Line 1,741:
         no_copy = true,
         no_copy = true,
         property = 'Has names',
         property = 'Has names',
         func = function ()
         func = function(tpl_args, frame)
             if g_args.name_list ~= nil then
             if tpl_args.name_list ~= nil then
                 g_args.name_list = util.string.split(g_args.name_list, ', ')
                 tpl_args.name_list = util.string.split(tpl_args.name_list, ', ')
                 g_args.name_list[#g_args.name_list+1] = g_args.name
                 tpl_args.name_list[#tpl_args.name_list+1] = tpl_args.name
             else
             else
                 g_args.name_list = {g_args.name}
                 tpl_args.name_list = {tpl_args.name}
             end
             end
         end,
         end,
Line 1,403: Line 1,753:
         no_copy = true,
         no_copy = true,
         property = 'Has lowercase names',
         property = 'Has lowercase names',
         func = function()
         func = function(tpl_args, frame)
             g_args.name_list_lower = {}
             tpl_args.name_list_lower = {}
             for index, value in ipairs(g_args.name_list) do
             for index, value in ipairs(tpl_args.name_list) do
                 g_args.name_list_lower[index] = string.lower(value)
                 tpl_args.name_list_lower[index] = mw.ustring.lower(value)
             end
             end
         end,
         end,
Line 1,413: Line 1,763:
         no_copy = true,
         no_copy = true,
         property = 'Has gem tags difference',
         property = 'Has gem tags difference',
         func = function()
         func = function(tpl_args, frame)
             if g_args.gem_tags ~= nil then
             if tpl_args.gem_tags ~= nil then
                 local gtags = {}
                 local gtags = {}
                 -- copy tags
                 -- copy tags
Line 1,423: Line 1,773:
                 end
                 end
                 -- delete existing tags
                 -- delete existing tags
                 for _, tag in ipairs(g_args.gem_tags) do
                 for _, tag in ipairs(tpl_args.gem_tags) do
                     gtags[tag] = nil
                     gtags[tag] = nil
                 end
                 end
                  
                  
                 -- add them as ordered list and not as hash table so it is consistent with the other gem tag list
                 -- add them as ordered list and not as hash table so it is consistent with the other gem tag list
                 g_args.gem_tags_difference = {}
                 tpl_args.gem_tags_difference = {}
                 for key, value in pairs(gtags) do
                 for key, value in pairs(gtags) do
                     g_args.gem_tags_difference[#g_args.gem_tags_difference+1] = key
                     tpl_args.gem_tags_difference[#tpl_args.gem_tags_difference+1] = key
                 end
                 end
             end
             end
Line 1,438: Line 1,788:
         no_copy = true,
         no_copy = true,
         property = nil,
         property = nil,
         func = function ()
         func = function(tpl_args, frame)
             if g_args.name == 'Prophecy' or g_args.base_item == 'Prophecy' then
             if tpl_args.name == 'Prophecy' or tpl_args.base_item == 'Prophecy' then
                 g_args.frame_type = 'prophecy'
                 tpl_args.frame_type = 'prophecy'
                 return
                 return
             end
             end
          
          
             local var = core.class_specifics[g_args.class]
             local var = core.class_specifics[tpl_args.class]
             if var ~= nil and var.frame_type ~= nil then
             if var ~= nil and var.frame_type ~= nil then
                 g_args.frame_type = var.frame_type
                 tpl_args.frame_type = var.frame_type
                 return
                 return
             end
             end
              
              
             g_args.frame_type = string.lower(g_args.rarity)
             if tpl_args.is_relic then
                tpl_args.frame_type = 'relic'
                return
            end
           
            tpl_args.frame_type = string.lower(tpl_args.rarity)
         end,
         end,
     },
     },
Line 1,463: Line 1,818:
     implicit_mods = {
     implicit_mods = {
         property = 'Has implicit mod ids',
         property = 'Has implicit mod ids',
         property_func = function ()
         property_func = function (tpl_args)
             g_args.implicit_mods = util.string.split(g_args.implicit_mods, '<MANY>')
             tpl_args.implicit_mods = util.string.split(tpl_args.implicit_mods, '<MANY>')
             for _, modid in ipairs(g_args.implicit_mods) do  
             for _, modid in ipairs(tpl_args.implicit_mods) do  
                 g_args.mods[#g_args.mods+1] = modid
                 tpl_args.mods[#tpl_args.mods+1] = modid
                 g_args._mods[#g_args._mods+1] = {
                 tpl_args._mods[#tpl_args._mods+1] = {
                     result=nil,
                     result=nil,
                     modid=modid,
                     modid=modid,
Line 1,484: Line 1,839:
         no_copy = true,
         no_copy = true,
         property = nil,
         property = nil,
         func = core.factory.damage_html{key='', write_key='physical_'},
         func = core.factory.damage_html{key='physical'},
     },
     },
     fire_damage_html = {
     fire_damage_html = {
Line 1,505: Line 1,860:
         property = nil,
         property = nil,
         func = core.factory.damage_html{key='chaos'},
         func = core.factory.damage_html{key='chaos'},
    },
    damage_avg = {
        no_copy = true,
        property = 'Has average damage',
        func = function(tpl_args, frame)
            local dmg = {min=0, max=0}
            for key, _ in pairs(dmg) do
                for _, data in ipairs(m_game.constants.damage_types) do
                    dmg[key] = dmg[key] + tpl_args[string.format('%s_damage_%s_range_average', data.short_lower, key)]
                end
            end
           
            dmg = (dmg.min + dmg.max) / 2
           
            tpl_args.damage_avg = dmg
        end,
     },
     },
     damage_html = {
     damage_html = {
         no_copy = true,
         no_copy = true,
         property = 'Has damage HTML',
         property = 'Has damage HTML',
         func = function ()
         func = function(tpl_args, frame)
             local text = {}
             local text = {}
             for _, data in ipairs(m_game.constants.damage_types) do
             for _, data in ipairs(m_game.constants.damage_types) do
                 local value = g_args[data.short_lower .. '_damage_html']
                 local value = tpl_args[data.short_lower .. '_damage_html']
                 if value ~= nil then
                 if value ~= nil then
                     text[#text+1] = value
                     text[#text+1] = value
Line 1,518: Line 1,889:
             end
             end
             if #text > 0 then
             if #text > 0 then
                 g_args.damage_html = table.concat(text, '<br>')
                 tpl_args.damage_html = table.concat(text, '<br>')
            end
        end,
    },
    item_limit = {
        no_copy = true,
        property = 'Has item limit',
        func = core.factory.number_cast('item_limit'),
    },
    jewel_radius = {
        no_copy = true,
        property = 'Has jewel radius',
        func = core.factory.copy_stats{key='jewel_radius', stat_key='local_jewel_effect_base_radius'}
    },
    jewel_radius_html = {
        no_copy = true,
        property = 'Has jewel radius HTML',
        func = function(tpl_args, frame)
            if tpl_args.jewel_radius then
                tpl_args.jewel_radius_html = string.format('%s (%i)', (m_game.constants.item.jewel_radius_to_size[tpl_args.jewel_radius] or '?'), tpl_args.jewel_radius)
             end
             end
         end,
         end,
Line 1,535: Line 1,925:
         },
         },
     },
     },
     damage_min = {
     physical_damage_min = {
         property = 'Has minimum physical damage',
         property = 'Has minimum physical damage',
         stats_add = {
         stats_add = {
Line 1,548: Line 1,938:
         },
         },
     },
     },
     damage_max = {
     physical_damage_max = {
         property = 'Has maximum physical damage',
         property = 'Has maximum physical damage',
         stats_add = {
         stats_add = {
Line 1,664: Line 2,054:
         stats_increased = {
         stats_increased = {
             'local_critical_strike_chance_+%',
             'local_critical_strike_chance_+%',
        },
        stats_override = {
            ['local_weapon_always_crit'] = {min=100, max=100},
         },
         },
         minimum = 0,
         minimum = 0,
Line 1,687: Line 2,080:
         stats_increased = {
         stats_increased = {
             'local_flask_life_to_recover_+%',
             'local_flask_life_to_recover_+%',
            'local_flask_amount_to_recover_+%',
         },
         },
         html_fmt_options = {
         html_fmt_options = {
Line 1,699: Line 2,093:
         stats_increased = {
         stats_increased = {
             'local_flask_mana_to_recover_+%',
             'local_flask_mana_to_recover_+%',
            'local_flask_amount_to_recover_+%',
         },
         },
     },
     },
Line 1,704: Line 2,099:
         property = 'Has flask duration',
         property = 'Has flask duration',
         stats_increased = {
         stats_increased = {
            'local_flask_duration_+%',
        },
        stats_increased_inverse = {
             'local_flask_recovery_speed_+%',
             'local_flask_recovery_speed_+%',
            'local_flask_duration_+%',
         },
         },
         minimum = 0,
         minimum = 0,
Line 1,729: Line 2,126:
         stats_increased = {
         stats_increased = {
             'local_max_charges_+%',
             'local_max_charges_+%',
            'local_charges_added_+%',
         },
         },
         minimum = 0,
         minimum = 0,
Line 1,788: Line 2,184:
             'local_evasion_and_energy_shield_+%',
             'local_evasion_and_energy_shield_+%',
             'local_armour_and_evasion_and_energy_shield_+%',
             'local_armour_and_evasion_and_energy_shield_+%',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
         },
         },
         minimum = 0,
         minimum = 0,
Line 1,800: Line 2,199:
         },
         },
         stats_increased = {
         stats_increased = {
            'local_dexterity_requirement_+%',
             'local_attribute_requirements_+%',
             'local_attribute_requirements_+%',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
         },
         },
         minimum = 0,
         minimum = 0,
Line 1,813: Line 2,216:
         },
         },
         stats_increased = {
         stats_increased = {
            'local_intelligence_requirement_+%',
             'local_attribute_requirements_+%',
             'local_attribute_requirements_+%',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
         },
         },
         minimum = 0,
         minimum = 0,
Line 1,826: Line 2,233:
         },
         },
         stats_increased = {
         stats_increased = {
            'local_strength_requirement_+%',
             'local_attribute_requirements_+%',
             'local_attribute_requirements_+%',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
         },
         },
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
             fmt = '%i',
             fmt = '%i',
        },
    },
    map_area_level = {
        property = 'Has map area level',
        stats_override = {
            ['map_item_level_override'] = true,
         },
         },
     },
     },
Line 1,839: Line 2,256:
         name = 'physical_dps',
         name = 'physical_dps',
         property = 'physical damage per second',
         property = 'physical damage per second',
         damage_args = {'damage', },
         damage_args = {'physical_damage', },
         label = util.html.abbr('pDPS', 'physical damage per second'),
         label = i18n.item_table.physical_dps,
         html_fmt_options = {
         html_fmt_options = {
             color = 'value',
             color = 'value',
Line 1,850: Line 2,267:
         property = 'fire damage per second',
         property = 'fire damage per second',
         damage_args = {'fire_damage'},
         damage_args = {'fire_damage'},
         label = util.html.abbr('Fire DPS', 'fire damage per second'),
         label = i18n.item_table.fire_dps,
         html_fmt_options = {
         html_fmt_options = {
             color = 'fire',
             color = 'fire',
Line 1,860: Line 2,277:
         property = 'cold damage per second',
         property = 'cold damage per second',
         damage_args = {'cold_damage'},
         damage_args = {'cold_damage'},
         label = util.html.abbr('Cold DPS', 'cold damage per second'),
         label = i18n.item_table.cold_dps,
         html_fmt_options = {
         html_fmt_options = {
             color = 'cold',
             color = 'cold',
Line 1,870: Line 2,287:
         property = 'lightning damage per second',
         property = 'lightning damage per second',
         damage_args = {'lightning_damage'},
         damage_args = {'lightning_damage'},
         label = util.html.abbr('Light. DPS', 'lightning damage per second'),
         label = i18n.item_table.lightning_dps,
         html_fmt_options = {
         html_fmt_options = {
             color = 'lightning',
             color = 'lightning',
Line 1,880: Line 2,297:
         property = 'chaos damage per second',
         property = 'chaos damage per second',
         damage_args = {'chaos_damage'},
         damage_args = {'chaos_damage'},
         label = util.html.abbr('Chaos DPS', 'chaos damage per second'),
         label = i18n.item_table.chaos_dps,
         html_fmt_options = {
         html_fmt_options = {
             color = 'chaos',
             color = 'chaos',
Line 1,890: Line 2,307:
         property = 'elemental damage per second',
         property = 'elemental damage per second',
         damage_args = {'fire_damage', 'cold_damage', 'lightning_damage'},
         damage_args = {'fire_damage', 'cold_damage', 'lightning_damage'},
         label = util.html.abbr('eDPS', 'elemental damage (i.e. fire/cold/lightning) per second'),
         label = i18n.item_table.elemental_dps,
         html_fmt_options = {
         html_fmt_options = {
             color = 'value',
             color = 'value',
Line 1,899: Line 2,316:
         name = 'poison_dps',
         name = 'poison_dps',
         property = 'poison damage per second',
         property = 'poison damage per second',
         damage_args = {'damage', 'chaos_damage'},
         damage_args = {'physical_damage', 'chaos_damage'},
         label = util.html.abbr('Poison DPS', 'poison damage (i.e. physical/chaos) per second'),
         label = i18n.item_table.poison_dps,
         html_fmt_options = {
         html_fmt_options = {
             color = 'value',
             color = 'value',
Line 1,909: Line 2,326:
         name = 'dps',
         name = 'dps',
         property = 'damage per second',
         property = 'damage per second',
         damage_args = {'damage', 'fire_damage', 'cold_damage', 'lightning_damage', 'chaos_damage'},
         damage_args = {'physical_damage', 'fire_damage', 'cold_damage', 'lightning_damage', 'chaos_damage'},
         label = util.html.abbr('DPS', 'total damage (i.e. physical/fire/cold/lightning/chaos) per second'),
         label = i18n.item_table.dps,
         html_fmt_options = {
         html_fmt_options = {
             color = 'value',
             color = 'value',
Line 1,918: Line 2,335:
}
}


-- TODO: Second pass for i18n item classes
-- base item is default, but will be validated later
-- base item is default, but will be validated later
-- Notes:
-- Notes:
--  inventory_icon must always be before alternate_art_inventory_icons  
--  inventory_icon must always be before alternate_art_inventory_icons  
core.default_args = {'class', 'rarity', 'name', 'name_list', 'name_list_lower', 'size_x', 'size_y', 'drop_enabled', 'drop_level', 'drop_level_maximum', 'required_level', 'required_level_final', 'inventory_icon', 'alternate_art_inventory_icons', 'flavour_text', 'help_text', 'tags', 'metadata_id', 'mods', 'implicit_mods', 'explicit_mods'}
--  is_relic after rarity
core.default_args = {'class', 'rarity', 'name', 'name_list', 'name_list_lower', 'size_x', 'size_y', 'drop_enabled', 'drop_level', 'drop_level_maximum', 'drop_leagues', 'required_level', 'required_level_final', 'inventory_icon', 'alternate_art_inventory_icons', 'flavour_text', 'help_text', 'tags', 'metadata_id', 'is_corrupted', 'is_relic', 'mods', 'implicit_mods', 'explicit_mods',}
-- frame_type is needed in stat_text
-- frame_type is needed in stat_text
core.late_args = {'frame_type', 'implicit_stat_text', 'explicit_stat_text', 'stat_text'}
core.late_args = {'frame_type', 'implicit_stat_text', 'explicit_stat_text', 'stat_text'}
Line 1,932: Line 2,351:
     weapons = {
     weapons = {
         keys = {['Claws'] = true, ['Daggers'] = true, ['Wands'] = true, ['One Hand Swords'] = true, ['Thrusting One Hand Swords'] = true, ['One Hand Axes'] = true, ['One Hand Maces'] = true, ['Bows'] = true, ['Staves'] = true, ['Two Hand Swords'] = true, ['Two Hand Axes'] = true, ['Two Hand Maces'] = true, ['Sceptres'] = true, ['Fishing Rods'] = true},
         keys = {['Claws'] = true, ['Daggers'] = true, ['Wands'] = true, ['One Hand Swords'] = true, ['Thrusting One Hand Swords'] = true, ['One Hand Axes'] = true, ['One Hand Maces'] = true, ['Bows'] = true, ['Staves'] = true, ['Two Hand Swords'] = true, ['Two Hand Axes'] = true, ['Two Hand Maces'] = true, ['Sceptres'] = true, ['Fishing Rods'] = true},
         args = {'required_dexterity', 'required_intelligence', 'required_strength', 'critical_strike_chance', 'attack_speed', 'damage_min', 'damage_max', 'range'},
         args = {'required_dexterity', 'required_intelligence', 'required_strength', 'critical_strike_chance', 'attack_speed', 'physical_damage_min', 'physical_damage_max', 'range'},
         late_args = {'physical_damage_html', 'fire_damage_html', 'cold_damage_html', 'lightning_damage_html', 'chaos_damage_html', 'damage_html'},
         late_args = {'physical_damage_html', 'fire_damage_html', 'cold_damage_html', 'lightning_damage_html', 'chaos_damage_html', 'damage_avg', 'damage_html'},
     },
     },
     gems = {
     gems = {
Line 1,951: Line 2,370:
core.class_specifics = {
core.class_specifics = {
     ['Amulets'] = {
     ['Amulets'] = {
         args = {'is_talisman'},
         args = {'is_talisman', 'talisman_tier'},
     },
     },
     ['Life Flasks'] = {
     ['Life Flasks'] = {
Line 1,971: Line 2,390:
         args = {'skill_screenshot', 'gem_icon'},
         args = {'skill_screenshot', 'gem_icon'},
         defaults = {
         defaults = {
             help_text = 'Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.',
             help_text = i18n.help_text_defaults.active_gem,
             size_x = 1,
             size_x = 1,
             size_y = 1,
             size_y = 1,
Line 1,980: Line 2,399:
         args = {'support_gem_letter_html'},
         args = {'support_gem_letter_html'},
         defaults = {
         defaults = {
             help_text = 'This is a Support Gem. It does not grant a bonus to your character, but skills in sockets connected to it. Place into an item socket connected to a socket containing the Active Skill Gem you wish to augment. Right click to remove from a socket.',
             help_text = i18n.help_text_defaults.support_gem,
             size_x = 1,
             size_x = 1,
             size_y = 1,
             size_y = 1,
Line 1,991: Line 2,410:
     ['Maps'] = {
     ['Maps'] = {
         args = {'map_tier', 'map_guild_character', 'map_area_id', 'map_area_level', 'unique_map_area_id', 'unique_map_area_level', 'unique_map_guild_character'},
         args = {'map_tier', 'map_guild_character', 'map_area_id', 'map_area_level', 'unique_map_area_id', 'unique_map_area_level', 'unique_map_guild_character'},
         skip_stat_lines = {
         skip_stat_lines = i18n.stat_skip_patterns.maps,
            '%d+%% increased Quantity of Items found in this Area',
            '%d+%% increased Rarity of Items found in this Area',
            '%+%d+%% Monster pack size',
            -- ranges
            '%(%d+%-%d+%)%% increased Quantity of Items found in this Area',
            '%(%d+%-%d+%)%% increased Rarity of Items found in this Area',
            '%+%(%d+%-%d+%)%% Monster pack size',
        },
     },
     },
     ['Currency'] = {
     ['Currency'] = {
Line 2,014: Line 2,425:
         args = {'is_master_doodad', 'master', 'master_level_requirement', 'master_favour_cost', 'variation_count'},
         args = {'is_master_doodad', 'master', 'master_level_requirement', 'master_favour_cost', 'variation_count'},
         defaults = {
         defaults = {
             help_text = 'Right click on this item then left click on a location on the ground to create the object.',
             help_text = i18n.help_text_defaults.hideout_doodad,
         },
         },
         frame_type = 'currency',
         frame_type = 'currency',
     },
     },
     ['Jewel'] = {
     ['Jewel'] = {
        late_args = {'item_limit', 'jewel_radius', 'jewel_radius_html'},
         defaults = {
         defaults = {
             help_text = 'Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.',
             help_text = i18n.help_text_defaults.jewel,
        },
        skip_stat_lines = {
            'Limited to %d+ %(Hidden%)',
            'Jewel has a radius of %d+ %(Hidden%)',
         },
         },
        skip_stat_lines = i18n.stat_skip_patterns.jewels,
     },
     },
     ['Quest Items'] = {
     ['Quest Items'] = {
Line 2,031: Line 2,440:
     },
     },
     ['Divination Card'] = {
     ['Divination Card'] = {
        args = {'card_art', 'card_reward'},
         frame_type = 'divicard',
         frame_type = 'divicard',
     },
     },
Line 2,098: Line 2,508:
         },
         },
         {
         {
             args = function ()
             args = function(tpl_args, frame)
                 if g_args.class == nil then  
                 if tpl_args.class == nil then  
                     return false  
                     return false  
                 end
                 end
                  
                  
                 return core.class_groups.weapons.keys[g_args.class] ~= nil
                 return core.class_groups.weapons.keys[tpl_args.class] ~= nil
             end,
             end,
             func = core.factory.display_value{
             func = core.factory.display_value{
Line 2,116: Line 2,526:
         {
         {
             args = {'gem_tags'},
             args = {'gem_tags'},
             func = function ()
             func = function(tpl_args, frame)
                 local out = {}
                 local out = {}
                 for i, tag in ipairs(g_args.gem_tags) do
                 for i, tag in ipairs(tpl_args.gem_tags) do
                     out[#out+1] = string.format('[[:Category:%s (gem tag)|%s]]', tag, tag)
                     out[#out+1] = string.format(i18n.gem_tag_category, tag, tag)
                 end
                 end
                  
                  
Line 2,131: Line 2,541:
                     [1] = {
                     [1] = {
                         key = 'support_gem_letter_html',
                         key = 'support_gem_letter_html',
                         before = 'Icon: ',
                         inline = i18n.tooltips.support_icon,
                     },
                     },
                 },
                 },
Line 2,142: Line 2,552:
                     [1] = {
                     [1] = {
                         key = 'radius',
                         key = 'radius',
                         before = 'Radius: ',
                         inline = i18n.tooltips.radius,
                         func = core.factory.descriptor_value{key='radius_description'},
                         func = core.factory.descriptor_value{key='radius_description'},
                     },
                     },
                     [2] = {
                     [2] = {
                         key = 'radius_seceondary',
                         key = 'radius_secondary',
                         before = ' / ',
                         inline = ' / %s',
                         func = core.factory.descriptor_value{key='radius_secondary_description'},
                         func = core.factory.descriptor_value{key='radius_secondary_description'},
                     },
                     },
                     [3] = {
                     [3] = {
                         key = 'radius_tertiary',
                         key = 'radius_tertiary',
                         before = ' / ',
                         inline = ' / %s',
                         func = core.factory.descriptor_value{key='radius_tertiary_description'},
                         func = core.factory.descriptor_value{key='radius_tertiary_description'},
                     },
                     },
Line 2,167: Line 2,577:
                         key = 'mana_cost',
                         key = 'mana_cost',
                         hide_default = 100,
                         hide_default = 100,
                         fmt = function ()
                         fmt = function (tpl_args, frame)
                             if g_args.has_percentage_mana_cost then
                             if tpl_args.has_percentage_mana_cost then
                                 return '%i%%'
                                 return '%i%%'
                             else
                             else
Line 2,174: Line 2,584:
                             end
                             end
                         end,
                         end,
                         before = function ()
                         inline = function (tpl_args, frame)
                             if g_args.has_reservation_mana_cost then
                             if tpl_args.has_reservation_mana_cost then
                                 return 'Mana Reserved: '
                                 return i18n.tooltips.mana_reserved
                             else
                             else
                                 return 'Mana Cost: '
                                 return i18n.tooltips.mana_cost
                             end
                             end
                         end,
                         end,
Line 2,194: Line 2,604:
                         hide_default = 100,
                         hide_default = 100,
                         fmt = '%i%%',
                         fmt = '%i%%',
                         before = 'Mana Multiplier: ',
                         inline = i18n.tooltips.mana_multiplier,
                     },
                     },
                 },
                 },
             },
             },
         },
         },
        -- TODO: i18n
         {
         {
             args = nil,
             args = nil,
Line 2,208: Line 2,619:
                         hide_default = 0,
                         hide_default = 0,
                         fmt = '%i (N) / ',
                         fmt = '%i (N) / ',
                         before = 'Souls per use: ',
                         inline = i18n.tooltips.vaal_souls_per_use,
                     },
                     },
                     [2] = {
                     [2] = {
Line 2,214: Line 2,625:
                         hide_default = 0,
                         hide_default = 0,
                         fmt = '%i (C) / ',
                         fmt = '%i (C) / ',
                         func = function (value)
                         func = function (tpl_args, frame, value)
                             return value*1.5
                             return value*1.5
                         end,
                         end,
Line 2,222: Line 2,633:
                         hide_default = 0,
                         hide_default = 0,
                         fmt = '%i (M)',
                         fmt = '%i (M)',
                         func = function (value)
                         func = function (tpl_args, frame, value)
                             return value*2
                             return value*2
                         end,
                         end,
Line 2,238: Line 2,649:
                         hide_default = 0,
                         hide_default = 0,
                         fmt = '%i',
                         fmt = '%i',
                         before = 'Can store ',
                         inline = i18n.tooltips.stored_uses,
                        after = ' use(s)',
                     },
                     },
                 },
                 },
Line 2,253: Line 2,663:
                         hide_default = 0,
                         hide_default = 0,
                         fmt = '%i',
                         fmt = '%i',
                         before = 'Can store ',
                         inline = i18n.tooltips.stored_uses,
                        after = ' use(s)',
                     },
                     },
                 },
                 },
Line 2,268: Line 2,677:
                         hide_default = 0,
                         hide_default = 0,
                         fmt = '%.2f sec',
                         fmt = '%.2f sec',
                         before = 'Cooldown Time: ',
                         inline = i18n.tooltips.cooldown_time,
                     },
                     },
                 },
                 },
Line 2,281: Line 2,690:
                         hide_default = 0,
                         hide_default = 0,
                         fmt = '%.2f sec',
                         fmt = '%.2f sec',
                         before = 'Cast Time: ',
                         inline = i18n.tooltips.cast_time,
                     },
                     },
                 },
                 },
Line 2,295: Line 2,704:
                         hide_default = 0,
                         hide_default = 0,
                         fmt = '%.2f%%',
                         fmt = '%.2f%%',
                         before = 'Critical Strike Chance: ',
                         inline = i18n.tooltips.critical_strike_chance,
                     },
                     },
                 },
                 },
Line 2,309: Line 2,718:
                         hide_default = 100,
                         hide_default = 100,
                         fmt = '%i%%',
                         fmt = '%i%%',
                         before = 'Damage Effectiveness: ',
                         inline = i18n.tooltips.damage_effectiveness,
                     },
                     },
                 },
                 },
Line 2,320: Line 2,729:
                     [1] = {
                     [1] = {
                         key = 'projectile_speed',
                         key = 'projectile_speed',
                         before = 'Projectile Speed: ',
                         inline = i18n.tooltips.projectile_speed,
                     },
                     },
                 },
                 },
Line 2,333: Line 2,742:
                         key = 'physical_damage_html',
                         key = 'physical_damage_html',
                         fmt = '%s',
                         fmt = '%s',
                         before = 'Physical Damage: '
                         inline = i18n.tooltips.physical_damage,
                     },
                     },
                 },
                 },
Line 2,340: Line 2,749:
         {
         {
             args = nil,
             args = nil,
             func = function()
             func = function(tpl_args, frame)
                 local text = ''
                 local text = ''
                 for _, dtype in ipairs({'fire_damage_html', 'cold_damage_html', 'lightning_damage_html'}) do
                 for _, dtype in ipairs({'fire_damage_html', 'cold_damage_html', 'lightning_damage_html'}) do
                     local value = g_args[dtype]
                     local value = tpl_args[dtype]
                     if value ~= nil then
                     if value ~= nil then
                         text = text .. ' ' .. value
                         text = text .. ' ' .. value
Line 2,350: Line 2,759:
                  
                  
                 if text ~= '' then
                 if text ~= '' then
                     return 'Elemental Damage:' .. text
                     return string.format(i18n.tooltips.elemental_damage, text)
                 else
                 else
                     return
                     return
Line 2,363: Line 2,772:
                         key = 'chaos_damage_html',
                         key = 'chaos_damage_html',
                         fmt = '%s',
                         fmt = '%s',
                         before='Chaos Damage: ',
                         inline = i18n.tooltips.chaos_damage,
                     },
                     },
                 },
                 },
Line 2,375: Line 2,784:
                         key = 'critical_strike_chance_html',
                         key = 'critical_strike_chance_html',
                         fmt = '%s',
                         fmt = '%s',
                         before = 'Critical Strike Chance: ',
                         inline = i18n.tooltips.critical_strike_chance,
                     },
                     },
                 },
                 },
Line 2,387: Line 2,796:
                         key = 'attack_speed_html',
                         key = 'attack_speed_html',
                         fmt = '%s',
                         fmt = '%s',
                         before = 'Attacks per Second: ',
                         inline = i18n.tooltips.attacks_per_second,
                     },
                     },
                 },
                 },
             },
             },
         },
         },
        -- Map only
         {
         {
             args = {'map_area_level'},
             args = {'range_html'},
             func = core.factory.display_value{
             func = core.factory.display_value{
                 options = {
                 options = {
                     [1] = {
                     [1] = {
                         key = 'map_area_level',
                         key = 'range_html',
                         fmt = '%i',
                         fmt = '%s',
                         before = 'Map Level: ',
                         inline = i18n.tooltips.weapon_range,
                     },
                     },
                 },
                 },
             },
             },
         },
         },
        -- Map only
         {
         {
             args = {'map_tier'},
            args = {'map_area_level'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'map_area_level',
                        fmt = '%i',
                        inline = i18n.tooltips.map_level,
                    },
                },
            },
        },
        {
             args = {'map_tier'},
             func = core.factory.display_value{
             func = core.factory.display_value{
                 options = {
                 options = {
Line 2,412: Line 2,833:
                         key = 'map_tier',
                         key = 'map_tier',
                         fmt = '%i',
                         fmt = '%i',
                         before = 'Map Tier: ',
                         inline = i18n.tooltips.map_tier,
                     },
                     },
                 },
                 },
Line 2,418: Line 2,839:
         },
         },
         {
         {
             args = {'map_guild_character'},
             args = function(tpl_args, frame)
                return tpl_args.map_guild_character ~= nil and tpl_args.rarity == 'Normal'
            end,
             func = core.factory.display_value{
             func = core.factory.display_value{
                 options = {
                 options = {
Line 2,424: Line 2,847:
                         key = 'map_guild_character',
                         key = 'map_guild_character',
                         fmt = '%s',
                         fmt = '%s',
                         before = util.html.abbr('Guild Character', 'When used in guild creation, this map can be used for the listed character') .. ': ',
                         inline = i18n.tooltips.map_guild_character,
                    },
                },
            },
        },
        {
            args = function(tpl_args, frame)
                return tpl_args.unique_map_guild_character ~= nil and tpl_args.rarity == 'Unique'
            end,
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'unique_map_guild_character',
                        fmt = '%s',
                        inline = i18n.tooltips.map_guild_character,
                     },
                     },
                 },
                 },
Line 2,438: Line 2,875:
                         fmt = '+%i%%',
                         fmt = '+%i%%',
                         color = 'mod',
                         color = 'mod',
                         before = 'Item Quantity: ',
                         inline = i18n.tooltips.item_quantity,
                        hide_default = 0,
                     },
                     },
                 },
                 },
Line 2,452: Line 2,890:
                         fmt = '+%i%%',
                         fmt = '+%i%%',
                         color = 'mod',
                         color = 'mod',
                         before = 'Item Rarity: ',
                         inline = i18n.tooltips.item_rarity,
                        hide_default = 0,
                     },
                     },
                 },
                 },
Line 2,466: Line 2,905:
                         fmt = '+%i%%',
                         fmt = '+%i%%',
                         color = 'mod',
                         color = 'mod',
                         before = 'Mosnter Pack Size: ',
                         inline = i18n.tooltips.monster_pack_size,
                        hide_default = 0,
                     },
                     },
                 },
                 },
Line 2,475: Line 2,915:
             args = nil,
             args = nil,
             func = core.factory.display_value{
             func = core.factory.display_value{
                type = 'stat',
                 options = {
                 options = {
                     [1] = {
                     [1] = {
                         keys = {'local_unique_item_limit'},
                         key = 'item_limit',
                         fmt = '%i',
                         fmt = '%i',
                         before = 'Limited to: ',
                         inline = i18n.tooltips.limited_to,
                     },
                     },
                 },
                 },
Line 2,486: Line 2,925:
         },
         },
         {
         {
             args = nil,
             args = {'jewel_radius_html'},
             func = core.factory.display_value{
             func = core.factory.display_value{
                type = 'stat',
                 options = {
                 options = {
                     [1] = {
                     [1] = {
                         keys = {'local_jewel_effect_base_radius'},
                         key = 'jewel_radius_html',
                         fmt = '%s',
                         fmt = '%s',
                         func = function(value)
                         inline = i18n.tooltips.radius,
                            return string.format('%s (%i)', (m_game.constants.item.jewel_radius_to_size[value] or '?'), value)
                        end,
                        before = 'Radius: ',
                     },
                     },
                 },
                 },
Line 2,506: Line 2,941:
             --func = core.factory.display_flask('flask_mana'),
             --func = core.factory.display_flask('flask_mana'),
             func = core.factory.display_value{
             func = core.factory.display_value{
                inline = i18n.tooltips.flask_mana_recovery,
                 options = {
                 options = {
                     [1] = {
                     [1] = {
                         key = 'flask_mana_html',
                         key = 'flask_mana_html',
                         fmt = '%s',
                         fmt = '%s',
                        before = 'Recovers ',
                     },
                     },
                     [2] = {
                     [2] = {
                         key = 'flask_duration_html',
                         key = 'flask_duration_html',
                         fmt = '%s',
                         fmt = '%s',
                        before = ' Mana over ',
                        after = ' seconds',
                     },
                     },
                 }
                 }
Line 2,524: Line 2,957:
             args = {'flask_life_html', 'flask_duration_html'},
             args = {'flask_life_html', 'flask_duration_html'},
             func = core.factory.display_value{
             func = core.factory.display_value{
                inline = i18n.tooltips.flask_life_recovery,
                 options = {
                 options = {
                     [1] = {
                     [1] = {
                         key = 'flask_life_html',
                         key = 'flask_life_html',
                         fmt = '%s',
                         fmt = '%s',
                        before = 'Recovers ',
                     },
                     },
                     [2] = {
                     [2] = {
                         key = 'flask_duration_html',
                         key = 'flask_duration_html',
                         fmt = '%s',
                         fmt = '%s',
                        before = ' Life over ',
                        after = ' seconds',
                     },
                     },
                 }
                 }
Line 2,541: Line 2,972:
         {
         {
             -- don't display for mana/life flasks
             -- don't display for mana/life flasks
             args = function ()
             args = function(tpl_args, frame)
                 for _, k in ipairs({'flask_life_html', 'flask_mana_html'}) do
                 for _, k in ipairs({'flask_life_html', 'flask_mana_html'}) do
                     if g_args[k] ~= nil then
                     if tpl_args[k] ~= nil then
                         return false
                         return false
                     end
                     end
                 end
                 end
                  
                  
                 return g_args['flask_duration_html'] ~= nil
                 return tpl_args['flask_duration_html'] ~= nil
             end,
             end,
             func = core.factory.display_value{
             func = core.factory.display_value{
                inline = i18n.tooltips.flask_duration,
                 options = {
                 options = {
                     [1] = {
                     [1] = {
                         key = 'flask_duration_html',
                         key = 'flask_duration_html',
                        before = 'Lasts ',
                        after = ' Seconds',
                         fmt = '%s',
                         fmt = '%s',
                     },
                     },
Line 2,564: Line 2,994:
             args = {'charges_per_use_html', 'charges_max_html'},
             args = {'charges_per_use_html', 'charges_max_html'},
             func = core.factory.display_value{
             func = core.factory.display_value{
                inline = i18n.tooltips.flask_charges_per_use,
                 options = {
                 options = {
                     [1] = {
                     [1] = {
                         key = 'charges_per_use_html',
                         key = 'charges_per_use_html',
                        before = 'Consumes ',
                         fmt = '%s',
                         fmt = '%s',
                     },
                     },
                     [2] = {
                     [2] = {
                         key = 'charges_max_html',
                         key = 'charges_max_html',
                        before = ' of ',
                        after = ' Charges on use',
                         fmt = '%s',
                         fmt = '%s',
                     },
                     },
Line 2,597: Line 3,025:
                     [1] = {
                     [1] = {
                         key = 'block_html',
                         key = 'block_html',
                         before = 'Block: ',
                         inline = i18n.tooltips.chance_to_block,
                         fmt = '%s',
                         fmt = '%s',
                         hide_default = 0,
                         hide_default = 0,
Line 2,611: Line 3,039:
                     [1] = {
                     [1] = {
                         key = 'armour_html',
                         key = 'armour_html',
                         before = 'Armour: ',
                         inline = i18n.tooltips.armour,
                         fmt = '%s',
                         fmt = '%s',
                         hide_default = 0,
                         hide_default = 0,
Line 2,625: Line 3,053:
                     [1] = {
                     [1] = {
                         key = 'evasion_html',
                         key = 'evasion_html',
                         before = 'Evasion: ',
                         inline = i18n.tooltips.evasion,
                         fmt = '%s',
                         fmt = '%s',
                         hide_default = 0,
                         hide_default = 0,
Line 2,639: Line 3,067:
                     [1] = {
                     [1] = {
                         key = 'energy_shield_html',
                         key = 'energy_shield_html',
                         before = 'Energy Shield: ',
                         inline = i18n.tooltips.energy_shield,
                         fmt = '%s',
                         fmt = '%s',
                         hide_default = 0,
                         hide_default = 0,
                         hide_default_key = 'energy_shield',
                         hide_default_key = 'energy_shield',
                    },
                },
            },
        },
        -- Amulet only
        {
            args = {'talisman_tier'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'talisman_tier',
                        fmt = '%i',
                        inline = i18n.tooltips.talisman_tier,
                     },
                     },
                 },
                 },
Line 2,656: Line 3,097:
                         hide_default = 1,
                         hide_default = 1,
                         fmt = '%i',
                         fmt = '%i',
                         before = 'Stack Size: ',
                         inline = i18n.tooltips.stack_size,
                     },
                     },
                 },
                 },
Line 2,669: Line 3,110:
                         key = 'essence_tier',
                         key = 'essence_tier',
                         fmt = '%i',
                         fmt = '%i',
                         before = 'Essence Tier: ',
                         inline = i18n.tooltips.essence_tier,
                     },
                     },
                 },
                 },
Line 2,677: Line 3,118:
     -- Requirements
     -- Requirements
     {
     {
        -- TODO: i18n Master name?
         {
         {
             args = {'master', 'master_level_requirement'},
             args = {'master', 'master_level_requirement'},
             func = function()
             func = function(tpl_args, frame)
                 -- masters have been validated before
                 -- masters have been validated before
                 local data
                 local data
                 for i, rowdata in ipairs(m_game.constants.masters) do
                 for i, rowdata in ipairs(m_game.constants.masters) do
                     if g_args.master == rowdata.full then
                     if tpl_args.master == rowdata.full then
                         data = rowdata
                         data = rowdata
                         break
                         break
Line 2,689: Line 3,131:
                 end
                 end
                  
                  
                 return util.html.poe_color('default', 'Requires ') .. string.format('[[%s|%s %s]]', data.full, data.short_upper, g_args.master_level_requirement)
                 return util.html.poe_color('default', i18n.tooltips.requires, string.format('[[%s|%s %s]]', data.full, data.short_upper, tpl_args.master_level_requirement))
             end
             end
         },
         },
        {
         -- Instead of item level, show drop level if any
            args = function ()
                if g_args.drop_enabled == true then
                    return false
                end
                return true
            end,
            func = function()
                local span = mw.html.create('span')
                span
                    :attr('class', 'infobox-disabled-drop')
                    :wikitext('DROP DISABLED')
                    :done()
                return tostring(span)
            end,
        },
         -- Instead of item level, show drop level if any
        {
            args = {'drop_enabled'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'drop_level',
                        fmt = '%i',
                        before = 'Drop Level: ',
                    },
                    [2] = {
                        key = 'drop_level_maximum',
                        hide_default = 100,
                        fmt = '%i',
                        before = ' / ',
                    },
                },
            },
        },
         {
         {
             args = nil,
             args = nil,
             func = function ()
             func = function(tpl_args, frame)
                 local opt = {
                 local opt = {
                     [1] = {
                     [1] = {
                         key = 'required_level_final',
                         key = 'required_level_final',
                         hide_default = 1,
                         hide_default = 1,
                         before = 'Level ',
                         inline = i18n.tooltips.level_inline,
                        inline_color = false,
                     },
                     },
                 }
                 }
Line 2,743: Line 3,152:
                         hide_default = 0,
                         hide_default = 0,
                         hide_default_key = string.format('required_%s', attr['long_lower']),
                         hide_default_key = string.format('required_%s', attr['long_lower']),
                         before = ', ',
                         inline = ', %s ' .. attr['short_upper'],
                        after = ' ' .. attr['short_upper'],
                        inline_color = false,
                     }
                     }
                 end
                 end
                  
                  
                 local requirements = core.factory.display_value{options = opt}()
                 local requirements = core.factory.display_value{options = opt}(tpl_args, frame)
                  
                  
                 -- return early
                 -- return early
Line 2,757: Line 3,166:
                 requirements = string.gsub(requirements, '^, ', '')
                 requirements = string.gsub(requirements, '^, ', '')
                  
                  
                 return util.html.poe_color('default', 'Requires ') .. requirements
                 return util.html.poe_color('default', string.format(i18n.tooltips.requires, requirements))
             end,
             end,
         },
         },
     },
     },
     -- Gem description
     -- Drop info
     {
     {
         css_class = '-textwrap tc -gemdesc',
         header = function ()
            return string.format('<b>%s</b><br>', i18n.tooltips.drop_restrictions)
        end,
         {
         {
             args = {'gem_description'},
            args = {'drop_enabled'},
             func = core.factory.display_value_only('gem_description'),
            func = core.factory.display_value{
         },
                options = {
                    [1] = {
                        key = 'drop_level',
                        fmt = '%i',
                        inline = i18n.tooltips.level,
                    },
                    [2] = {
                        key = 'drop_level_maximum',
                        hide_default = 100,
                        fmt = '%i',
                        inline = ' / %s',
                    },
                },
            },
        },
        {
            args = {'drop_leagues'},
            func = function(tpl_args, frame)
                return string.format(i18n.tooltips.league_restriction, util.html.poe_color('value', table.concat(tpl_args.drop_leagues, ', ')))
            end
        },
        {
            args = function(tpl_args, frame)
                if tpl_args.drop_enabled == true then
                    return false
                end
                return true
            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,
        },
    },
    -- Gem description
    {
        css_class = '-textwrap tc -gemdesc',
        {
             args = {'gem_description'},
             func = core.factory.display_value_only('gem_description'),
         },
     },
     },
     -- Gem Quality Stats
     -- Gem Quality Stats
Line 2,774: Line 3,229:
         {
         {
             args = {'quality_stat_text'},
             args = {'quality_stat_text'},
             func = function ()
             func = function(tpl_args, frame)
                 lines = {}
                 lines = {}
                 lines[#lines+1] = util.html.poe_color('default', 'Per 1% Quality:')
                 lines[#lines+1] = util.html.poe_color('default', i18n.tooltips.gem_quality)
                 lines[#lines+1] = g_args.quality_stat_text
                 lines[#lines+1] = tpl_args.quality_stat_text
                  
                  
                 return table.concat(lines, '<br>')
                 return table.concat(lines, '<br>')
Line 2,787: Line 3,242:
         css_class = '-textwrap tc -mod',
         css_class = '-textwrap tc -mod',
         {
         {
             args = function ()
             args = function(tpl_args, frame)
                 return core.class_groups.gems.keys[g_args.class] and g_args.stat_text
                 return core.class_groups.gems.keys[tpl_args.class] and tpl_args.stat_text
             end,
             end,
             func = function ()
             func = function(tpl_args, frame)
                 lines = {}
                 lines = {}
                 lines[#lines+1] = g_args.stat_text
                 lines[#lines+1] = tpl_args.stat_text
                 if g_args.gem_tags:contains('Vaal') then
                 if tpl_args.gem_tags:contains('Vaal') then
                     lines[#lines+1] = util.html.poe_color('corrupted', 'Corrupted')  
                     lines[#lines+1] = util.html.poe_color('corrupted', i18n.tooltips.corrupted)  
                 end
                 end
                 return table.concat(lines, '<br>')
                 return table.concat(lines, '<br>')
Line 2,802: Line 3,257:
     -- Implicit Stats
     -- Implicit Stats
     {
     {
         css_class = 'tc -mod',
         css_class = '-textwrap tc -mod',
         func = function ()
         func = function(tpl_args, frame)
             if g_args.implicit_stat_text ~= '' then
             if tpl_args.implicit_stat_text ~= '' then
                 return {g_args.implicit_stat_text}
                 return {tpl_args.implicit_stat_text}
             else
             else
                 return {}
                 return {}
Line 2,813: Line 3,268:
     -- Stats
     -- Stats
     {
     {
         css_class = 'tc -mod',
         css_class = '-textwrap tc -mod',
         func = function ()
         func = function(tpl_args, frame)
             if g_args.explicit_stat_text ~= '' then
             if tpl_args.explicit_stat_text ~= '' then
                 return {g_args.explicit_stat_text}
                 return {tpl_args.explicit_stat_text}
             else
             else
                 return {}
                 return {}
Line 2,849: Line 3,304:
         {
         {
             args = {'variation_count'},
             args = {'variation_count'},
             func = function ()
             func = function(tpl_args, frame)
                 local txt
                 local txt
                 if g_args.variation_count == 1 then
                 if tpl_args.variation_count == 1 then
                     txt = 'Variation'
                     txt = i18n.tooltips.variation_singular
                 else
                 else
                     txt = 'Variations'
                     txt = i18n.tooltips.variation_plural
                 end
                 end
                 return string.format('%i %s', g_args.variation_count, txt)
                 return string.format('%i %s', tpl_args.variation_count, txt)
             end,
             end,
         },
         },
Line 2,893: Line 3,348:
                     [1] = {
                     [1] = {
                         key = 'master_favour_cost',
                         key = 'master_favour_cost',
                         before = 'Favour cost: ',
                         inline = i18n.tooltips.favour_cost,
                         color = 'currency',
                         color = 'currency',
                     },
                     },
Line 2,905: Line 3,360:
                     [1] = {
                     [1] = {
                         key = 'seal_cost_normal',
                         key = 'seal_cost_normal',
                         before = 'Seal cost: <br>',
                         inline = i18n.tooltips.seal_cost,
                         fmt = '%dx/',
                         fmt = '%dx/',
                         color = 'currency',
                         color = 'currency',
Line 2,918: Line 3,373:
                         fmt = '%dx',
                         fmt = '%dx',
                         color = 'currency',
                         color = 'currency',
                         after = function ()  
                         inline = function (tpl_args, frame)  
                            -- direct call will mess up g_args
                             return '%s' .. p.item_link{item_name_exact='Silver Coin'}
                             return g_frame:expandTemplate{title='Item link',args={item_name_exact='Silver Coin'}}
                         end,
                         end,
                     },
                     },
Line 2,930: Line 3,384:


core.item_link_params = {'name', 'inventory_icon', 'html'}
core.item_link_params = {'name', 'inventory_icon', 'html'}
core.item_link_broken_cat = '[[Category:Pages with broken item links]]'


core.result = {}
core.result = {}


-- for sort type see:
-- https://meta.wikimedia.org/wiki/Help:Sorting
core.result.generic_item = {
core.result.generic_item = {
     {
     {
         arg = 'essence',
         arg = 'base_item',
         header = 'Essence<br>Tier',
         header = i18n.item_table.base_item,
         property = 'Has essence tier',
        properties = {'Has base item', 'Has base item wiki page'},
         cast = tonumber,
        display = function(tr, data)
         display = h.tbl.display.na_or_val,
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has base item'])
                    :wikitext(string.format('[[%s|%s]]', data['Has base item'], data['Has base item wiki page']))
        end,
        order = 1000,
        sort_type = 'text',
    },
    {
        arg = 'class',
        header = i18n.item_table.item_class,
         properties = {'Has item class'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='[[%s]]',
            },
        }},
        order = 1001,
        sort_type = 'text',
    },
    {
        arg = 'essence',
         header = i18n.item_table.essence_tier,
        properties = {'Has essence tier'},
         display = h.tbl.display.factory.value{},
        order = 2000,
     },
     },
     {
     {
         arg = 'drop_level',
         arg = 'drop_level',
         header = 'Drop<br>Level',
         header = i18n.item_table.drop_level,
         property = 'Has drop level',
         properties = {'Has drop level'},
        cast = tonumber,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.na_or_val,
        order = 3000,
     },
     },
     {
     {
         arg = 'stack_size',
         arg = 'stack_size',
         header = 'Stack<br>Size',
         header = i18n.item_table.stack_size,
         property = 'Has stack size',
         properties = {'Has stack size'},
        cast = tonumber,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.na_or_val,
        order = 4000,
     },
     },
     {
     {
         arg = 'stack_size_currency_tab',
         arg = 'stack_size_currency_tab',
         header = util.html.abbr('Tab<br>Stack<br>Size', 'Stack size in the currency stash tab'),
         header = i18n.item_table.stack_size_currency_tab,
         property = 'Has currency tab stack size',
         properties = {'Has currency tab stack size'},
        cast = tonumber,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.na_or_val,
        order = 4001,
     },
     },
     {
     {
         arg = 'level',
         arg = 'level',
         header = m_game.level_requirement.icon,
         header = m_game.level_requirement.icon,
         property = 'Has level requirement',
         properties = {'Has level requirement'},
        cast = tonumber,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.na_or_val,
        order = 5000,
     },
     },
     {
     {
         arg = 'ar',
         arg = 'ar',
         header = util.html.abbr('AR', 'Armour'),
         header = i18n.item_table.armour,
         property = 'Has armour range average',
         properties = h.tbl.range_properties('armour'),
        cast = nil,
         display = h.tbl.display.factory.range{property='armour'},
         display = h.tbl.display.factory.range{property='armour'},
        order = 6000,
     },
     },
     {
     {
         arg = 'ev',
         arg = 'ev',
         header = util.html.abbr('EV', 'Evasion Rating'),
         header =i18n.item_table.evasion,
         property = 'Has evasion range average',
         properties = h.tbl.range_properties('evasion'),
        cast = nil,
         display = h.tbl.display.factory.range{property='evasion'},
         display = h.tbl.display.factory.range{property='evasion'},
        order = 6001,
     },
     },
     {
     {
         arg = 'es',
         arg = 'es',
         header = util.html.abbr('ES', 'Energy Shield'),
         header = i18n.item_table.energy_shield,
         property = 'Has energy shield range average',
         properties = h.tbl.range_properties('energy shield'),
        cast = nil,
         display = h.tbl.display.factory.range{property='energy shield'},
         display = h.tbl.display.factory.range{property='energy shield'},
        order = 6002,
     },
     },
     {
     {
         arg = 'block',
         arg = 'block',
         header = 'Block',
         header = i18n.item_table.block,
         property = 'Has block range average',
         properties = h.tbl.range_properties('block'),
        cast = nil,
         display = h.tbl.display.factory.range{property='block'},
         display = h.tbl.display.factory.range{property='block'},
        order = 6003,
     },
     },
     {
     --[[{
         arg = 'damage_min',
         arg = 'physical_damage_min',
         header = util.html.abbr('Min', 'Local minimum weapon damage'),
         header = util.html.abbr('Min', 'Local minimum weapon damage'),
         property = 'Has minimum physical damage range average',
         properties = h.tbl.range_properties('minimum physical damage'),
        cast = nil,
         display = h.tbl.display.factory.range{property='minimum physical damage'},
         display = h.tbl.display.factory.range{property='minimum physical damage'},
          
         order = 7000,
     },
     },
     {
     {
         arg = 'damage_max',
         arg = 'physical_damage_max',
         header = util.html.abbr('Max', 'Local maximum weapon damage'),
         header = util.html.abbr('Max', 'Local maximum weapon damage'),
         property = 'Has maximum physical damage range average',
         properties = h.tbl.range_properties('maximum physical damage'),
        cast = nil,
         display = h.tbl.display.factory.range{property='maximum physical damage'},
         display = h.tbl.display.factory.range{property='maximum physical damage'},
        order = 7001,
          
          
     },
     },]]--
     {
     {
         arg = 'weapon',
         arg = {'weapon', 'damage'},
        header = util.html.abbr('Damage', 'Colour coded damage'),
         header = i18n.item_table.damage,
         property = nil,
         properties = {'Has damage HTML', 'Has average damage'},
        cast = nil,
        display = function (tr, data)
        -- TODO: Very similar code to display.factory.range. Factorize?
             tr
         display = function (tr, value, data)
                 :tag('td')
            local rows = {}
                    :attr('data-sort-value', data['Has average damage'])
            local dmg = 0
                    :wikitext(data['Has damage HTML'])
            for _, damage_data in ipairs(m_game.constants.damage_types) do
                local range = {}
               
                for _, bound in ipairs{'minimum', 'maximum'} do
                    local options = {}
                    local chk_keys = {}
                    local value = {}
                    local property = string.format('%s %s damage', bound, damage_data.short_lower)
                   
                    if damage_data.short_lower == 'physical' then
                        value.base = tonumber(data['?Has base ' .. property])
                        chk_keys[#chk_keys+1] = 'base'
                    else
                        options.no_color = true
                    end
               
                    for key, range_data in pairs(h.range_map) do
                        value[key] = tonumber(data[string.format('?Has %s%s', property, range_data.property)])
                        chk_keys[#chk_keys+1] = key
                    end
                   
                    local data = {value=value}
                    if util.table.has_one_value(value, chk_keys, nil) then
                        data.fail = true
                    else
                        data.text = h.format_value(value, options)
                        data.fail = false
                    end
                   
                    range[bound] = data
                end
               
                if range.minimum.fail or range.maximum.fail then
                -- all zeros shouldn't show up
                elseif range.minimum.value.avg ~= 0 and range.maximum.avg ~= 0 then
                    local text = string.format('%s-%s', range.minimum.text, range.maximum.text)
                    -- physical uses white/mod colour depending, while elemental damage is always added damage and shows in the native colour
                    if damage_data.short_lower ~= 'physical' then
                        text = util.html.poe_color(damage_data.short_lower, text)
                    end
                    rows[#rows+1] = text
                    dmg = dmg + (range.minimum.value.avg + range.maximum.value.avg) / 2
                end
             end
           
            if #rows > 0 then
                 tr
                    :tag('td')
                        :attr('data-sort-value', dmg)
                        :wikitext(table.concat(rows, '<br>'))
                        :done()
            else
                local td = util.html.td.na{as_tag=true}
                td:attr('data-sort-value', dmg)
                tr:wikitext(tostring(td))
            end
         end,
         end,
        order = 8000,
     },
     },
     {
     {
         arg = 'weapon',
         arg = {'weapon', 'aps'},
         header = util.html.abbr('APS', 'Attacks per second'),
         header = i18n.item_table.attacks_per_second,
         property = 'Has attack speed range average',
         properties = h.tbl.range_properties('attack speed'),
        cast = nil,
         display = h.tbl.display.factory.range{property='attack speed'},
         display = h.tbl.display.factory.range{
         order = 8001,
            property='attack speed',
            options={
                fmt = '%.2f',
            },
         },
     },
     },
     {
     {
         arg = 'weapon',
         arg = {'weapon', 'crit'},
         header = util.html.abbr('Crit', 'Local weapon critical strike'),
         header = i18n.item_table.local_critical_strike_chance,
         property = 'Has critical strike chance range average',
         properties = h.tbl.range_properties('critical strike chance'),
        cast = nil,
         display = h.tbl.display.factory.range{property='critical strike chance'},
         display = h.tbl.display.factory.range{
         order = 8002,
            property='critical strike chance',
            options={
                fmt = '%.2f%%',
            },
         },
     },
     },
     {
     {
         arg = 'flask_life',
         arg = 'flask_life',
         header = util.html.abbr('Life', 'Life regenerated over the flask duration'),
         header = i18n.item_table.flask_life,
         property = 'Has flask life recovery range average',
         properties = h.tbl.range_properties('flask life recovery'),
        cast = nil,
         display = h.tbl.display.factory.range{property='flask life recovery'},
         display = h.tbl.display.factory.range{property='flask life recovery'},
        order = 9000,
     },
     },
     {
     {
         arg = 'flask_mana',
         arg = 'flask_mana',
         header = util.html.abbr('Mana', 'Mana regenerated over the flask duration'),
         header = i18n.item_table.flask_mana,
         property = 'Has flask mana recovery range average',
         properties = h.tbl.range_properties('flask mana recovery'),
        cast = nil,
         display = h.tbl.display.factory.range{property='flask mana recovery'},
         display = h.tbl.display.factory.range{property='flask mana recovery'},
        order = 9001,
     },
     },
     {
     {
         arg = 'flask',
         arg = 'flask',
         header = 'Duration',
         header = i18n.item_table.flask_duration,
         property = 'Has flask duration range average',
         properties = h.tbl.range_properties('flask duration'),
        cast = nil,
         display = h.tbl.display.factory.range{property='flask duration'},
         display = h.tbl.display.factory.range{property='flask duration'},
        order = 9002,
     },
     },
     {
     {
         arg = 'flask',
         arg = 'flask',
         header = util.html.abbr('Usage', 'Number of charges consumed on use'),
         header = i18n.item_table.flask_charges_per_use,
         property = 'Has flask charges per use range average',
         properties = h.tbl.range_properties('flask charges per use'),
        cast = nil,
         display = h.tbl.display.factory.range{property='flask charges per use'},
         display = h.tbl.display.factory.range{property='flask charges per use'},
        order = 9003,
     },
     },
     {
     {
         arg = 'flask',
         arg = 'flask',
         header = util.html.abbr('Capacity', 'Maximum number of flask charges held'),
         header = i18n.item_table.flask_maximum_charges,
         property = 'Has maximum flask charges range average',
         properties = h.tbl.range_properties('maximum flask charges'),
        cast = nil,
         display = h.tbl.display.factory.range{property='maximum flask charges'},
         display = h.tbl.display.factory.range{property='maximum flask charges'},
        order = 9004,
    },
    {
        arg = 'item_limit',
        header = i18n.item_table.item_limit,
        properties = {'Has item limit'},
        display = h.tbl.display.factory.value{},
        order = 10000,
    },
    {
        arg = 'jewel_radius',
        header = i18n.item_table.jewel_radius,
        properties = {'Has jewel radius', 'Has jewel radius HTML'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has jewel radius'])
                    :wikitext(data['Has jewel radius HTML'])
        end,
        order = 10001,
    },
    {
        arg = 'map_tier',
        header = i18n.item_table.map_tier,
        properties = {'Has map tier'},
        display = h.tbl.display.factory.value{},
        order = 11000,
    },
    {
        arg = 'map_level',
        header = i18n.item_table.map_level,
        properties = {'Has map area level'},
        display = h.tbl.display.factory.value{},
        order = 11010,
    },
    {
        arg = 'map_guild_character',
        header = i18n.item_table.map_guild_character,
        properties = {'Has map guild character'},
        display = h.tbl.display.factory.value{colour='value'},
        order = 11020,
        sort_type = 'text',
     },
     },
     {
     {
         arg = 'buff',
         arg = 'buff',
         header = 'Buff Effects',
         header = i18n.item_table.buff_effects,
         property = 'Has buff stat text',
         properties = {'Has buff stat text'},
        cast = nil,
         display = h.tbl.display.factory.value{colour='mod'},
         display = function (tr, value, data)
        order = 12000,
            return h.na_or_val(tr, value, function (value)
         sort_type = 'text',
                return util.html.poe_color('mod', value)
            end)
         end,
     },
     },
     {
     {
         arg = 'stat',
         arg = 'stat',
         header = 'Stats',
         header = i18n.item_table.stats,
         property = 'Has stat text',
         properties = {'Has stat text'},
        cast = nil,
         display = h.tbl.display.factory.value{colour='mod'},
         display = function (tr, value, data)
        order = 12001,
            return h.na_or_val(tr, value, function (value)
         sort_type = 'text',
                return util.html.poe_color('mod', value)
            end)
         end,
     },
     },
     {
     {
         arg = 'description',
         arg = 'description',
         header = 'Effect(s)',
         header = i18n.item_table.effects,
         property = 'Has description',
         properties = {'Has description'},
        cast = nil,
         display = h.tbl.display.factory.value{colour='mod'},
         display = function (tr, value, data)
        order = 12002,
            return h.na_or_val(tr, value, function (value)
         sort_type = 'text',
                return util.html.poe_color('mod', value)
            end)
         end,
     },
     },
     {
     {
         arg = 'flavour_text',
         arg = 'flavour_text',
         header = 'Flavour Text',
         header = i18n.item_table.flavour_text,
         property = 'Has flavour text',
         properties = {'Has flavour text'},
        cast = nil,
         display = h.tbl.display.factory.value{colour='flavour'},
         display = function (tr, value, data)
        order = 12003,
            return h.na_or_val(tr, value, function (value)
         sort_type = 'text',
                return util.html.poe_color('flavour', value)
            end)
         end,
     },
     },
     {
     {
         arg = 'help_text',
         arg = 'help_text',
         header = 'Help Text',
         header = i18n.item_table.help_text,
         property = 'Has help text',
         properties = {'Has help text'},
        cast = nil,
         display = h.tbl.display.factory.value{colour='help'},
         display = function (tr, value, data)
        order = 12004,
            return h.na_or_val(tr, value, function (value)
         sort_type = 'text',
                return util.html.poe_color('help', value)
            end)
         end,
     },
     },
     {
     {
         arg = 'buff_icon',
         arg = 'buff_icon',
         header = 'Buff Icon',
         header = i18n.item_table.buff_icon,
         property = 'Has buff icon',
         properties = {'Has buff icon'},
         cast = nil,
         display = h.tbl.display.factory.value{options = {
        display = function (tr, value, data)
             [1] = {
             return h.na_or_val(tr, value, function (value)
                 fmt='[[%s]]',
                 return string.format('[[%s]]', value)
             },
             end)
        }},
         end,
        order = 13000,
         sort_type = 'text',
     },
     },
}
}


for _, data in ipairs(core.dps_map) do
for i, data in ipairs(core.dps_map) do
     table.insert(core.result.generic_item, #core.result.generic_item-5, {
     table.insert(core.result.generic_item, {
         arg = data.name,
         arg = data.name,
         header = data.label,
         header = data.label,
         property = nil,
         properties = h.tbl.range_properties(data.property),
        cast = nil,
         display = h.tbl.display.factory.range{property=data.property},
         display = h.tbl.display.factory.range{
         order = 8100+i,  
            property=data.property,
            no_base=true,
            options={
                fmt = '%.1f',
            },
         },
     })
     })
end
end


core.result.skill_gem = {
core.result.skill_gem_new = {
     {
     {
         arg = 'icon',
         arg = 'icon',
         header = util.html.abbr('L', 'Support gem letter.'),
         header = i18n.item_table.support_gem_letter,
         property = 'Has support gem letter HTML',
         properties = {'Has support gem letter HTML'},
        cast = nil,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.na_or_val,
        order = 1000,
        sort_type = 'text',
     },
     },
     {
     {
         arg = 'skill_icon',
         arg = 'skill_icon',
         header = 'Icon',
         header = i18n.item_table.skill_icon,
         property = 'Has skill gem icon',
         properties = {'Has skill gem icon'},
        cast = nil,
         display = h.tbl.display.factory.value{options = {
         display = h.tbl.display.wikilink,
            [1] = {
                fmt='[[%s]]',
            },
        }},
        order = 1001,
        sort_type = 'text',
     },
     },
     {
     {
         arg = 'description',
         arg = 'description',
         header = 'Description',
         header = i18n.item_table.description,
         property = 'Has description',
         properties = {'Has description'},
        cast = nil,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.na_or_val,
        order = 2000,
        sort_type = 'text',
     },
     },
     {
     {
         arg = 'level',
         arg = 'level',
         header = m_game.level_requirement.icon,
         header = m_game.level_requirement.icon,
         property = 'Has level requirement',
         properties = {'Has level requirement'},
        cast = tonumber,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.na_or_val,
        order = 3004,
     },
     },
     {
     {
         arg = 'crit',
         arg = 'crit',
         header = util.html.abbr('Crit', 'Critical Strike Chance'),
         header = i18n.item_table.skill_critical_strike_chance,
         property = 'Has critical strike chance',
         properties = {'Has critical strike chance'},
        cast = tonumber,
         display = h.tbl.display.factory.value{options = {
         display = h.tbl.display.percent,
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 4000,
     },
     },
     {
     {
         arg = 'cast_time',
         arg = 'cast_time',
         header = util.html.abbr('Cast<br>Time', 'Casting time of the skill in seconds'),
         header = i18n.item_table.cast_time,
         property = 'Has cast time',
         properties = {'Has cast time'},
        cast = tonumber,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.seconds,
        order = 4001,
     },
     },
     {
     {
         arg = 'dmgeff',
         arg = 'dmgeff',
         header = util.html.abbr('Dmg.<br>Eff.', 'Damage Effectiveness'),
         header = i18n.item_table.damage_effectiveness,
         property = 'Has damage effectiveness',
         properties = {'Has damage effectiveness'},
        cast = tonumber,
         display = h.tbl.display.factory.value{options = {
         display = h.tbl.display.percent,
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 4002,
     },
     },
     {
     {
         arg = 'mcm',
         arg = 'mcm',
         header = util.html.abbr('MCM', 'Mana cost multiplier - missing values indicate it changes on gem level'),
         header = i18n.item_table.mana_cost_multiplier,
         property = 'Has mana multiplier',
         properties = {'Has mana multiplier'},
        cast = tonumber,
         display = h.tbl.display.factory.value{options = {
         display = h.tbl.display.percent,
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 5000,
     },
     },
     {
     {
         arg = 'mana',
         arg = 'mana',
         header = util.html.abbr('Mana', 'Mana cost'),
         header = i18n.item_table.mana_cost,
        property = 'Has mana cost',
        properties = {'Has mana cost', 'Has percentage mana cost', 'Has reservation mana cost'},
        cast = tonumber,
         display = function (tr, data)
         display = function (tr, value, data)
             local appendix = ''
             local appendix = ''
             if data['?Has percentage mana cost'] then
             if util.cast.boolean(data['Has percentage mana cost']) then
                 appendix = appendix .. '%'
                 appendix = appendix .. '%'
             end
             end
             if data['?Has reservation mana cost'] then
             if util.cast.boolean(data['Has reservation mana cost']) then
                 appendix = appendix .. ' ' .. util.html.abbr('R', 'reserves mana')
                 appendix = appendix .. ' ' .. i18n.item_table.reserves_mana_suffix
             end
             end
              
              
             local str
             tr
           
                :tag('td')
            if value ~= nil then
                    :attr('data-sort-value', data['Has mana cost'])
                str = string.format('%d', value) .. appendix
                    :wikitext(string.format('%d', data['Has mana cost']) .. appendix)
            end
            return h.na_or_val(tr, str)
         end,
         end,
        order = 5001,
     },
     },
     {
     {
         arg = 'vaal',
         arg = 'vaal',
         header = util.html.abbr('Souls', 'Vaal souls requirement in Normal/Cruel/Merciless difficulty'),
         header = i18n.item_table.vaal_souls_requirement,
         property = 'Has vaal souls requirement',
         properties = {'Has vaal souls requirement'},
        cast = tonumber,
         display = function (tr, data)
         display = function (tr, value, data)  
             local souls = tonumber(data['Has vaal souls requirement'])
             return h.na_or_val(tr, value, function (value)
            tr
                return string.format('%d / %d / %d', value, value*1.5, value*2)
                :tag('td')
            end)
                    :attr('data-sort-value', souls)
                    :wikitext(string.format('%d / %d / %d', souls, souls*1.5, souls*2))
         end,
         end,
        order = 6000,
     },
     },
     {
     {
         arg = 'vaal',
         arg = 'vaal',
         header = util.html.abbr('Uses', 'Maximum number of stored uses'),
         header = i18n.item_table.stored_uses,
         property = 'Has vaal stored uses',
         properties = {'Has vaal stored uses'},
        cast = tonumber,
         display = h.tbl.display.factory.value{},
         display = h.tbl.display.na_or_val,
        order = 6001,
     },
     },
     {
     {
         arg = 'radius',
         arg = 'radius',
         header = util.html.abbr('R1', 'Primary radius'),
         header = i18n.item_table.primary_radius,
        property = 'Has primary radius',
        properties = {'Has primary radius', 'Has primary radius description'},
         cast = tonumber,
         options = {[2] = {optional = true}},
         display = function (tr, value, data)
         display = function (tr, data)
             return h.na_or_val(tr, value, core.factory.descriptor_value{key='?Has primary radius description', tbl=data})
             tr
                :tag('td')
                    :attr('data-sort-value', data['Has primary radius'])
                    :wikitext(core.factory.descriptor_value{tbl=data, key='Has primary radius description'}(nil, nil, data['Has primary radius']))
         end,
         end,
        order = 7000,
     },
     },
     {
     {
         arg = 'radius',
         arg = 'radius',
         header = util.html.abbr('R2', 'Secondary radius'),
         header = i18n.item_table.secondary_radius,
        property = 'Has secondary radius',
        properties = {'Has secondary radius', 'Has secondary radius description'},
         cast = tonumber,
         options = {[2] = {optional = true}},
         display = function (tr, value, data)
         display = function (tr, data)
             return h.na_or_val(tr, value, core.factory.descriptor_value{key='?Has secondary radius description', tbl=data})
             tr
                :tag('td')
                    :attr('data-sort-value', data['Has secondary radius'])
                    :wikitext(core.factory.descriptor_value{tbl=data, key='Has secondary radius description'}(nil, nil, data['Has secondary radius']))
         end,
         end,
        order = 7001,
     },
     },
     {
     {
         arg = 'radius',
         arg = 'radius',
         header = util.html.abbr('R3', 'Tertiary radius'),
         header = i18n.item_table.tertiary_radius,
        property = 'Has tertiary radius',
        properties = {'Has tertiary radius', 'Has tertiary radius description'},
         cast = tonumber,
         options = {[2] = {optional = true}},
         display = function (tr, value, data)
         display = function (tr, data)
             return h.na_or_val(tr, value, core.factory.descriptor_value{key='?Has tertiary radius description', tbl=data})
             tr
                :tag('td')
                    :attr('data-sort-value', data['Has tertiary radius'])
                  :wikitext(core.factory.descriptor_value{tbl=data, key='Has tertiary radius description'}(nil, nil, data['Has tertiary radius']))
         end,
         end,
        order = 7002,
     },
     },
}
}


for _, attr in ipairs(m_game.constants.attributes) do
for i, attr in ipairs(m_game.constants.attributes) do
     table.insert(core.result.generic_item, 6, {
     table.insert(core.result.generic_item, 7, {
         arg = attr.short_lower,
         arg = attr.short_lower,
         header = attr.icon,
         header = attr.icon,
         property = string.format('Has %s requirement', attr.long_lower),
         properties = h.tbl.range_properties(string.format('%s requirement', attr.long_lower)),
        cast = tonumber,
         display = h.tbl.display.factory.range{property=string.format('%s requirement', attr.long_lower)},
         display = h.tbl.display.factory.range{property=string.format('%s requirement', attr.long_lower)},
        order = 5000+i,
     })
     })
     table.insert(core.result.skill_gem, 3, {
     table.insert(core.result.skill_gem_new, 1, {
         arg = attr.short_lower,
         arg = attr.short_lower,
         header = attr.icon,
         header = attr.icon,
         property = string.format('Has %s percentage', attr.long_lower),
         properties = {string.format('Has %s percentage', attr.long_lower)},
        cast = tonumber,
         display = function (tr, data)
         display = function (tr, value)  
             tr
             return h.na_or_val(tr, value, function (value)
                :tag('td')
                return '[[File:Yes.png|yes|link=]]'
                    :attr('data-sort-value', data[string.format('Has %s percentage', attr.long_lower)])
            end)
                    :wikitext('[[File:Yes.png|yes|link=]]')
         end,
         end,
        order = 3000+i,
     })
     })
end
end
Line 3,385: Line 3,852:
     local t = os.clock()
     local t = os.clock()
      
      
     g_args = getArgs(frame, {
     local tpl_args = getArgs(frame, {
         parentFirst = true
         parentFirst = true
     })
     })
     g_frame = util.misc.get_frame(frame)
     frame = util.misc.get_frame(frame)
      
      
     --
     --
Line 3,396: Line 3,863:
     core.build_item_classes()
     core.build_item_classes()
      
      
     g_args._total_args = {}
     tpl_args._flags = {}
     g_args._base_item_args = {}
    tpl_args._total_args = {}
     g_args._mods = {}
     tpl_args._base_item_args = {}
     g_args._stats = {}
     tpl_args._mods = {}
     g_args._subobjects = {}
     tpl_args._stats = {}
     g_args._properties = {}
     tpl_args._explicit_stats = {}
    tpl_args._subobjects = {}
     tpl_args._properties = {}
      
      
     -- Using general purpose function to handle release and removal versions
     -- Using general purpose function to handle release and removal versions
     util.args.version(g_args, {frame=g_frame, set_properties=true})
     util.args.version(tpl_args, {frame=frame, set_properties=true})
      
      
     -- Must validate some argument early. It is required for future things
     -- Must validate some argument early. It is required for future things
     local err
     core.process_arguments(tpl_args, frame, {array=core.default_args})
    core.process_arguments(tpl_args, frame, {array=core.item_classes[tpl_args.class].args})
      
      
     err = core.process_arguments{array=core.default_args}
     -- set defaults
    if err then
        return err
    end
      
      
    err = core.process_arguments{array=core.item_classes[g_args.class].args}
     for k, v in pairs(core.item_classes[tpl_args.class].defaults) do
    if err then
         if tpl_args[k] == nil then
        return err
             tpl_args[k] = v
    end
         end
   
    -- set defaults
   
     for k, v in pairs(core.item_classes[g_args.class].defaults) do
         if g_args[k] == nil then
             g_args[k] = v
         end
     end
     end
      
      
     -- Base Item
     -- Base Item


     core.process_base_item()
     core.process_base_item(tpl_args, frame)
      
      
     -- Prophecy special snowflake
     -- Prophecy special snowflake
     if g_args.base_item == 'Prophecy' then
     if tpl_args.base_item == 'Prophecy' then
         err = core.process_arguments{array=core.prophecy_args}
         err = core.process_arguments(tpl_args, frame, {array=core.prophecy_args})
         if err then
         if err then
             return err
             return err
         end
         end
          
          
         g_args.inventory_icon = 'File:Prophecy inventory icon.png'
         tpl_args.inventory_icon = string.format(i18n.inventory_icon, 'Prophecy')
     end
     end
      
      
Line 3,447: Line 3,907:
         local i = 1
         local i = 1
         while success do
         while success do
             success = core.validate_mod{key=k, i=i}
             success = core.validate_mod(tpl_args, frame, {key=k, i=i})
             i = i + 1
             i = i + 1
         end
         end
     end
     end
      
      
     core.process_smw_mods()
     core.process_smw_mods(tpl_args, frame)
          
          
     -- Add stats - this is for when mods are not set, but we still need stats to calcuate new armour values etc
     -- Add stats - this is for when mods are not set, but we still need stats to calcuate new armour values etc
     util.args.stats(g_args, {prefix='extra_'})
     util.args.stats(tpl_args, {prefix='extra_'})
     for _, stat in ipairs(g_args.extra_stats) do
     for _, stat in ipairs(tpl_args.extra_stats) do
         if stat.value ~= nil then
         if stat.value ~= nil then
             stat.min = stat.value
             stat.min = stat.value
Line 3,462: Line 3,922:
             stat.avg = stat.value
             stat.avg = stat.value
         end
         end
         h.stats_update(stat.id, stat)
         h.stats_update(tpl_args, stat.id, stat, nil, '_stats')
        h.stats_update(tpl_args, stat.id, stat, nil, '_explicit_stats')
     end
     end


     -- Transpose stats into subobjects
     -- Transpose stats into subobjects
     for id, data in pairs(g_args._stats) do
     for id, data in pairs(tpl_args._stats) do
         g_args._subobjects[id] = {
         tpl_args._subobjects[id] = {
             ['Has stat ids'] = id,
             ['Has stat id'] = id,
             ['Has minimum stat values'] = data.min,
            ['Has minimum stat value'] = data.min,
             ['Has maximum stat values'] = data.max,
            ['Has maximum stat value'] = data.max,
             ['Has average stat values'] = data.avg,
            ['Has average stat value'] = data.avg,
        }
    end
    for id, data in pairs(tpl_args._explicit_stats) do
        tpl_args._subobjects['explicit_' .. id] = {
            ['Has explicit stat id'] = id,
             ['Has minimum stat value'] = data.min,
             ['Has maximum stat value'] = data.max,
             ['Has average stat value'] = data.avg,
         }
         }
     end
     end
Line 3,477: Line 3,946:
     -- Handle extra stats (for gems)
     -- Handle extra stats (for gems)
      
      
     if core.class_groups.gems.keys[g_args.class] then
     if core.class_groups.gems.keys[tpl_args.class] then
         m_skill.skill(g_frame, g_args)
         m_skill.skill(frame, tpl_args)
     end
     end
      
      
Line 3,487: Line 3,956:
     local skip = {}
     local skip = {}
     -- override attributes for tabula rasa
     -- override attributes for tabula rasa
     if g_args._stats.local_unique_tabula_rasa_no_requirement_or_energy_shield ~= nil and g_args._stats.local_unique_tabula_rasa_no_requirement_or_energy_shield.min == 1 then
     if tpl_args._stats.local_unique_tabula_rasa_no_requirement_or_energy_shield ~= nil and tpl_args._stats.local_unique_tabula_rasa_no_requirement_or_energy_shield.min == 1 then
         for _, arg in ipairs({'required_strength', 'required_intelligence', 'required_dexterity', 'energy_shield'}) do
         tpl_args.required_level_final = 1
            skip[arg] = true
            for short_key, range_data in pairs(h.range_map) do
                g_args[arg .. range_data.var] = 0
                g_args._properties[core.stat_map[arg].property .. range_data.property] = 0
            end
        end
        g_args.required_level_final = 1
     end
     end
      
      
     -- general stats
     -- general stats
     for k, data in pairs(core.stat_map) do
     for k, data in pairs(core.stat_map) do
         local value = g_args[k]
         local value = tpl_args[k]
   
   
         if value == nil and data.default ~= nil then
         if value == nil and data.default ~= nil then
             value = data.default
             value = data.default
             g_args[k] = data.default
             tpl_args[k] = data.default
         end
         end
          
          
         if value ~= nil and skip[k] == nil then
         if value ~= nil and skip[k] == nil then
             value = {min=value, max=value, base=value}
             value = {min=value, max=value, base=value}
             -- The simple cases; this must be using ipairs as "add" must apply before
             -- If stats are overriden we scan save some CPU time here
            for _, operator in ipairs({'add', 'more'}) do
            local overridden = false
                local st = data['stats_' .. operator]
            if data.stats_override ~= nil then
                if st ~= nil then
                for stat_id, override_value in pairs(data.stats_override) do
                    for _, statid in ipairs(st) do
                    local stat_value = tpl_args._stats[stat_id]
                        if g_args._stats[statid] ~= nil then
                    if stat_value ~= nil then
                            h.stat[operator](value, g_args._stats[statid])
                        -- 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
                     end
                 end
                 end
            end
               
           
                -- For increased stats we need to add them up first
            -- 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_increased
                    local st = data['stats_' .. stat_key]
            if st ~= nil then
                    if st ~= nil then
                local total_increase = {min=0, max=0}
                        local total_increase = {min=0, max=0}
                for _, statid in ipairs(st) do
                        for _, statid in ipairs(st) do
                    if g_args._stats[statid] ~= nil then
                            if tpl_args._stats[statid] ~= nil then
                        for var, current_value in pairs(total_increase) do
                                for var, current_value in pairs(total_increase) do
                            total_increase[var] = current_value + g_args._stats[statid][var]
                                    total_increase[var] = current_value + tpl_args._stats[statid][var]
                                end
                            end
                         end
                         end
                        stat_func(value, total_increase)
                     end
                     end
                 end
                 end
                 h.stat.more(value, total_increase)
                  
            end
                if data.minimum ~= nil then
           
                    for _, key in ipairs({'min', 'max'}) do
            if data.minimum ~= nil then
                        if value[key] < data.minimum then
                for _, key in ipairs({'min', 'max'}) do
                            value[key] = data.minimum  
                    if value[key] < data.minimum then
                        end
                        value[key] = data.minimum  
                     end
                     end
                 end
                 end
            else
             end
             end
              
              
Line 3,548: Line 4,036:
             if (data.default ~= nil and (value.min ~= data.default or value.max ~= data.default)) or data.default == nil then
             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(h.range_map) do
                 for short_key, range_data in pairs(h.range_map) do
                     g_args._properties[data.property .. range_data.property] = value[short_key]
                     tpl_args._properties[data.property .. range_data.property] = value[short_key]
                 end
                 end
                  
                  
                 -- process to HTML to use on list pages or other purposes
                 -- process to HTML to use on list pages or other purposes
                 h.handle_range_args(k, data.property, value, data.html_fmt_options or {})
                 h.handle_range_args(tpl_args, frame, k, data.property, value, data.html_fmt_options or {})
             end
             end
              
              
             for short_key, range_data in pairs(h.range_map) do
             for short_key, range_data in pairs(h.range_map) do
                 g_args[k .. range_data.var] = value[short_key]
                 tpl_args[k .. range_data.var] = value[short_key]
             end
             end
         end
         end
Line 3,562: Line 4,050:


     -- calculate and handle weapon dps
     -- calculate and handle weapon dps
     if core.class_groups.weapons.keys[g_args.class] then
     if core.class_groups.weapons.keys[tpl_args.class] then
         for _, data in ipairs(core.dps_map) do
         for _, data in ipairs(core.dps_map) do
             local damage = {
             local damage = {
Line 3,574: Line 4,062:
                     value[short_key] = 0
                     value[short_key] = 0
                     for _, damage_key in ipairs(data.damage_args) do
                     for _, damage_key in ipairs(data.damage_args) do
                         value[short_key] = value[short_key] + g_args[string.format('%s_%s%s', damage_key, var_type, range_data.var)]
                         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
                 end
Line 3,581: Line 4,069:
             local value = {}
             local value = {}
             for short_key, range_data in pairs(h.range_map) do
             for short_key, range_data in pairs(h.range_map) do
                 local result = (damage.min[short_key] + damage.max[short_key]) / 2 * g_args[string.format('attack_speed%s', range_data.var)]
                 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
                 value[short_key] = result
                 g_args[string.format('%s%s', data.name, range_data.var)] = result
                 tpl_args[string.format('%s%s', data.name, range_data.var)] = result
                 -- It will set the property, even if 0.  
                 -- It will set the property, even if 0.  
                 -- Not sure if it is better to not set it, but on the other hand this way it can be queried for having no dps of a particular type
                 -- Not sure if it is better to not set it, but on the other hand this way it can be queried for having no dps of a particular type
                 g_args._properties[string.format('Has %s%s', data.property, range_data.property)] = result
                 tpl_args._properties[string.format('Has %s%s', data.property, range_data.property)] = result
             end
             end
              
              
             if value.avg > 0 then
             if value.avg > 0 then
                 h.handle_range_args(data.name, 'Has ' .. data.property, value, data.html_fmt_options or {})
                 h.handle_range_args(tpl_args, frame, data.name, 'Has ' .. data.property, value, data.html_fmt_options or {})
             end
             end
         end
         end
Line 3,596: Line 4,084:
      
      
     -- late processing
     -- late processing
     err = core.process_arguments{array=core.late_args}
     core.process_arguments(tpl_args, frame, {array=core.late_args})
     if err then
     core.process_arguments(tpl_args, frame, {array=core.item_classes[tpl_args.class].late_args})
        return err
    end
      
      
     err = core.process_arguments{array=core.item_classes[g_args.class].late_args}
     -- Handle upgrade from restrictions/info
    if err then
    core.process_upgraded_from(tpl_args, frame)
        return err
    end
      
      
     -- Setting semantic properties Part 1 (base values)
     -- Setting semantic properties Part 1 (base values)
Line 3,610: Line 4,094:
     local val
     local val
      
      
     for _, k in ipairs(g_args._total_args) do
     for _, k in ipairs(tpl_args._total_args) do
         local prop = core.map[k].property
         local prop = core.map[k].property
         val = g_args[k]
         val = tpl_args[k]
         if val == nil then
         if val == nil then
         elseif prop == nil then
         elseif prop == nil then
             --mw.logObject(k)
             --mw.logObject(k)
         else
         else
             g_args._properties[prop] = val
             tpl_args._properties[prop] = val
         end
         end
     end
     end
      
      
     util.smw.set(g_frame, g_args._properties)
     util.smw.set(frame, tpl_args._properties)
      
      
     -- Subobjects
     -- Subobjects
     local command
     local command
     for key, properties in pairs(g_args._subobjects) do
     for key, properties in pairs(tpl_args._subobjects) do
         command = ''
         command = ''
         if type(key) ~= 'number' then
         if type(key) ~= 'number' then
             command = key
             command = key
         end
         end
         util.smw.subobject(g_frame, command, properties)
         util.smw.subobject(frame, command, properties)
     end
     end
      
      
     --
     -- ------------------------------------------------------------------------
     -- Validate arguments
     -- Infobox handling
     --  
     -- ------------------------------------------------------------------------
      
      
     local x = v.itembox()
    tpl_args._properties = {}
     local extra_class = ''
    local container = mw.html.create('span')
        :attr( 'class', 'item-box -' .. tpl_args.frame_type)
      
      
     mw.logObject(os.clock() - t)
     if tpl_args.class == 'Divination Card' then
   
        --TODO div card code
    return x
        container
end
            :tag('span')
 
                :attr( 'class', 'divicard-wrapper')
--
                :tag('span')
-- Template:Item link & Template:Sl
                    :attr('class', 'divicard-art')
--
                    :wikitext( '[[File:' .. tpl_args.card_art .. '|link=|alt=]]' )
 
                    :done()
function p.item_link (frame)
                :tag('span')
    --
                    :attr('class', 'divicard-frame')
    -- Args/Frame
                    :wikitext( '[[File:Divination card frame.png|link=|alt=]]' )
    --
                    :done()
   
                :tag('span')
    g_args = getArgs(frame, {
                    :attr('class', 'divicard-header')
        parentFirst = true,
                    :wikitext(tpl_args.name)
        removeBlanks = false,
                    :done()
    })
                :tag('span')
    g_frame = util.misc.get_frame(frame)
                    :attr('class', 'divicard-stack')
   
                    :wikitext(tpl_args.stack_size)
    -- Backwards compability
                    :done()
    g_args.item_name = g_args.item_name or g_args[1]
                :tag('span')
    if g_args.item_name ~= nil then
                    :attr('class', 'divicard-reward')
        g_args.item_name = string.lower(g_args.item_name)
                    :tag('span')
    end
                        :wikitext(tpl_args.card_reward)
    g_args.name = g_args.name or g_args[2]
                        :done()
   
                    :done()
    if util.table.has_all_value(g_args, {'page', 'item_name', 'item_name_exact'}) then
                :tag('span')
        error('page, item_name or item_name_exact must be specified')
                    :attr('class', 'divicard-flavour text-color -flavour')
    end
                    :tag('span')
   
                        :wikitext(tpl_args.flavour_text)
    g_args.large = util.cast.boolean(g_args.large)
                        :done()
   
                    :done()
    local img
                :done()
    local result
        --TODO Extras?
   
     else
     if util.table.has_one_value(g_args, core.item_link_params, nil) or g_args.item_name ~= nil then
         local header_css
         local query = {}
         if tpl_args.base_item and tpl_args.rarity ~= 'Normal' then
       
             line_type = 'double'
         if g_args.page ~= nil then
             -- TODO returns the result even if the + format is specified.
            query[#query+1] = string.format('[[%s]]', g_args.page)
         else
         else
             if g_args.item_name ~= nil then
             line_type = 'single'
                query[#query+1] = string.format('[[Has lowercase names::%s]]', g_args.item_name)
            elseif g_args.item_name_exact ~= nil then
                query[#query+1] = string.format('[[Has name::%s]]', g_args.item_name_exact)
            end
           
            query[#query] = query[#query] .. ' [[Has inventory icon::+]] [[Has infobox HTML::+]]'
           
            if g_args.link_type == 'skill' then
                query[#query] = query[#query] .. ' [[Concept:Skill gems]]'
            end
         end
         end
          
          
         query[#query+1] = '?Has name'
         local name_line = tpl_args.name
        query[#query+1] = '?Has inventory icon'
         if tpl_args.base_item and tpl_args.base_item ~= 'Prophecy' then
        query[#query+1] = '?Has infobox HTML'
             name_line = name_line .. '<br>' .. tpl_args.base_item
        query[#query+1] = '?Has alternate inventory icons'
        query[#query+1] = '?Has inventory width'
        query[#query+1] = '?Has inventory height'
       
        -- attributes
        result = util.smw.query(query, g_frame)
       
        local err
         if #result == 0 then
            err = util.misc.raise_error_or_return{raise_required=true, args=g_args, msg=string.format(
                'No results found for search parameter "%s".',
                g_args.page or g_args.item_name or g_args.item_name_exact
            )}
        elseif #result > 1 then
             err = util.misc.raise_error_or_return{raise_required=true, args=g_args, msg=string.format(
                'Too many results for search parameter "%s". Consider using page parameter instead.',
                g_args.page or g_args.item_name or g_args.item_name_exact
            )}
         end
         end
          
          
         if err ~= nil then
         container
             return err .. core.item_link_broken_cat
             :tag('span')
         end
                :attr( 'class', 'header -' .. line_type )
          
:wikitext( name_line )
         result = result[1]
            :done()
    else
           
         result = {g_args.page or g_args.item_name_exact}
         local grpcont
    end
         local valid
   
         local statcont = container:tag('span')
    for _, k in ipairs(core.item_link_params) do
        statcont
        local prop = core.map[k].property
            :attr('class', 'item-stats')
        if g_args[k] ~= nil then
            :done()
            result[prop] = g_args[k]
           
         for _, group in ipairs(core.display_groups) do
            grpcont = {}
            if group.func == nil then
                for _, disp in ipairs(group) do
                    valid = true
                    -- No args to verify which means always valid
                    if disp.args == nil then
                    elseif type(disp.args) == 'table' then
                        for _, key in ipairs(disp.args) do
                            if tpl_args[key] == nil then
                                valid = false
                                break
                            end
                        end
                    elseif type(disp.args) == 'function' then
                        valid = disp.args(tpl_args, frame)
                    end
                    if valid then
                        grpcont[#grpcont+1] = disp.func(tpl_args, frame)
                    end
                end
            else
                grpcont = group.func(tpl_args, frame)
            end
           
            if #grpcont > 0 then
                local header = ''
                if group.header then
                    header = group.header()
                end
                statcont
                    :tag('span')
                    :attr('class', 'group ' .. (group.css_class or ''))
                    :wikitext(header .. table.concat(grpcont, '<br>'))
                    :done()
            end
         end
         end
     end
     end
      
      
     if g_args.image ~= nil then
    frame:callParserFunction('#set:', tpl_args._properties)
         if result['Has alternate inventory icons'] == '' then
   
            return util.misc.raise_error_or_return{raise_required=true, args=g_args, msg=string.format(
     if tpl_args.gem_icon ~= nil then
                'Image parameter was specified, but there is no alternate art defined on page "%s"',
         container:wikitext(string.format('[[%s]]', tpl_args.gem_icon))
                result[1]
    end
            ) .. core.item_link_broken_cat}
   
        end
    -- Store the infobox so it can be accessed with ease on other pages
       
    frame:callParserFunction('#set:', {['Has infobox HTML'] = tostring(container)})
        result['Has alternate inventory icons'] = util.string.split(result['Has alternate inventory icons'], '<MANY>')
   
    if tpl_args.inventory_icon ~= nil then
        container:wikitext(string.format('[[%s]]', tpl_args.inventory_icon))
    end
   
    local infobox = mw.html.create('span')
    infobox
        :attr('class', 'infobox-page-container')
        :node(container)
          
          
        local index = tonumber(g_args.image)
    if tpl_args.skill_screenshot then
        if index ~= nil then
         infobox:wikitext(string.format('<br>[[%s|300px]]', tpl_args.skill_screenshot))
            img = result['Has alternate inventory icons'][index]
         else
            -- offset 1 is needed
            local suffix = string.len(' inventory icon.png') + 1
            -- add an extra offset by 1 to account for the space
            local prefix = string.len(string.sub(result['Has inventory icon'], 1, -suffix)) + 2
           
            for _, filename in ipairs(result['Has alternate inventory icons']) do
                if string.sub(filename, prefix, -suffix) == g_args.image then
                    img = filename
                    break
                end
            end
        end
       
        if img == nil then
            return util.misc.raise_error_or_return{raise_required=true, args=g_args, msg=string.format(
                'Alternate art with index/name "%s" not found on page "%s"',
                g_args.image, result[1]
            ) .. core.item_link_broken_cat}
        end
    elseif result['Has inventory icon'] ~= '' then
        img = result['Has inventory icon']
     end
     end
      
      
     -- output
    local out = tostring(infobox)
   
     -- ------------------------------------------------------------------------
    -- Category handling
    -- ------------------------------------------------------------------------
      
      
     local container = mw.html.create('span')
     local cats = {}
    container:attr('class', 'inline-infobox-container')
    if tpl_args.rarity == 'Unique' then
        cats[#cats+1] = string.format(i18n.categories.unique_affix, tpl_args.class)
    elseif tpl_args.base_item == 'Prophecy' then
        cats[#cats+1] = 'Prophecies'
    elseif tpl_args.is_talisman then
        cats[#cats+1] = 'Talismans'
    else
        cats[#cats+1] = tpl_args.class
    end
      
      
     if not g_args.large then
     for _, attr in ipairs(m_game.constants.attributes) do
        container:wikitext(string.format('[[%s]]', img))
        if tpl_args[attr.long_lower .. '_percent'] then
            cats[#cats+1] = string.format('%s %s', attr.long_upper, tpl_args.class)
        end
     end
     end
     
   
     container:wikitext(string.format('[[%s|%s]]', result[1], result['Has name'] or result[1]))
    local affix
          
     if tpl_args.class == 'Active Skill Gems' or tpl_args.class == 'Support Skill Gems' then
     if result['Has infobox HTML'] ~= '' then
         affix = i18n.categories.gem_tag_affix
         container
    end
            :tag('span')
     if affix ~= nil then
                :attr('class', 'inline-infobox-hover')
         for _, tag in ipairs(tpl_args.gem_tags) do
                :wikitext(result['Has infobox HTML'])
            cats[#cats+1] = string.format(affix, tag)
                :wikitext(string.format('[[%s]]', img))
        end
                :done()
    end
   
    if #tpl_args.alternate_art_inventory_icons > 0 then
        cats[#cats+1] = i18n.categories.alternate_artwork
     end
     end
      
      
     if g_args.large then
    -- TODO: add maintenance categories
        local width = tonumber(result['Has inventory width']) or tonumber(g_args.width)
   
        local height = tonumber(result['Has inventory height']) or tonumber(g_args.height)
     if tpl_args.release_version == nil then
       
         cats[#cats+1] = i18n.categories.missing_release_version
        if width and height then
    end
            img = string.format('[[%s|%sx%spx]]', img, width*c.image_size, height*c.image_size)
         elseif width then
            img = string.format('[[%s|%spx]]', img, width*c.image_size)
        elseif height then
            img = string.format('[[%s|x%spx]]', img, height*c.image_size)
        else
            img = string.format('[[%s]]', img)
        end
      
      
         container:wikitext(img)
    if tpl_args._flags.text_modifier then
         cats[#cats+1] = i18n.categories.improper_modifiers
     end
     end
       
   
     return tostring(container)
    out = out .. util.misc.add_category(cats, {ingore_blacklist=tpl_args.debug})
   
    --
    --
    --
 
     mw.logObject(os.clock() - t)
   
    return out
end
end


--
--
-- Template:Il
-- Template:Item link & Template:Sl
--
--


function p.item_link_compability (frame)
function p.item_link (frame)
     if util.misc.is_frame(frame) then
     --
         frame.args.raise = true
    -- Args/Frame
     else
    --
         frame.raise = true
   
    local tpl_args = getArgs(frame, {
        parentFirst = true,
        removeBlanks = false,
    })
    frame = util.misc.get_frame(frame)
   
    -- Backwards compability
    tpl_args.item_name = tpl_args.item_name or tpl_args[1]
    if tpl_args.item_name ~= nil then
         tpl_args.item_name = mw.ustring.lower(tpl_args.item_name)
    end
    tpl_args.name = tpl_args.name or tpl_args[2]
      
    if util.table.has_all_value(tpl_args, {'page', 'item_name', 'item_name_exact'}) then
         error(i18n.errors.item_link_invalid_args)
     end
     end
      
      
    tpl_args.large = util.cast.boolean(tpl_args.large)
   
    local img
     local result
     local result
    local status, err = pcall(function ()
        result = p.item_link(frame)
    end)
    mw.logObject(err)
      
      
     if status then
     if util.table.has_one_value(tpl_args, core.item_link_params, nil) or tpl_args.item_name ~= nil then
        return result
         local query = {}
    elseif string.match(err, ".*Too many results.*") then
       
         return err .. core.item_link_broken_cat
         if tpl_args.page ~= nil then
    else
            -- TODO returns the result even if the + format is specified.
         return require('Module:Item').itemLink(frame)
            query[#query+1] = string.format('[[%s]]', tpl_args.page)
    end
        else
end
            if tpl_args.item_name ~= nil then
 
                query[#query+1] = string.format('[[Has lowercase names::%s]]', tpl_args.item_name)
 
            elseif tpl_args.item_name_exact ~= nil then
-- ----------------------------------------------------------------------------
                query[#query+1] = string.format('[[Has name::%s]]', tpl_args.item_name_exact)
-- Result formatting templates for SMW queries
            end
-- ----------------------------------------------------------------------------
           
 
            query[#query] = query[#query] .. ' [[Has inventory icon::+]] [[Has infobox HTML::+]]'
--
           
-- Template:
            if tpl_args.link_type == 'skill' then
--
                query[#query] = query[#query] .. ' [[Concept:Skill gems]]'
 
            end
function p.simple_item_list_row(frame)
        end
    -- Args
       
    g_args = getArgs(frame, {
        query[#query+1] = '?Has name'
        parentFirst = true
        query[#query+1] = '?Has inventory icon'
    })
        query[#query+1] = '?Has infobox HTML'
    g_frame = util.misc.get_frame(frame)
        query[#query+1] = '?Has alternate inventory icons'
   
        query[#query+1] = '?Has inventory width'
    --
        query[#query+1] = '?Has inventory height'
    local args = util.string.split_args(g_args.userparam, {sep=', '})
       
    g_args.userparam = args
        -- attributes
   
        result = util.smw.query(query, frame)
    local link = p.item_link{page=g_args[1], name=g_args['?Has name'], inventory_icon=g_args['?Has inventory icon'] or '', html=g_args['?Has infobox HTML'] or ''}
       
    if args.format == nil then
        local err
        return '* ' .. link
        if #result == 0 then
    elseif args.format == 'none' then
            err = util.misc.raise_error_or_return{raise_required=true, args=tpl_args, msg=string.format(
         return link
                i18n.errors.item_link_no_results,
    elseif args.format == 'li' then
                tpl_args.page or tpl_args.item_name or tpl_args.item_name_exact
        return string.format('<li>%s</li>', link)
            )}
         elseif #result > 1 then
            err = util.misc.raise_error_or_return{raise_required=true, args=tpl_args, msg=string.format(
                i18n.errors.item_link_too_many_results,
                tpl_args.page or tpl_args.item_name or tpl_args.item_name_exact
            )}
        end
       
        if err ~= nil then
            return err .. i18n.categories.broken_item_links
        end
       
        result = result[1]
     else
     else
         error(string.format('Unrecognized format parameter "%s"', args.format))
         result = {tpl_args.page or tpl_args.item_name_exact}
    end
   
    for _, k in ipairs(core.item_link_params) do
        local prop = core.map[k].property
        if tpl_args[k] ~= nil then
            result[prop] = tpl_args[k]
        end
     end
     end
end
-- ----------------------------------------------------------------------------
-- Reponsibile for subtemplates of Template:SMW item table
--
item_table_factory = {}
function item_table_factory.intro(args)
    -- args:
    --  data_array
    --  header
      
      
     return function (frame)
     if tpl_args.image ~= nil then
         -- Args
         if result['Has alternate inventory icons'] == '' then
        local tpl_args = getArgs(frame, {
            return util.misc.raise_error_or_return{raise_required=true, args=tpl_args, msg=string.format(
             parentFirst = true
                i18n.errors.item_link_alt_art_undefined,
        })
                result[1]
         g_frame = util.misc.get_frame(frame)
             ) .. i18n.categories.broken_item_links}
         end
          
          
         --
         result['Has alternate inventory icons'] = util.string.split(result['Has alternate inventory icons'], '<MANY>')
        tpl_args.userparam = util.string.split_args(tpl_args.userparam, {sep=', '})
          
          
         local tr = mw.html.create('tr')
         local index = tonumber(tpl_args.image)
         tr
        if index ~= nil then
             :tag('th')
            img = result['Has alternate inventory icons'][index]
                :wikitext(args.header)
         else
                :done()
             -- offset 1 is needed
               
            local suffix = string.len(' inventory icon.png') + 1
        for _, rowinfo in ipairs(args.data_array) do
            -- add an extra offset by 1 to account for the space
            if rowinfo.arg == nil or tpl_args.userparam[rowinfo.arg] then
            local prefix = string.len(string.sub(result['Has inventory icon'], 1, -suffix)) + 2
                tr
           
                     :tag('th')
            for _, filename in ipairs(result['Has alternate inventory icons']) do
                        :wikitext(rowinfo.header)
                if string.sub(filename, prefix, -suffix) == tpl_args.image then
                        :done()
                    img = filename
                     break
                end
             end
             end
         end
         end
          
          
         return '<table class="wikitable sortable item-table">' .. tostring(tr)
         if img == nil then
            return util.misc.raise_error_or_return{raise_required=true, args=tpl_args, msg=string.format(
                i18n.errors.item_link_alt_art_invalid_index,
                tpl_args.image, result[1]
            ) .. i18n.categories.broken_item_links}
        end
    elseif result['Has inventory icon'] ~= '' then
        img = result['Has inventory icon']
    end
   
    -- output
   
    local container = mw.html.create('span')
    container:attr('class', 'inline-infobox-container')
   
    if not tpl_args.large then
        container:wikitext(string.format('[[%s]]', img))
     end
     end
end
     
 
    container:wikitext(string.format('[[%s|%s]]', result[1], result['Has name'] or result[1]))
-- Base test: =p.item_list_row{userparam='weapon', 'Clarity', ['?Has name'] = 'Clarity', ['?Has inventory icon'] = 'Clarity inventory icon.png', ['?Has infobox HTML'] = 'x', ['?Has inventory width'] = '1', ['?Has inventory height'] = '1'}
-- =p.item_list_row{userparam='weapon=1', ['?Has base minimum physical damage'] = '5', ['?Has minimum physical damage range average'] = '5', ['?Has minimum physical damage range minimum'] = '5', ['?Has minimum physical damage range maximum'] = '5', ['?Has base maximum physical damage'] = '6', ['?Has maximum physical damage range average'] = '8', ['?Has maximum physical damage range minimum'] = '7', ['?Has maximum physical damage range maximum'] = '9', 'Clarity', ['?Has name'] = 'Clarity', ['?Has inventory icon'] = 'Clarity inventory icon.png', ['?Has infobox HTML'] = 'x', ['?Has inventory width'] = '1', ['?Has inventory height'] = '1'}
 
function item_table_factory.row(args)
    -- args:
    --  data_array
    return function (frame)
        -- Args
        local tpl_args = getArgs(frame, {
            parentFirst = true
        })
        g_frame = util.misc.get_frame(frame)
          
          
         --
    if result['Has infobox HTML'] ~= '' then
        tpl_args.userparam = util.string.split_args(tpl_args.userparam, {sep=', '})
         container
            :tag('span')
                :attr('class', 'inline-infobox-hover')
                :wikitext(result['Has infobox HTML'])
                :wikitext(string.format('[[%s]]', img))
                :done()
    end
   
    if tpl_args.large then
        local width = tonumber(result['Has inventory width']) or tonumber(tpl_args.width)
        local height = tonumber(result['Has inventory height']) or tonumber(tpl_args.height)
          
          
         local tr = mw.html.create('tr')
         if width and height then
          
            img = string.format('[[%s|%sx%spx]]', img, width*c.image_size, height*c.image_size)
        local il_args = {
         elseif width then
             page=tpl_args[1],  
             img = string.format('[[%s|%spx]]', img, width*c.image_size)
             name=tpl_args['?Has name'],
        elseif height then
            inventory_icon=tpl_args['?Has inventory icon'],
             img = string.format('[[%s|x%spx]]', img, height*c.image_size)
            html=tpl_args['?Has infobox HTML'],
         else
            width=tpl_args['?Has inventory width'],
             img = string.format('[[%s]]', img)
            height=tpl_args['?Has inventory height'],
        }
         if tpl_args.userparam.large then
             il_args.large = tpl_args.userparam.large
         end
         end
   
        container:wikitext(img)
    end
          
          
         tr
    return tostring(container)
             :tag('td')
end
                 :wikitext(p.item_link(il_args))
 
                 :done()
--
                  
-- Template:Il
         for _, rowinfo in ipairs(args.data_array) do
--
             if rowinfo.arg == nil or tpl_args.userparam[rowinfo.arg] then
 
                 local value
function p.item_link_compability (frame)
                if rowinfo.property ~= nil then
    if util.misc.is_frame(frame) then
                    value = tpl_args['?' .. rowinfo.property]
        frame.args.raise = true
                    if rowinfo.cast then
    else
                        value = rowinfo.cast(value)
        frame.raise = true
                    end
    end
                 end
   
                 rowinfo.display(tr, value, tpl_args)
    local result
 
    local status, err = pcall(function ()
        result = p.item_link(frame)
    end)
    mw.logObject(err)
   
    if status then
        return result
    elseif string.match(err, ".*Too many results.*") then
        return err .. i18n.categories.broken_item_links
    else
        return require('Module:Item').itemLink(frame)
    end
end
 
 
-- ----------------------------------------------------------------------------
-- Result formatting templates for SMW queries
-- ----------------------------------------------------------------------------
 
--
-- Template:
--
 
function p.simple_item_list_row(frame)
    -- Args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = util.misc.get_frame(frame)
   
    --
    local args = util.string.split_args(tpl_args.userparam, {sep=', '})
    tpl_args.userparam = args
   
    local link = p.item_link{page=tpl_args[1], name=tpl_args['?Has name'], inventory_icon=tpl_args['?Has inventory icon'] or '', html=tpl_args['?Has infobox HTML'] or ''}
    if args.format == nil then
        return '* ' .. link
    elseif args.format == 'none' then
        return link
    elseif args.format == 'li' then
        return string.format('<li>%s</li>', link)
    else
        error(string.format(i18n.errors.generic_argument_parameter, 'format', args.format))
    end
end
 
-- ----------------------------------------------------------------------------
-- Reponsibile for subtemplates of Template:SMW item table
--
 
-- =p.item_table{mode='item', stat_column1_format='%s%%', conditions='[[Has item class::+]] [[Has subobject.Has stat id::~additional_*dexterity*]] [[Has rarity::Unique]]', stat_column1_header='Dexterity', stat_column1_stat_format='add', stat_column1_format='Test %s', stat_column1_stat1_id='additional_dexterity', stat_column1_stat2_id='additional_strength_and_dexterity'}
-- =p.item_table{mode='skill'}
function p.item_table(frame)
    -- args
    local tpl_args = getArgs(frame, {
            parentFirst = true
        })
    frame = util.misc.get_frame(frame)
   
    local modes = {
        skill = {
            data = core.result.skill_gem_new,
            header = i18n.item_table.skill_gem,
        },
        item = {
            data = core.result.generic_item,
            header = i18n.item_table.item,
        },
    }
   
    if tpl_args.mode == nil then
        tpl_args.mode = 'item'
    end
   
    if modes[tpl_args.mode] == nil then
        error(i18n.errors.invalid_item_table_mode)
    end
   
    local row_infos = {}
    for _, row_info in ipairs(modes[tpl_args.mode].data) do
        local enabled = false
        if row_info.arg == nil then
            enabled = true
        elseif type(row_info.arg) == 'string' and util.cast.boolean(tpl_args[row_info.arg]) then
            enabled = true
        elseif type(row_info.arg) == 'table' then
            for _, argument in ipairs(row_info.arg) do
                if util.cast.boolean(tpl_args[argument]) then
                    enabled = true
                    break
                end
            end
        end
       
        if enabled then
            row_info.options = row_info.options or {}
            row_infos[#row_infos+1] = row_info
        end
    end
   
    -- Parse stat arguments
    local stat_columns = {}
    local query_stats = {}
    local stat_results = {}
    local i = 0
    repeat
        i = i + 1
       
        local prefix = string.format('stat_column%s_', i)
        local col_info = {
            header = tpl_args[prefix .. 'header'] or tostring(i),
            format = tpl_args[prefix .. 'format'],
            stat_format = tpl_args[prefix .. 'stat_format'] or 'separate',
            order = tonumber(tpl_args[prefix .. 'order']) or (10000000 + i),
            stats = {},
            options = {},
        }
       
        local j = 0
        repeat
            j = j +1
       
            local stat_info = {
                id = tpl_args[string.format('%sstat%s_id', prefix, j)],
            }
           
            if stat_info.id then
                col_info.stats[#col_info.stats+1] = stat_info
                query_stats[stat_info.id] = {}
            else
                -- Stop iteration entirely if this was the first index but no stat was supplied. We assume that we stop in this case.
                if j == 1 then
                    i = nil
                end
                -- stop iteration
                j = nil
            end
        until j == nil
       
        -- Don't add this column if no stats were provided.
        if #col_info.stats > 0 then
            stat_columns[#stat_columns+1] = col_info
        end
    until i == nil
   
    for _, col_info in ipairs(stat_columns) do
        local row_info = {
            --arg
            header = col_info.header,
            properties = {},
            display = function(tr, data, properties)
                if col_info.stat_format == 'separate' then
                    local stat_texts = {}
                    local num_stats = 0
                    local vmax = 0
                    for _, stat_info in ipairs(col_info.stats) do
                        num_stats = num_stats + 1
                        -- stat results from outside body
                        local stat = (stat_results[data[1]] or {})[stat_info.id]
                        if stat ~= nil then
                            stat_texts[#stat_texts+1] = h.format_value(tpl_args, frame, stat, {no_color=true})
                            vmax = vmax + stat.max
                        end
                    end
                   
                    if num_stats ~= #stat_texts then
                        tr:wikitext(util.html.td.na())
                    else
                        local text
                        if col_info.format then
                            text = string.format(col_info.format, unpack(stat_texts))
                        else
                            text = table.concat(stat_texts, ', ')
                        end
                   
                        tr:tag('td')
                            :attr('data-sort-value', vmax)
                            :attr('class', 'tc -mod')
                            :wikitext(text)
                    end
                elseif col_info.stat_format == 'add' then
                    local total_stat = {
                        min = 0,
                        max = 0,
                        avg = 0,
                    }
                    for _, stat_info in ipairs(col_info.stats) do
                        local stat = (stat_results[data[1]] or {})[stat_info.id]
                        if stat ~= nil then
                            for k, v in pairs(total_stat) do
                                total_stat[k] = v + stat[k]
                            end
                        end
                    end
                   
                    if col_info.format == nil then
                        col_info.format = '%s'
                    end
                   
                    tr:tag('td')
                        :attr('data-sort-value', total_stat.max)
                        :attr('class', 'tc -mod')
                        :wikitext(string.format(col_info.format, h.format_value(tpl_args, frame, total_stat, {no_color=true})))
                else
                    error(string.format(i18n.errors.generic_argument_parameter, 'stat_format', col_info.stat_format))
                end
            end,
            order = col_info.order,
        }
        table.insert(row_infos, row_info)
    end
   
    -- sort the rows
    table.sort(row_infos, function (a, b)
        return (a.order or 0) < (b.order or 0)
    end)
   
    -- Parse query arguments
    local query = {tpl_args.conditions}
    for key, value in pairs(tpl_args) do
        if string.sub(key, 0, 2) == 'q_' then
            query[string.sub(key, 3)] = value
        end
    end
   
    --Override and set defaults
    query.limit = 1000
   
    -- Set required fields
    query[#query+1] = '?Has name#'
    query[#query+1] = '?Has iventory icon#'
    query[#query+1] = '?Has infobox HTML#'
    query[#query+1] = '?Has inventory width#'
    query[#query+1] = '?Has inventory height#'
    for _, rowinfo in ipairs(row_infos) do
        if type(rowinfo.properties) == 'function' then
            rowinfo.properties = rowinfo.properties()
        end
        for index, property in ipairs(rowinfo.properties) do
            rowinfo.options[index] = rowinfo.options[index] or {}
            query[#query+1] = '?' .. property .. '#'
        end
    end
    local results = util.smw.query(query, frame)
   
    if #results == 0 and tpl_args.default ~= nil then
        return tpl_args.default
    end
   
    if #stat_columns > 0 then
        local continue = true
        local offset = 0
        while continue do
            query = {string.format('[[-Has subobject::<q>%s</q>]] [[Has stat id::+]]', tpl_args.conditions)}
            query[#query+1] = '?Has average stat value#'
            query[#query+1] = '?Has maximum stat value#'
            query[#query+1] = '?Has minimum stat value#'
            query[#query+1] = '?Has stat id#'
            query.limit = 1000
            query.offset = 0
            local temp = util.smw.query(query, frame)
            for _, row in ipairs(temp) do
                if query_stats[row['Has stat id']] ~= nil then
                    local stat = {
                        min = tonumber(row['Has minimum stat value']),
                        max = tonumber(row['Has maximum stat value']),
                        avg = tonumber(row['Has average stat value']),
                    }
                   
                    local page = util.string.split(row[1], '#')[1]
                    if stat_results[page] == nil then
                        stat_results[page] = {[row['Has stat id']] = stat}
                    else
                        stat_results[page][row['Has stat id']] = stat
                    end
                end
            end
           
            -- stop iteration if we didn't hit the query limit
            if #temp == 1000 then
                offset = offset + 1000
            else
                continue = false
            end
        end
    end
   
    local tbl = mw.html.create('table')
    tbl:attr('class', 'wikitable sortable item-table')
   
    -- Header
   
    local tr = tbl:tag('tr')
    tr
        :tag('th')
            :wikitext(modes[tpl_args.mode].header)
            :done()
           
    for _, row_info in ipairs(row_infos) do
        tr
            :tag('th')
                :attr('data-sort-type', row_info.sort_type or 'number')
                :wikitext(row_info.header)
                :done()
    end
   
    for _, row in ipairs(results) do
        tr = tbl:tag('tr')
   
        local il_args = {
            page=row[1],
            name=row['Has name'],
            inventory_icon=row['Has inventory icon'],
            html=row['Has infobox HTML'],
            width=row['Has inventory width'],
            height=row['Has inventory height'],
        }
       
        if tpl_args.large then
            il_args.large = tpl_args.large
        end
       
         tr
             :tag('td')
                 :wikitext(p.item_link(il_args))
                :done()
               
        for _, rowinfo in ipairs(row_infos) do
            -- this has been cast from a function in an earlier step
            local display = true
            for index, property in ipairs(rowinfo.properties) do
                -- this will bet set to an empty value not nil confusingly
                if row[property] == '' then
                    if rowinfo.options[index].optional ~= true then
                        display = false
                        break
                    else
                        row[property] = nil
                    end
                end
            end
            if display then
                rowinfo.display(tr, row, rowinfo.properties)
            else
                tr:wikitext(util.html.td.na())
            end
        end
    end
   
    return tostring(tbl)
end
 
 
item_table_factory = {}
function item_table_factory.intro(args)
    -- args:
    --  data_array
    --  header
   
    return function (frame)
        -- Args
        local tpl_args = getArgs(frame, {
            parentFirst = true
        })
        frame = util.misc.get_frame(frame)
       
        --
        tpl_args.userparam = util.string.split_args(tpl_args.userparam, {sep=', '})
       
        local tr = mw.html.create('tr')
        tr
            :tag('th')
                :wikitext(args.header)
                 :done()
                  
         for _, rowinfo in ipairs(args.data_array) do
             if rowinfo.arg == nil or tpl_args.userparam[rowinfo.arg] then
                 tr
                    :tag('th')
                        :wikitext(rowinfo.header)
                        :done()
            end
        end
       
        return '<table class="wikitable sortable item-table">' .. tostring(tr)
    end
end
 
-- ----------------------------------------------------------------------------
-- Item lists
-- ----------------------------------------------------------------------------
 
function p.skill_gem_list_by_gem_tag(frame)
    -- Args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = util.misc.get_frame(frame)
   
    if tpl_args.class == 'Support Skill Gems' then
    elseif tpl_args.class == 'Active Skill Gems' then
    else
        error(i18n.errors.invalid_item_class)
    end
 
    local query = {}
    query[#query+1] = string.format('[[Has item class::%s]]', tpl_args.class)
    query[#query+1] = '?Has gem tags'
    query[#query+1] = '?Has name'
    query[#query+1] = '?Has inventory icon'
    --query[#query+1] = '?Has infobox HTML'
    query.limit = 5000
    query.sort = 'Has name'
   
    local results = util.smw.query(query, frame)
   
    local tags = {}
   
    for _, row in ipairs(results) do
        row['Has gem tags'] = util.string.split(row['Has gem tags'], '<MANY>')
        for _, tag in ipairs(row['Has gem tags']) do
            if tags[tag] == nil then
                tags[tag] = {}
            end
            table.insert(tags[tag], row)
        end
    end
   
    local tags_sorted = {}
    for tag, _ in pairs(tags) do
        table.insert(tags_sorted, tag)
    end
    table.sort(tags_sorted)
   
    local tbl = mw.html.create('table')
    tbl
        :attr('class', 'wikitable sortable')
        :tag('tr')
            :tag('th')
                 :wikitext('Tag')
                :done()
            :tag('th')
                :wikitext('Skills')
                :done()
            :done()
   
    for _, tag in ipairs(tags_sorted) do
        local rows = tags[tag]
        local tr = tbl:tag('tr')
        tr
            :tag('td')
                 :wikitext(tag)
           
        local td = tr:tag('td')
        for i, row in ipairs(rows) do
            td:wikitext(p.item_link{page=row[1], name=row['Has Name'], inventory_icon=row['Has inventory icon'], html=row['Has infobox HTML'] or ''})
            if i < #rows then
                td:wikitext('<br>')
             end
             end
         end
         end
       
        return tostring(tr)
     end
     end
 
    return tostring(tbl)
end
end
-- Template:SMW item table/skill_gem/intro
p.skill_gem_list_intro = item_table_factory.intro{data_array=core.result.skill_gem, header='Skill gem'}
-- Template:SMW item table/skill gem
p.skill_gem_list_row = item_table_factory.row{data_array=core.result.skill_gem}
-- Template:SMW item table/intro
p.item_list_intro = item_table_factory.intro{data_array=core.result.generic_item, header='Item'}
-- Template:SMW item table
p.item_list_row = item_table_factory.row{data_array=core.result.generic_item}


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Item lists
-- Misc. Item templates
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


function p.skill_gem_list_by_gem_tag(frame)
--
     -- Args
-- Template:Item class
     g_args = getArgs(frame, {
--
 
function p.item_class (frame)
     -- Get args
     local tpl_args = getArgs(frame, {
         parentFirst = true
         parentFirst = true
     })
     })
     g_frame = util.misc.get_frame(frame)
     frame = util.misc.get_frame(frame)
if not doInfoCard then
doInfoCard = require('Module:Infocard')._main
end
      
      
     if g_args.class == 'Support Skill Gems' then
     core.factory.table_cast('name', {key='full', tbl=m_game.constants.item.class})(tpl_args, frame)
     elseif g_args.class == 'Active Skill Gems' then
   
     if tpl_args.name_list ~= nil then
        tpl_args.name_list = util.string.split(tpl_args.name_list, ', ')
     else
     else
         error('invalid item class')
         tpl_args.name_list = {}
     end
     end
    local query = {}
    query[#query+1] = string.format('[[Has item class::%s]]', g_args.class)
    query[#query+1] = '?Has gem tags'
    query[#query+1] = '?Has name'
    query[#query+1] = '?Has inventory icon'
    --query[#query+1] = '?Has infobox HTML'
    query.limit = 5000
    query.sort = 'Has name'
      
      
     local results = util.smw.query(query, g_frame)
    --
   
     local ul = mw.html.create('ul')
    for _, item in ipairs(tpl_args.name_list) do
        ul
            :tag('li')
                :wikitext(item)
                :done()
    end
   
 
    -- Output Infocard
   
    local tplargs = {
        ['header'] = tpl_args.name,
        ['subheader'] = i18n.item_class_infobox.page .. i18n.item_class_infobox.info,
        [1] = i18n.item_class_infobox.also_referred_to_as .. tostring(ul),
    }
      
      
     local tags = {}
     -- cats
      
      
     for _, row in ipairs(results) do
     local cats = {
         row['Has gem tags'] = util.string.split(row['Has gem tags'], '<MANY>')
         'Item classes',
         for _, tag in ipairs(row['Has gem tags']) do
         tpl_args.name,
            if tags[tag] == nil then
     }
                tags[tag] = {}
            end
            table.insert(tags[tag], row)
        end
     end
      
      
     local tags_sorted = {}
     -- Done
    for tag, _ in pairs(tags) do
        table.insert(tags_sorted, tag)
    end
    table.sort(tags_sorted)
      
      
     local tbl = mw.html.create('table')
     return doInfoCard(tplargs) .. util.misc.add_category(cats, {ingore_blacklist=tpl_args.debug})
    tbl
end
        :attr('class', 'wikitable sortable')
        :tag('tr')
            :tag('th')
                :wikitext('Tag')
                :done()
            :tag('th')
                :wikitext('Skills')
                :done()
            :done()
   
    for _, tag in ipairs(tags_sorted) do
        local rows = tags[tag]
        local tr = tbl:tag('tr')
        tr
            :tag('td')
                :wikitext(tag)
           
        local td = tr:tag('td')
        for i, row in ipairs(rows) do
            td:wikitext(p.item_link{page=row[1], name=row['Has Name'], inventory_icon=row['Has inventory icon'], html=row['Has infobox HTML'] or ''})
            if i < #rows then
                td:wikitext('<br>')
            end
        end
    end
 
    return tostring(tbl)
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Misc. Item templates
-- Debug stuff
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


--
p.debug = {}
-- Template:Item class
function p.debug.show_range_vars ()
--
    for _, prop in ipairs({'HTML', 'range text', 'range colour'}) do
 
        for var, data in pairs(core.stat_map) do
function p.item_class (frame)
            mw.logObject(string.format('%s %s', data.property, prop))
    -- Get args
        end
    g_args = getArgs(frame, {
       
        parentFirst = true
         for _, data in ipairs(core.dps_map) do
    })
            mw.logObject(string.format('Has %s %s', data.property, prop))
    g_frame = util.misc.get_frame(frame)
         end
if not doInfoCard then
doInfoCard = require('Module:Infocard')._main
end
   
    core.factory.table_cast('name', {key='full', tbl=m_game.constants.item.class})()
   
    if g_args.name_list ~= nil then
         g_args.name_list = util.string.split(g_args.name_list, ', ')
    else
         g_args.name_list = {}
     end
     end
   
end
    --
   
    local ul = mw.html.create('ul')
    for _, item in ipairs(g_args.name_list) do
        ul
            :tag('li')
                :wikitext(item)
                :done()
    end
   


    -- Output Infocard
function p.debug._tbl_data(tbl)
   
     keys = {}
    local tplargs = {
     for _, data in ipairs(core.result.generic_item) do
        ['header'] = g_args.name,
         if type(data.arg) == 'string' then
        ['subheader'] = '[[Item class]] ' .. util.html.abbr('(?)', 'Item classes categorize items. Classes are often used to restrict items or skill gems to a specific class or by item filters'),
             keys[data.arg] = 1
        [1] = 'Also reffered to as:' .. tostring(ul),
        elseif type(data.arg) == 'table' then
    }
            for _, arg in ipairs(data.arg) do
   
                keys[arg] = 1
    -- cats
             end
   
    local cats = {
        'Item classes',
        g_args.name,
    }
   
    -- Done
   
     return doInfoCard(tplargs) .. util.misc.add_category(cats, {ingore_blacklist=g_args.debug})
end
 
-- ----------------------------------------------------------------------------
-- Debug stuff
-- ----------------------------------------------------------------------------
 
p.debug = {}
function p.debug.show_range_vars ()
     for _, prop in ipairs({'HTML', 'range text', 'range colour'}) do
         for var, data in pairs(core.stat_map) do
             mw.logObject(string.format('%s %s', data.property, prop))
        end
       
        for _, data in ipairs(core.dps_map) do
             mw.logObject(string.format('Has %s %s', data.property, prop))
         end
         end
     end
     end
end
-- ----------------------------------------------------------------------------
-- Property views..
-- ----------------------------------------------------------------------------
function v.itembox ()
    local container = v._itembox_core()
      
      
     if g_args.gem_icon ~= nil then
     local out = {}
        container:wikitext(string.format('[[%s]]', g_args.gem_icon))
     for key, _ in pairs(keys) do
    end
         out[#out+1] = string.format("['%s'] = '1'", key)
   
    -- Store the infobox so it can be accessed with ease on other pages
     g_frame:callParserFunction('#set:', {['Has infobox HTML'] = tostring(container)})
   
    if g_args.inventory_icon ~= nil then
         container:wikitext(string.format('[[%s]]', g_args.inventory_icon))
    end
   
    local infobox = tostring(container)
   
    local span = mw.html.create('span')
    span
        :attr('class', 'infobox-page-container')
        :wikitext(infobox)
       
    if g_args.skill_screenshot then
        span:wikitext(string.format('<br>[[%s|300px]]', g_args.skill_screenshot))
     end
     end
      
      
     return tostring(span) .. v._itembox_categories()
     return table.concat(out, ', ')
end
end


function v._itembox_core()
function p.debug.generic_item_all()
    -- needed later for setting caculated properties
     return p.debug._tbl_data(core.result.generic_item)
    g_args._properties = {}
    local container = mw.html.create('span')
        :attr( 'class', 'item-box -' .. g_args.frame_type)
      
    if g_args.class == 'Divination Card' then
        --TODO div card code
    else
        local header_css
        if g_args.base_item and g_args.rarity ~= 'Normal' then
            line_type = 'double'
        else
            line_type = 'single'
        end
       
        local name_line = g_args.name
        if g_args.base_item and g_args.base_item ~= 'Prophecy' then
            name_line = name_line .. '<br>' .. g_args.base_item
        end
       
        container
            :tag('span')
                :attr( 'class', 'header -' .. line_type )
:wikitext( name_line )
            :done()
           
        local grpcont
        local valid
        local statcont = container:tag('span')
        statcont
            :attr('class', 'item-stats')
            :done()
           
        for _, group in ipairs(core.display_groups) do
            grpcont = {}
            if group.func == nil then
                for _, disp in ipairs(group) do
                    valid = true
                    -- No args to verify which means always valid
                    if disp.args == nil then
                    elseif type(disp.args) == 'table' then
                        for _, key in ipairs(disp.args) do
                            if g_args[key] == nil then
                                valid = false
                                break
                            end
                        end
                    elseif type(disp.args) == 'function' then
                        valid = disp.args()
                    end
                    if valid then
                        grpcont[#grpcont+1] = disp.func()
                    end
                end
            else
                grpcont = group.func()
            end
           
            if #grpcont > 0 then
                statcont
                    :tag('span')
                    :attr('class', 'group ' .. (group.css_class or ''))
                    :wikitext(table.concat(grpcont, '<br>'))
                    :done()
            end
        end
    end
   
    g_frame:callParserFunction('#set:', g_args._properties)
   
    return container
end
end


function v._itembox_categories()
function p.debug.skill_gem_all()
    cats = {}
     return p.debug._tbl_data(core.result.skill_gem_new)
    if g_args.rarity == 'Unique' then
        cats[#cats+1] = 'Unique ' .. g_args.class
    elseif g_args.base_item == 'Prophecy' then
        cats[#cats+1] = 'Prophecies'
    else
        cats[#cats+1] = g_args.class
    end
   
    for _, attr in ipairs(m_game.constants.attributes) do
        if g_args[attr.long_lower .. '_percent'] then
            cats[#cats+1] = string.format('%s %s', attr.long_upper, g_args.class)
        end
     end
   
    local suffix
    if g_args.class == 'Active Skill Gems' or g_args.class == 'Support Skill Gems' then
        suffix = ' (gem tag)'
    end
    if suffix ~= nil then
        for _, tag in ipairs(g_args.gem_tags) do
            cats[#cats+1] = tag .. suffix
        end
    end
   
    if #g_args.alternate_art_inventory_icons > 0 then
        cats[#cats+1] = 'Items with alternate artwork'
    end
   
    -- TODO: add maintenance categories
   
    if g_args.release_version == nil then
        cats[#cats+1] = 'Items without a release version'
    end
   
    --
    -- Output formatting
    --
   
    return util.misc.add_category(cats, {ingore_blacklist=g_args.debug})
end
end


return p
return p

Revision as of 12:57, 12 March 2017

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

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

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


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


-- SMW reworked item module

-- ----------------------------------------------------------------------------
-- TODO
-- ----------------------------------------------------------------------------
-- Items
-- -----
--
-- DROP restriction improvements:
--  drop location (area)
--  drop difficulty
--  drop monster type(s)
--
-- divination card support
--
-- unique items:
--  3D art
--  supporter attribution
--
-- potentially include quality bonus in calculations
-- singular for weapon class in infoboxes
--
-- Maps: 
--  Area level can be retrieved eventually
--
-- Essence: 
--  type column
--  monster modifier info
--
-- Weapons:
--  show DPS in infobox; hover?
--
-- ----------
-- Item table
-- ----------
-- Skills need proper range values in their tables 
--
-- ----------
-- Item class
-- ----------
--
-- remove the ul if name_list is not provided
-- maybe smw


-- ----------------------------------------------------------------------------
-- Imports
-- ----------------------------------------------------------------------------
local xtable = require('Module:Table')
local util = require('Module:Util')
local getArgs = require('Module:Arguments').getArgs
local m_game = require('Module:Game')
local m_skill = require('Module:Skill')

local p = {}
local c = {}
c.image_size = 39
-- 9 doesn't hit the query limit
c.max_mod_params = 11
c.max_stat_params = 8

-- ----------------------------------------------------------------------------
-- Strings
-- ----------------------------------------------------------------------------
-- This section contains strings used by this module.
-- Add new strings here instead of in-code directly, this will help other
-- people to correct spelling mistakes easier and help with translation to
-- other PoE wikis.
--
-- TODO: Maybe move this out to a separate sub-page module
local i18n = {
    range = '(%s to %s)',
    inventory_icon = 'File:%s inventory icon.png',
    status_icon = 'File:%s status icon.png',
    skill_screenshot = 'File:%s skill screenshot.jpg',
    skill_icon = 'File:%s skill icon.png',
    divination_card_art = 'File:%s card art.png',
    gem_tag_category = '[[:Category:%s (gem tag)|%s]]',

    categories = {
        -- maintenance cats
        broken_item_links = '[[Category:Pages with broken item links]]',
        improper_modifiers = 'Items with improper modifiers',
        missing_release_version = 'Items without a release version',

        -- regular cats
        alternate_artwork = 'Items with alternate artwork',

        -- misc
        gem_tag_affix = '%s (gem tag)',
        unique_affix = 'Unique %s',
    },

    stat_skip_patterns = {
        maps = {
            '%d+%% increased Quantity of Items found in this Area',
            '%d+%% increased Rarity of Items found in this Area',
            '%+%d+%% Monster pack size',
            -- ranges
            '%(%d+%-%d+%)%% increased Quantity of Items found in this Area',
            '%(%d+%-%d+%)%% increased Rarity of Items found in this Area',
            '%+%(%d+%-%d+%)%% Monster pack size',
        },
        jewels = {
            'Limited to %d+ %(Hidden%)',
            'Jewel has a radius of %d+ %(Hidden%)',
        },
    },


    help_text_defaults = {
        active_gem = 'Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.',
        support_gem = 'This is a Support Gem. It does not grant a bonus to your character, but skills in sockets connected to it. Place into an item socket connected to a socket containing the Active Skill Gem you wish to augment. Right click to remove from a socket.',
        hideout_doodad = 'Right click on this item then left click on a location on the ground to create the object.',
        jewel = 'Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.',
    },

    -- Used by the item table
    item_table = {
        item = 'Item',
        skill_gem = 'Skill gem',

        physical_dps = util.html.abbr('pDPS', 'physical damage per second'),
        fire_dps = util.html.abbr('Fire DPS', 'fire damage per second'),
        cold_dps = util.html.abbr('Cold DPS', 'cold damage per second'),
        lightning_dps = util.html.abbr('Light. DPS', 'lightning damage per second'),
        chaos_dps = util.html.abbr('Chaos DPS', 'chaos damage per second'),
        elemental_dps = util.html.abbr('eDPS', 'elemental damage (i.e. fire/cold/lightning) per second'),
        poison_dps = util.html.abbr('Poison DPS', 'poison damage (i.e. physical/chaos) per second'),
        dps = util.html.abbr('DPS', 'total damage (i.e. physical/fire/cold/lightning/chaos) per second'),
        base_item = 'Base Item',
        item_class = 'Item Class',
        essence_tier = 'Essence<br>Tier',
        drop_level = 'Drop<br>Level',
        stack_size = 'Stack<br>Size',
        stack_size_currency_tab = util.html.abbr('Tab<br>Stack<br>Size', 'Stack size in the currency stash tab'),
        armour = util.html.abbr('AR', 'Armour'),
        evasion = util.html.abbr('EV', 'Evasion Rating'),
        energy_shield = util.html.abbr('ES', 'Energy Shield'),
        block = util.html.abbr('Block', 'Chance to Block'),
        damage = util.html.abbr('Damage', 'Colour coded damage'),
        attacks_per_second = util.html.abbr('APS', 'Attacks per second'),
        local_critical_strike_chance = util.html.abbr('Crit', 'Local weapon critical strike chance'),
        flask_life = util.html.abbr('Life', 'Life regenerated over the flask duration'),
        flask_mana = util.html.abbr('Mana', 'Mana regenerated over the flask duration'),
        flask_duration = 'Duration',
        flask_charges_per_use = util.html.abbr('Usage', 'Number of charges consumed on use'),
        flask_maximum_charges = util.html.abbr('Capacity', 'Maximum number of flask charges held'),
        item_limit = 'Limit',
        jewel_radius = 'Radius',
        map_tier = 'Map<br>Tier',
        map_level = 'Map<br>Level',
        map_guild_character = util.html.abbr('Char', 'Character for the guild tag'),
        buff_effects = 'Buff Effects',
        stats = 'Stats',
        effects = 'Effect(s)',
        flavour_text = 'Flavour Text',
        help_text = 'Help Text',
        buff_icon = 'Buff<br>Icon',

        -- Skills
        support_gem_letter = util.html.abbr('L', 'Support gem letter.'),
        skill_icon = 'Icon',
        description = 'Description',
        skill_critical_strike_chance = util.html.abbr('Crit', 'Critical Strike Chance'),
        cast_time = util.html.abbr('Cast<br>Time', 'Casting time of the skill in seconds'),
        damage_effectiveness = util.html.abbr('Dmg.<br>Eff.', 'Damage Effectiveness'),
        mana_cost_multiplier = util.html.abbr('MCM', 'Mana cost multiplier - missing values indicate it changes on gem level'),
        mana_cost = util.html.abbr('Mana', 'Mana cost'),
        reserves_mana_suffix = util.html.abbr('R', 'reserves mana'),
        vaal_souls_requirement = util.html.abbr('Souls', 'Vaal souls requirement in Normal/Cruel/Merciless difficulty'),
        stored_uses = util.html.abbr('Uses', 'Maximum number of stored uses'),
        primary_radius = util.html.abbr('R1', 'Primary radius'),
        secondary_radius = util.html.abbr('R2', 'Secondary radius'),
        tertiary_radius = util.html.abbr('R3', 'Tertiary radius'),
    },

    -- Used by the item info box
    tooltips = {
        corrupted = 'Corrupted',
        support_icon = 'Icon: %s',
        radius = 'Radius: %s',
        mana_reserved = 'Mana Reserved: %s',
        mana_cost = 'Mana Cost: %s',
        mana_multiplier = 'Mana Multiplier: %s',
        vaal_souls_per_use = 'Souls per use: %s',
        stored_uses = 'Can store %s use(s)',
        cooldown_time = 'Cooldown Time: %s',
        cast_time = 'Cast Time: %s',
        critical_strike_chance = 'Critical Strike Chance: %s',
        damage_effectiveness = 'Damage Effectiveness: %s',
        projectile_speed = 'Projectile Speed: %s',
        physical_damage = 'Physical Damage: %s',
        elemental_damage = 'Elemental Damage:%s',
        chaos_damage = 'Chaos Damage: %s',
        attacks_per_second = 'Attacks per Second: %s',
        weapon_range = 'Weapon Range: %s',
        map_level = 'Map Level: %s',
        map_tier = 'Map Tier: %s',
        map_guild_character = util.html.abbr('Guild Character', 'When used in guild creation, this map can be used for the listed character') .. ': %s',
        item_quantity = 'Item Quantity: %s',
        item_rarity = 'Item Rarity: %s',
        monster_pack_size = 'Monster Pack Size: %s',
        limited_to = 'Limited to: %s',
        flask_mana_recovery = 'Recovers %s Mana over %s seconds',
        flask_life_recovery = 'Recovers %s Life over %s seconds',
        flask_duration = 'Lasts %s Seconds',
        flask_charges_per_use = 'Consumes %s of %s Charges on use',
        chance_to_block = 'Chance to Block: %s',
        armour = 'Armour: %s',
        evasion = 'Evasion: %s',
        energy_shield = 'Energy Shield: %s',
        talisman_tier = 'Talisman Tier: %s',
        stack_size = 'Stack Size: %s',
        essence_tier = 'Essence Tier: %s',
        requires = 'Requires %s',
        level_inline = 'Level %s',
        level = 'Level: %s',
        drop_restrictions = 'Acquisition',
        league_restriction = util.html.abbr('League(s):', 'Item can be obtained in relation to these league(s)') .. ' %s',
        drop_disabled = 'DROP DISABLED',
        gem_quality = 'Per 1% Quality:',
        variation_singular = 'Variation',
        variation_plural = 'Variations',
        favour_cost = 'Favour cost: %s',
        seal_cost = 'Seal cost: <br>%s',
    },

    item_class_infobox = {
        page = '[[Item class]]',
        info = util.html.abbr('(?)', 'Item classes categorize items. Classes are often used to restrict items or skill gems to a specific class or by item filters'),
        also_referred_to_as = 'Also referred to as:',
    },

    debug = {
        base_item_property_not_found = 'Base item property not found: %s',
        property_value_mismatch = 'Value for argument "%s" is set to something else then default: %s',
    },

    errors = {
        missing_base_item = 'Rarity is set to above normal, but base item is not set. A base item for rarities above normal is required!',
        missing_rarity = 'Base item parameter is set, but rarity is set to normal. A rarity above normal is required!',
        duplicate_base_items = 'More then one result found for the specified base item. Consider using base_item_page or base_item_id to narrow down the results.',
        smw_mod_count_duplicate_format = '%s (pages: %s)',
        smw_mod_count_mismatch = 'Number of mods found does not match number of queried mods. Not found: "%s", duplicates: "%s"',
        invalid_league = '%s is not a recognized league',
        invalid_argument = 'Argument %s to item template is invalid. Please check the documentation for acceptable values.',
        not_a_percentage = '%s must be a percentage (in range 0 to 100).',
        invalid_tag = '%s is not a valid tag',
        item_link_invalid_args = 'page, item_name or item_name_exact must be specified',
        item_link_no_results = 'No results found for search parameter "%s".',
        item_link_too_many_results = 'Too many results for search parameter "%s". Consider using page parameter instead.',
        item_link_alt_art_undefined = 'Image parameter was specified, but there is no alternate art defined on page "%s"',
        item_link_alt_art_invalid_index = 'Alternate art with index/name "%s" not found on page "%s"',
        generic_argument_parameter = 'Unrecognized %s parameter "%s"',
        invalid_item_class = 'Invalid item class',
        invalid_item_table_mode = 'Invalid mode for item table',
        non_unique_relic = 'Only unique items can be be relics',
    },
}

-- ----------------------------------------------------------------------------
-- Other stuff
-- ----------------------------------------------------------------------------

local h = {}

function h.debug(tpl_args, func)
    if tpl_args.debug ==  nil then
        return
    end
    func()
end

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

-- helper to loop over the range variables easier
h.range_map = {
    min = {
        property = ' range minimum',
        var = '_range_minimum',
    },
    max = {
        property = ' range maximum',
        var = '_range_maximum',
    },
    avg = {
        property = ' range average',
        var = '_range_average',
    },
}

function h.handle_range_args(tpl_args, frame, argument_key, property, value, fmt_options)
    fmt_options = mw.clone(fmt_options)
    fmt_options.return_color = true
    local html, colour = h.format_value(tpl_args, frame, value, fmt_options)
    tpl_args[argument_key .. '_html'] = html
    tpl_args._properties[property .. ' HTML'] = html
    tpl_args._properties[property .. ' range colour'] = colour
    
    fmt_options = mw.clone(fmt_options)
    fmt_options.no_color = true
    tpl_args._properties[property .. ' range text'] = h.format_value(tpl_args, frame, value, fmt_options)
end

function h.stats_update(tpl_args, id, value, modid, key)
    if tpl_args[key][id] == nil then
        tpl_args[key][id] = {
            references = {modid},
            min = value.min,
            max = value.max,
            avg = value.avg,
        }
    else
        if modid ~= nil then
            table.insert(tpl_args[key][id].references, modid)
        end
        tpl_args[key][id].min = tpl_args[key][id].min + value.min
        tpl_args[key][id].max = tpl_args[key][id].max + value.max
        tpl_args[key][id].avg = tpl_args[key][id].avg + value.avg
    end
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

h.tbl = {}

function h.tbl.range_properties(property)
    return function()
        local props = {}
        for _, prop in ipairs({'maximum', 'text', 'colour'}) do
            props[#props+1] = string.format('Has %s range %s', property, prop)
        end
        return props
    end
end

h.tbl.display = {}
function h.tbl.display.na_or_val(tr, value, data)
    return h.na_or_val(tr, value)
end

function h.tbl.display.seconds(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('%ss', value)
    end)
end

function h.tbl.display.percent(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('%s%%', value)
    end)
end

function h.tbl.display.wikilink(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('[[%s]]', value)
    end)
end

h.tbl.display.factory = {}
function h.tbl.display.factory.value(args)
    args.options = args.options or {}

    return function(tr, data, properties)
        values = {}
        
        for index, prop in ipairs(properties) do
            local value = data[prop]
            if args.options[index] and args.options[index].fmt then
                value = string.format(args.options[index].fmt, value)
            end
            values[#values+1] = value
        end
        
        
        local td = tr:tag('td')
        td:attr('data-sort-value', table.concat(values, ', '))
        td:wikitext(table.concat(values, ', '))
        if args.colour then
            td:attr('class', 'tc -' .. args.colour)
        end
    end
end

function h.tbl.display.factory.range(args)
    -- args: table
    --  property
    return function (tr, data, properties)
        tr
            :tag('td')
                :attr('data-sort-value', data[string.format('Has %s range maximum', args.property)] or '0')
                :attr('class', 'tc -' .. (data[string.format('Has %s range colour', args.property)] or 'default'))
                :wikitext(data[string.format('Has %s range text', args.property)])
                :done()
    end
end

function h.format_value(tpl_args, frame, value, options)
    -- value: table
    --  min:
    --  max:
    -- options: table
    --  fmt: formatter to use for the value instead of valfmt
    --  fmt_range: formatter to use for the range values. Default: (%s to %s)
    --  inline: Use this format string to insert value
    --  inline_color: colour to use for the inline value; false to disable colour
    --  func: Function to adjust the value with before output
    --  color: colour code for util.html.poe_color, overrides mod colour
    --  no_color: set to true to ingore colour entirely
    --  return_color: also return colour

    if options.no_color == nil then
        if options.color then
            value.color = options.color
        elseif value.base ~= value.min or value.base ~= value.max then
            value.color = 'mod'
        else
            value.color = 'value'
        end
    end
    
    if options.func ~= nil then
        value.min = options.func(tpl_args, frame, value.min)
        value.max = options.func(tpl_args, frame, value.max)
    end
    
    if options.fmt == nil then
        options.fmt = '%s'
    elseif type(options.fmt) == 'function' then
        options.fmt = options.fmt(tpl_args, frame)
    end
    
    if value.min == value.max then
        value.out = string.format(options.fmt, value.min)
    else
        value.out = string.format(string.format(options.fmt_range or i18n.range, options.fmt, options.fmt), value.min, value.max)
    end
    
    if options.no_color == nil then
        value.out = util.html.poe_color(value.color, value.out)
    end
    
    local return_color
    if options.return_color ~= nil then
        return_color = value.color
    end
    
    local text = options.inline
    
    if type(text) == 'string' then
    elseif type(text) == 'function' then
        text = text(tpl_args, frame)
    else
        text = nil
    end
    
    if text and text ~= '' then
        local color
        if options.inline_color == nil then
            color = 'default'
        elseif options.inline_color ~= false then
            color = color.inline_color
        end 
        
        if color ~= nil then
            text = util.html.poe_color(color, text)
        end
        
        return string.format(text, value.out), return_color
    end
    
    -- If we didn't return before, return here
    return value.out, return_color
end

-- ----------------------------------------------------------------------------
-- core
-- ----------------------------------------------------------------------------

local core = {}

function core.validate_mod(tpl_args, frame, args)
    -- args:
    --  key   - implict or explicit
    --  i
    --  value
    local value = tpl_args[args.key .. args.i]
    local out = {
        result=nil,
        modid=nil,
        type=args.key,
    }
    
    if value ~= nil then
        table.insert(tpl_args.mods, value)
        table.insert(tpl_args[args.key .. '_mods'], value)
        out.modid = value
        --out.result = nil
        table.insert(tpl_args._mods, out)
        return true
    else
        value = tpl_args[args.key .. args.i .. '_text']
        if value ~= nil then
            tpl_args._flags.text_modifier = true
            --table.insert(tpl_args._subobjects, {
            --    ['Is mod number'] = args.i,
            --   ['Has mod text'] = value,
            --})
            out.result = value
            table.insert(tpl_args._mods, out)
            return true
        end
    end
    
    return false
end

function core.process_smw_mods(tpl_args, frame)
    if #tpl_args.mods == 0 then 
        return
    end
    
    local mods = {}
    for _, mod_data in ipairs(tpl_args._mods) do
        if mod_data.result == nil then
            mods[mod_data.modid] = mod_data
        end
    end
    
    local results = {}
    local result
    local query
    local number_of_queries = math.ceil(#tpl_args.mods / c.max_mod_params)
    local pages = {}
    
    for query_number=1, number_of_queries do
        local offset = (query_number-1)*c.max_mod_params
        local mod_ids = {}
        for j=(1+offset), (c.max_mod_params+offset) do
            mod_ids[#mod_ids+1] = tpl_args.mods[j]
        end
    
        query = {
            string.format('[[Is mod::%s]]', table.concat(mod_ids, '||')),
            '?#=Page',
            '?Is mod#',
            '?Has mod group#',
            '?Has mod type#',
            '?Has stat text#',
            '?Has level requirement#',
        }
        
        result = util.smw.query(query, frame)
    
        -- remap this as table with modids as key
        for _, row in ipairs(result) do
            results[#results+1] = row
            
            local mod_data = mods[row['Is mod']]
            mod_data.result = row
            
            -- needed for the query
            pages[#pages+1] = row[1]
            -- needed for the mapping stats to mods
            pages[row[1]] = mod_data
            
            -- 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(row['Has level requirement']) * 0.8)
                if req > tpl_args[key] then
                    tpl_args[key] = req
                end
            end
        end
    end
    
    -- TODO: Can items have mods twice? I dont think so, if they do it would need to be accounted for here
    if #results ~= #tpl_args.mods then
        local missing = {}
        for _, modid in ipairs(tpl_args.mods) do
            if mods[modid].result == nil then
                missing[#missing+1] = modid
            end
        end

        local duplicates = {}
        for _, row in ipairs(results) do
            if duplicates[row['Is mod']] == nil then
                duplicates[row['Is mod']] = {row['Page']}
            else
                table.insert(duplicates[row['Is mod']], row['Page'])
            end
        end
        
        local text = {}
        for modid, pages in pairs(duplicates) do
            if #pages <= 1 then
                duplicates[modid] = nil
            else
                text[#text+1] = string.format(i18n.errors.smw_mod_count_duplicate_format, modid, table.concat(pages, ', '))
            end
        end
        
        error(string.format(i18n.errors.smw_mod_count_mismatch, table.concat(missing, ', '), table.concat(text)))
    end
    
    -- fetch stats
    
    number_of_queries = math.ceil(#pages / c.max_stat_params)
    
    for query_number=1, number_of_queries do
        local offset = (query_number-1)*c.max_stat_params
        local temp_pages = {}
        for j=(1+offset), (c.max_stat_params+offset) do
            temp_pages[#temp_pages+1] = pages[j]
        end
        
        query = {
            string.format('[[-Has subobject::%s]] [[Has stat id::+]] [[Has minimum stat value::+]] [[Has maximum stat value::+]]', table.concat(temp_pages, '||')),
            '?Is stat number#',
            '?Has stat id#',
            '?Has minimum stat value#',
            '?Has maximum stat value#',
        }
        
        local stats = util.smw.query(query, frame)
        
        -- process and cache stats
        for _, stat in ipairs(stats) do
            local mod_data = pages[util.string.split(stat[1], '#')[1]]
            if mod_data.result.stats == nil then
                mod_data.result.stats = {stat, }
            else
                mod_data.result.stats[#mod_data.result.stats+1] = stat
            end
        
            local id = stat['Has stat id']
            local value = {
                min = tonumber(stat['Has minimum stat value']),
                max = tonumber(stat['Has maximum stat value']),
            }
            value.avg = (value.min+value.max)/2
            
            h.stats_update(tpl_args, id, value, mod_data.result['Is mod'], '_stats')
            if mod_data.type ~= 'implicit' then
                h.stats_update(tpl_args, id, value, mod_data.result['Is mod'], '_explicit_stats')
            end
        end
    end
end

function core.process_base_item(tpl_args, frame, args)
    local query = {}

    if tpl_args.base_item_id ~= nil then
        query[#query+1] = string.format('[[Has metadata id::%s]]', tpl_args.base_item_id)
    elseif tpl_args.base_item_page ~= nil then
        query[#query+1] = string.format('[[%s]]', tpl_args.base_item_page)
    elseif tpl_args.base_item ~= nil then
        query[#query+1] = string.format('[[Has name::%s]]', tpl_args.base_item)
    elseif tpl_args.rarity ~= 'Normal' then
        error(core.err{msg=i18n.errors.missing_base_item})
    else
        return
    end
    
    if #query > 1 and tpl_args.rarity == 'Normal' then
        error(core.err{msg=i18n.errors.missing_rarity})
    end
    
    query[#query] = query[#query] .. string.format('[[Has item class::%s]] [[Has rarity::Normal]]', tpl_args['class'])
    query[#query+1] = '?Has implicit mod ids#'
    query[#query+1] = '?Has metadata id'
    query[#query+1] = '?Has name'
    
    for _, k in ipairs(tpl_args._base_item_args) do
        if core.map[k].property ~= nil then
            query[#query+1] = string.format('?%s#', core.map[k].property)
        end
    end
    
    local result = util.smw.query(query, frame)
    
    if #result > 1 then
        error(core.err{msg=i18n.errors.duplicate_base_items})
        -- TODO be more explicit in the error?
    end
    
    result = result[1]
    
    tpl_args.base_item_data = result
    core.process_arguments(tpl_args, frame, {array={'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.property then
            local value = result[data.property]
            -- I can just use data.default since it will be nil if not provided. Neat! ;)
            if value ~= "" and tpl_args[k] == data.default then
                tpl_args[k] = value
                if data.property_func ~= nil then
                    data.property_func(tpl_args, frame)
                elseif data.func ~= nil then
                    data.func(tpl_args, frame)
                end
            elseif value == "" then
                h.debug(tpl_args, function ()
                    mw.logObject(string.format(i18n.debug.base_item_property_not_found, data.property))
                end)
            elseif tpl_args[k] ~= data.default then
                h.debug(tpl_args, function ()
                    mw.logObject(string.format(i18n.debug.property_value_mismatch, k, tostring(tpl_args[k])))
                end)
            end
        elseif data.property_func ~= nil then
            -- property func is set, but not a property. Probably fetching subobjects here
            data.property_func(tpl_args, frame)
        end
    end
end

function core.process_arguments(tpl_args, frame, args)
    for _, k in ipairs(args.array) do
        local data = core.map[k]
        table.insert(tpl_args._total_args, k)
        if data.no_copy == nil then
            table.insert(tpl_args._base_item_args, k)
        end
        if data.func ~= nil then
            data.func(tpl_args, frame)
            --[[local status, err = pcall(data.func)
            -- an error was raised, return the error string instead of the template
            if not status then
                return err
            end
            ]]--
        end
        if data.default ~= nil and tpl_args[k] == nil then
            tpl_args[k] = data.default
        end
    end
end
        
function core.process_mod_stats(tpl_args, args)
    local lines = {}
    
    local skip = core.class_specifics[tpl_args.class]
    if skip then
        skip = skip.skip_stat_lines
    end 
    
    for _, modinfo in ipairs(tpl_args._mods) do
        if modinfo.type == args.type then
            if modinfo.modid == nil then
                table.insert(lines, modinfo.result)
            else
                for _, line in ipairs(util.string.split(modinfo.result['Has stat text'], '<br>')) do
                    if line ~= '' then
                        if skip == nil then
                            table.insert(lines, line)
                        else
                            local skipped = false
                            for _, pattern in ipairs(skip) do
                                if string.match(line, pattern) then
                                    skipped = true
                                    break
                                end
                            end
                            if not skipped then
                                table.insert(lines, line)
                            end
                        end
                    end
                end
            end
        end
    end
    
    if #lines == 0 then
        return
    else
        return table.concat(lines, '<br>')
    end
end

function core.process_upgraded_from(tpl_args, args)
    return
    -- TODO DISABLED
    --[[
    local sets = {}
    local setid = 1
    local set
    repeat
        local prefix = string.format('upgrade_from_set%s_', setid)
        local groupid = 1
        local group
        set = {
            groups = {},
            optional = util.cast.boolean(tpl_args[prefix .. 'optional']),
            text = tpl_args[prefix .. 'text'],
        }
        repeat 
            local group_prefix = string.format('%sgroup%s_', prefix, index)
            group = {
                item = tpl_args[group_prefix .. 'item'],
                count = tonumber(tpl_args[group_prefix .. 'count']) or 1,
                notes = tpl_args[group_prefix .. 'notes'],
            }
            
            if group.item == nil then
                set.groups[#set.groups+1] = group
            end
            
            groupid = groupid + 1
        until group.item == nil end
        
        -- set was empty, can terminate safely
        if #set.groups == 0 then
            set = nil
        else
            setid = setid + 1
            sets[#sets+1] = set
        end
    until set == nil end
    
    if #sets == 0 then
        return
    end
    
    tpl_args.upgrade_from_sets = sets
    
    
    -- TODO: Semantic properties, validate items
    ]]--
end

function core.err(args)
    local err = mw.html.create('div')
    err
        :attr('style', 'font-color: red; font-weight: bold;')
        :wikitext(args.msg or string.format(i18n.errors.invalid_argument, args.key))
        :done()
        
    return tostring(err)
end

--
-- function factory
--
core.factory = {}
function core.factory.array_table_cast(k, args)
    -- Arguments:
    --
    -- tbl
    -- errmsg
    return function (tpl_args, frame)
        local elements
        
        if tpl_args[k] ~= nil then
            elements = util.string.split(tpl_args[k], ', ')
            for _, element in ipairs(elements) do 
                local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
                if r == nil then
                    error(core.err{msg=string.format(args.errmsg, element)})
                end
            end
            tpl_args[k] = xtable:new(elements)
        end
    end
end

function core.factory.table_cast(k, args)
    return function (tpl_args, frame)
        args.value = tpl_args[k]
        tpl_args[k] = util.table.find_in_nested_array(args)
        if tpl_args[k] == nil then
            error(core.err{key=k})
        end
    end
end

function core.factory.assoc_table_cast(k, args)
    -- Arguments:
    --
    -- tbl
    -- errmsg
    return function (tpl_args, frame)
        local elements
        
        if tpl_args[k] ~= nil then
            elements = util.string.split(tpl_args[k], ', ')
            for _, element in ipairs(elements) do 
                if args.tbl[element] == nil then
                    error(core.err{msg=string.format(args.errmsg, element)})
                end
            end
            tpl_args[k] = elements
        end
    end
end

function core.factory.number_cast(k)
    return function (tpl_args, frame)
        tpl_args[k] = tonumber(tpl_args[k])
    end
end

function core.factory.boolean_cast(k)
    return function(tpl_args, frame)
        if tpl_args[k] ~= nil then
            tpl_args[k] = util.cast.boolean(tpl_args[k])
        end
    end
end

function core.factory.percentage(k)
    return function (tpl_args, frame)
        local v = tonumber(tpl_args[k])
        
        if v == nil then
            return core.err{key=k}
        end
        
        if v < 0 or v > 100 then
            return core.err{msg=string.format(i18n.errors.not_a_percentage, k)}
        end
        
        tpl_args[k] = v
    end
end

 function core.factory.display_value(args)
    -- args:
    --  type: Type of the keys (nil = regular, gem = skill gems, stat = stats)
    --  options<Array>:
    --   key: key to use
    --   allow_zero: allow zero values
    --   hide_default: hide the value if this is set
    --   hide_default_key: key to use if it isn't equal to the key parameter
    --   -- from h.format_value --
    --   fmt: formatter to use for the value instead of valfmt
    --   fmt_range: formatter to use for the range values. Default: (%s to %s)
    --   insert: insert results into this object
    --   func: Function to adjust the value with before output
    --   color: colour code for util.html.poe_color, overrides mod colour
    --   no_color: set to true to ingore colour entirely
    
    for k, default in pairs({options = {}}) do
        if args[k] == nil then
            args[k] = default
        end
    end
    
    return function (tpl_args, frame)
        local base_values = {}
        local temp_values = {}
        if args.type == 'gem' then
            if not core.class_groups.gems.keys[tpl_args.class] then
                return
            end
            for i, data in ipairs(args.options) do
                local value = tpl_args['static_' .. data.key]
                if value ~= nil then
                    base_values[#base_values+1] = value
                    temp_values[#temp_values+1] = {value={min=value,max=value}, index=i}
                else
                    value = {
                        min=tpl_args[string.format('level1_%s', data.key)],
                        max=tpl_args[string.format('level%s_%s', tpl_args.max_level, data.key)], 
                    }
                    if value.min == nil or value.max == nil then
                    else
                        base_values[#base_values+1] = value.min
                        temp_values[#temp_values+1] = {value=value, index=i}
                    end
                end
            end
        elseif args.type == 'stat' then
            for i, data in ipairs(args.options) do
                local value = tpl_args._stats[data.key]
                if value ~= nil then
                    base_values[i] = value.min
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
        else
            for i, data in ipairs(args.options) do
                base_values[i] = tpl_args[data.key]
                local value = {}
                if tpl_args[data.key .. '_range_minimum'] ~= nil then
                    value.min = tpl_args[data.key .. '_range_minimum']
                    value.max = tpl_args[data.key .. '_range_maximum']
                elseif tpl_args[data.key] ~= nil then
                    value.min = tpl_args[data.key]
                    value.max = tpl_args[data.key]
                end
                if value.min == nil then
                else
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
        end
        
        local final_values = {}
        for i, data in ipairs(temp_values) do
            local opt = args.options[data.index]
            local insert = false
            if opt.hide_default == nil then
                insert = true
            elseif opt.hide_default_key == nil then
                local v = data.value
                if opt.hide_default ~= v.min and opt.hide_default ~= v.max then
                    insert = true
                end
            else
                local v = {
                    min = tpl_args[opt.hide_default_key .. '_range_minimum'],
                    max = tpl_args[opt.hide_default_key .. '_range_maximum'],
                }
                if v.min == nil or v.max == nil then
                    if opt.hide_default ~= tpl_args[opt.hide_default_key] then
                        insert = true
                    end
                elseif opt.hide_default ~= v.min and opt.hide_default ~= v.max then
                    insert = true
                end
            end
            
            if insert == true then
                table.insert(final_values, data)
            end
        end
        
        -- all zeros = dont display and return early
        if #final_values == 0 then
            return nil
        end
        
        local out = {}
        
        for i, data in ipairs(final_values) do
            local value = data.value
            value.base = base_values[data.index]
            
            local options = args.options[data.index]
            
            if options.color == nil and args.type == 'gem' then
                value.color = 'value'
            end
            
            out[#out+1] = h.format_value(tpl_args, frame, value, options)
        end
        
        if args.inline then
            return util.html.poe_color('default', string.format(args.inline, unpack(out)))
        else
            return table.concat(out, '')
        end
    end
end

function core.factory.display_value_only(key)
    return function(tpl_args, frame)
        return tpl_args[key]
    end
end

function core.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 = util.html.abbr(value, args.tbl[args.key])
        end
        return value
    end
end

function core.factory.damage_html(args)
    return function(tpl_args, frame)
        if args.key ~= 'physical' then
           args.color = args.key
           args.no_color = true
        end
        local keys = {
            min=args.key .. '_damage_min',
            max=args.key .. '_damage_max',
        }
        local value = {}
        
        for ktype, key in pairs(keys) do
            value[ktype] = core.factory.display_value{options={ [1] = {
                key = key,
                no_color = args.no_color,
                hide_default = 0
            }}}(tpl_args, frame)
        end
        
        if value.min and value.max then
            value = value.min .. '&ndash;' .. value.max
            if args.color ~= nil then
                value = util.html.poe_color(args.color, value)
            end
            tpl_args[args.key .. '_damage_html'] = value
        end
    end
end

function core.factory.copy_stats(args)
    return function(tpl_args, frame)
        if tpl_args._stats[args.stat_key] then
            if tpl_args._stats[args.stat_key].min ~= tpl_args._stats[args.stat_key].max then
                error('TODO: Missing support for range item limits')
            end
            tpl_args[args.key] = tpl_args._stats[args.stat_key].min
        end
    end
end

--
-- argument mapping
--
-- format:
-- tpl_args key = {
--   no_copy = true or nil           -- When loading an base item, dont copy this key 
--   property = 'prop',              -- Property associated with this key
--   property_func = function or nil -- Function to unpack the property into a native lua value. 
--                                      If not specified, func is used. 
--                                      If neither is specified, value is copied as string
--   func = function or nil          -- Function to unpack the argument into a native lua value and validate it. 
--                                      If not specified, value will not be set.
--   default = object                -- Default value if the parameter is nil
-- }
core.map = {
    -- special params
    html = {
        no_copy = true,
        property = 'Has infobox HTML',
        func = nil,
    },
    implicit_stat_text = {
        property = 'Has implicit stat text',
        func = function(tpl_args, frame)
            tpl_args.implicit_stat_text = core.process_mod_stats(tpl_args, {type='implicit'})
        end,
    },
    explicit_stat_text = {
        property = 'Has explicit stat text',
        func = function(tpl_args, frame)
            tpl_args.explicit_stat_text = core.process_mod_stats(tpl_args, {type='explicit'})
            
            if tpl_args.is_talisman or tpl_args.is_corrupted then
                if tpl_args.explicit_stat_text == nil or tpl_args.explicit_stat_text == '' then
                    tpl_args.explicit_stat_text = util.html.poe_color('corrupted', i18n.tooltips.corrupted)
                else
                    tpl_args.explicit_stat_text = (tpl_args.explicit_stat_text or '') .. '<br>' .. util.html.poe_color('corrupted', i18n.tooltips.corrupted)
                end
            end
        end,
    },
    stat_text = {
        property = 'Has stat text',
        func = function(tpl_args, frame)
            local sep = ''
            if tpl_args.implicit_stat_text and tpl_args.explicit_stat_text then
                sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
            end
            local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
            
            if string.len(text) > 0 then
                tpl_args.stat_text = text
            end
        end,
    },
    -- generic
    class = {
        no_copy = true,
        property = 'Has item class',
        func = core.factory.table_cast('class', {key='full', tbl=m_game.constants.item.class}),
    },
    rarity = {
        no_copy = true,
        property = 'Has rarity',
        func = core.factory.table_cast('rarity', {key={'full', 'long_lower'}, tbl=m_game.constants.item.rarity, rtrkey='full'}),
    },
    name = {
        no_copy = true,
        property = 'Has name',
        func = nil,
    },
    size_x = {
        property = 'Has inventory width',
        func = core.factory.number_cast('size_x'),
    },
    size_y = {
        property = 'Has inventory height',
        func = core.factory.number_cast('size_y'),
    },
    drop_enabled = {
        property = 'Is drop enabled',
        func = core.factory.boolean_cast('drop_enabled'),
        default = true,
    },
    drop_level = {
        no_copy = true,
        property = 'Has drop level',
        func = core.factory.number_cast('drop_level'),
    },
    drop_level_maximum = {
        no_copy = true,
        property = 'Has maximum drop level',
        func = core.factory.number_cast('drop_level_maximum'),
    },
    drop_leagues = {
        no_copy = true,
        property = 'Has league drop restrictions',
        func = core.factory.assoc_table_cast('drop_leagues', {tbl=m_game.constants.leagues, errmsg=i18n.errors.invalid_league}),
    },
    required_level = {
        property = 'Has base level requirement',
        func = core.factory.number_cast('required_level'),
        default = 1,
    },
    required_level_final = {
        property = 'Has level requirement',
        func = function(tpl_args, frame)
            tpl_args.required_level_final = tpl_args.required_level
        end,
        default = 1,
    },
    required_dexterity = {
        property = 'Has base dexterity requirement',
        func = core.factory.number_cast('required_dexterity'),
        default = 0,
    },
    required_strength = {
        property = 'Has base strength requirement',
        func = core.factory.number_cast('required_strength'),
        default = 0,
    },
    required_intelligence = {
        property = 'Has base intelligence requirement',
        func = core.factory.number_cast('required_intelligence'),
        default = 0,
    },
    inventory_icon = {
        no_copy = true,
        property = 'Has inventory icon',
        func = function(tpl_args, frame)
            tpl_args.inventory_icon_id = tpl_args.inventory_icon or tpl_args.name
            tpl_args.inventory_icon = string.format(i18n.inventory_icon, tpl_args.inventory_icon_id) 
        end,
    },
    -- note: this must be called after inventory item to work correctly as it depends on tpl_args.inventory_icon_id being set
    alternate_art_inventory_icons = {
        no_copy = true,
        property = 'Has alternate inventory icons',
        func = function(tpl_args, frame)
            local icons = {}
            if tpl_args.alternate_art_inventory_icons ~= nil then
                local names = util.string.split(tpl_args.alternate_art_inventory_icons, ', ')
                
                for _, name in ipairs(names) do
                    icons[#icons+1] = string.format(i18n.inventory_icon, string.format('%s %s', tpl_args.inventory_icon_id, name))
                end
            end
            tpl_args.alternate_art_inventory_icons = icons
        end,
        default = {},
    },
    buff_icon = {
        property = 'Has buff icon',
        func = function(tpl_args, frame)
            tpl_args.buff_icon = string.format(i18n.status_icon, tpl_args.name) 
        end,
    },
    help_text = {
        property = 'Has help text',
        func = nil,
    },
    flavour_text = {
        no_copy = true,
        property = 'Has flavour text',
        func = nil,
    },
    tags = {
        property = 'Has tags',
        property_func = function(tpl_args, frame)
            tpl_args.tags = util.string.split(tpl_args.tags, '<MANY>')
        end,
        func = core.factory.assoc_table_cast('tags', {
            tbl = m_game.constants.tags,
            errmsg = i18n.errors.invalid_tag,
        }),
    },
    metadata_id = {
        no_copy = true,
        property = 'Has metadata id',
        func = nil,
    },
    is_corrupted = {
        no_copy = true,
        property = 'Is corrupted',
        func = core.factory.boolean_cast('is_corrupted'), 
        default = false,
    },
    is_relic = {
        no_copy = true,
        property = 'Is relic',
        func = function(tpl_args, frame)
            core.factory.boolean_cast('is_relic')(tpl_args, frame)
            if tpl_args.is_relic == true and tpl_args.rarity ~= 'Unique' then
                error(i18n.errors.non_unique_relic)
            end
        end,
        default = false,
    },
    --
    -- specific section
    --
    -- amulets
    is_talisman = {
        property = 'Is talisman',
        func = core.factory.boolean_cast('is_talisman'),
        default = false,
    },
    
    talisman_tier = {
        property = 'Has talisman tier',
        func = core.factory.number_cast('talisman_tier'),
    },
    
    -- flasks
    charges_max = {
        property = 'Has base maximum flask charges',
        func = core.factory.number_cast('charges_max'),
    },
    charges_per_use = {
        property = 'Has base flask charges per use',
        func = core.factory.number_cast('charges_per_use'),
    },
    flask_mana = {
        property = 'Has base flask mana recovery',
        func = core.factory.number_cast('flask_mana'),
    },
    flask_life = {
        property = 'Has base flask life recovery',
        func = core.factory.number_cast('flask_life'),
    },
    flask_duration = {
        property = 'Has base flask duration',
        func = core.factory.number_cast('flask_duration'),
    },
    buff_id = {
        property = 'Has buff id',
        func = nil,
    },
    buff_values = {
        property = nil,
        property_func = function(tpl_args, frame)
            local results = util.smw.query({
                string.format('[[-Has subobject::%s]] [[Is number::+]] [[Has buff value::+]]', tpl_args.base_item_page),
                '?Is number#',
                '?Has buff value#',
            }, frame)
            local values = {}
            for _, row in ipairs(results) do
                local i = tonumber(row['Is number'])
                local value = tonumber(row['Has buff value'])
                values[i] = value
                if i ~= nil then
                    local key = 'buff_value' .. i
                    tpl_args[key] = value
                    tpl_args._subobjects[key] = {
                        ['Is number'] = i,
                        ['Has buff value'] = value,
                    }
                end
            end
            
            tpl_args.buff_values = values
        end,
        func = function(tpl_args, frame)
            local values = {}
            local i = 0
            local value
            local key
            repeat 
                i = i + 1
                key = 'buff_value' .. i
                value = tonumber(tpl_args[key])
                values[i] = value
                tpl_args[key] = value
                tpl_args._subobjects[key] = {
                    ['Is number'] = i,
                    ['Has buff value'] = value,
                }
            until values[i] == nil
            
            tpl_args.buff_values = values
        end,
    },
    buff_stat_text = {
        property = 'Has buff stat text',
        func = nil,
    },
    
    -- weapons & active skills
    critical_strike_chance = {
        property = 'Has base critical strike chance',
        func = core.factory.number_cast('critical_strike_chance'),
    },
    -- weapons
    attack_speed = {
        property = 'Has base attack speed',
        func = core.factory.number_cast('attack_speed'),
    },
    physical_damage_min = {
        property = 'Has base minimum physical damage',
        func = core.factory.number_cast('physical_damage_min'),
    },
    physical_damage_max = {
        property = 'Has base maximum physical damage',
        func = core.factory.number_cast('physical_damage_max'),
    },
    range = {
        property = 'Has base weapon range',
        func = core.factory.number_cast('range'),
    },
    -- armor-type stuff
    armour = {
        property = 'Has base armour',
        func = core.factory.number_cast('armour'),
        default = 0,
    },
    energy_shield = {
        property = 'Has base energy shield',
        func = core.factory.number_cast('energy_shield'),
        default = 0,
    },
    evasion = {
        property = 'Has base evasion',
        func = core.factory.number_cast('evasion'),
        default = 0,
    },
    -- shields
    block = {
        property = 'Has base block',
        func = core.factory.number_cast('block'),
    },
    -- skill gem stuff
    gem_description = {
        property = 'Has description',
        func = nil,
    },
    dexterity_percent = {
        property = 'Has dexterity percentage',
        func = core.factory.percentage('dexterity_percent'),
    },
    strength_percent = {
        property = 'Has strength percentage',
        func = core.factory.percentage('strength_percent'),
    },
    intelligence_percent = {
        property = 'Has intelligence percentage',
        func = core.factory.percentage('intelligence_percent'),
    },
    primary_attribute = {
        property = 'Has primary attribute',
        func = function(tpl_args, frame)
            for _, attr in ipairs(m_game.constants.attributes) do
                local val = tpl_args[attr.long_lower .. '_percent'] 
                if val and val >= 60 then
                    tpl_args['primary_attribute'] = attr.long_upper
                    return
                end
            end
            tpl_args['primary_attribute'] = 'None'
        end,
    },
    gem_tags = {
        property = 'Has gem tags',
        -- TODO: default rework
        func = core.factory.array_table_cast('gem_tags', {
            tbl = m_game.constants.item.gem_tags,
            errmsg = i18n.errors.invalid_tag,
        }),
        default = {},
    },
    experience = {
        property = 'Has maximum experience',
        func = core.factory.percentage('experience'),
    },
    skill_screenshot = {
        property = 'Has skill screenshot',
        func = function(tpl_args, frame)
            tpl_args.skill_screenshot = string.format(i18n.skill_screenshot, tpl_args.name)
        end,
    },
    -- Active gems only
    gem_icon = {
        property = 'Has skill gem icon',
        func = function(tpl_args, frame)
            -- TODO readd support if needed.
            tpl_args.gem_icon = string.format(i18n.skill_icon, tpl_args.name) 
        end,
    },
    -- Support gems only
    support_gem_letter_html = {
        property = 'Has support gem letter HTML',
        func = function(tpl_args, frame)
            if tpl_args.support_gem_letter == nil then
                return
            end
        
            -- TODO replace this with a loop possibly
            local css_map = {
                strength = 'red',
                intelligence = 'blue',
                dexterity = 'green',
            }
            local id
            for k, v in pairs(css_map) do
                k = string.format('%s_percent', k)
                if tpl_args[k] and tpl_args[k] > 50 then
                    id = v
                    break
                end
            end
            
            if id ~= nil then
                local container = mw.html.create('span')
                container
                    :attr('class', string.format('support-gem-id-%s', id))
                    :wikitext(tpl_args.support_gem_letter)
                    :done()
                tpl_args.support_gem_letter_html = tostring(container)
            end
        end,
    },
    -- Maps
    map_tier = {
        property = 'Has map tier',
        func = core.factory.number_cast('tier'),
    },
    map_guild_character = {
        no_copy = true,
        property = 'Has map guild character',
        func = nil,
    },
    map_area_id = {
        no_copy = true,
        property = 'Has map area id',
        func = nil, -- TODO: Validate against a query?
    },
    map_area_level = {
        no_copy = true,
        property = 'Has map area level',
        func = core.factory.number_cast('map_area_level'),
    },
    unique_map_guild_character = {
        property = 'Has unique map guild character',
        property_func = function(tpl_args, frame)
            tpl_args.map_guild_character = tpl_args.unique_map_guild_character
            tpl_args.unique_map_guild_character = nil
        end,
        func = nil,
    },
    unique_map_area_id = {
        property = 'Has unique map area id',
        func = nil, -- TODO: Validate against a query?
        property_func = function(tpl_args, frame)
            tpl_args.map_area_id = tpl_args.unique_map_area_id
            tpl_args.unique_map_area_id = nil
        end,
    },
    unique_map_area_level = {
        property = 'Has unique map area level',
        func = core.factory.number_cast('unique_map_area_level'),
        property_func = function(tpl_args, frame)
            tpl_args.map_area_level = tpl_args.unique_map_area_level
            tpl_args.unique_map_area_level = nil
        end,
    },
    --
    -- Currency-like items
    --
    stack_size = {
        property = 'Has stack size',
        func = core.factory.number_cast('stack_size'),
    },
    stack_size_currency_tab = {
        property = 'Has currency tab stack size',
        func = core.factory.number_cast('stack_size_currency_tab'),
    },
    description = {
        property = 'Has description',
        func = nil,
    },
    cosmetic_type = {
        property = 'Has cosmetic type',
        func = nil,
    },
    -- for essences
    is_essence = {
        property = 'Is essence',
        func = core.factory.boolean_cast('is_essence'),
        default = false,
    },
    essence_level_restriction = {
        property = 'Has essence level restriction',
        func = core.factory.number_cast('essence_level_restriction'),
    },
    essence_tier = {
        property = 'Has essence tier',
        func = core.factory.number_cast('essence_tier'),
    },
    --
    -- hideout doodads (HideoutDoodads.dat)
    --
    is_master_doodad = {
        property = 'Is master doodad',
        func = core.factory.boolean_cast('is_master_doodad'),
    },
    master = {
        property = 'Is sold by master',
        -- todo validate against list of master names
        func = core.factory.table_cast('master', {key='full', tbl=m_game.constants.masters}),
    },
    master_level_requirement = {
        property = 'Has master level requirement',
        func = core.factory.number_cast('master_level_requirement'),
    },
    master_favour_cost = {
        property = 'Has master favour cost',
        func = core.factory.number_cast('master_favour_cost'),
    },
    variation_count = {
        property = 'Has variation count',
        func = core.factory.number_cast('variation_count'),
    },
    -- Propehcy
    prophecy_id = {
        property = 'Has prophecy id',
        func = nil,
    },
    prediction_text = {
        property = 'Has prophecy prediction text',
        func = nil,
    },
    seal_cost_normal = {
        property = 'Has prophecy seal cost in normal difficulty',
        func = core.factory.number_cast('seal_cost_normal'),
    },
    seal_cost_cruel = {
        property = 'Has prophecy seal cost in cruel difficulty',
        func = core.factory.number_cast('seal_cost_cruel'),
    },
    seal_cost_merciless = {
        property = 'Has prophecy seal cost in merciless difficulty',
        func = core.factory.number_cast('seal_cost_merciless'),
    },
    -- Divination cards
    card_art = {
        proprety = 'Has divination card art',
        func = function(tpl_args, frame)
            tpl_args.card_art = string.format(i18n.divination_card_art, tpl_args.name)
        end,
    },
    card_reward = {
        property = 'Has divinication card reward',
        func = nil,
    },
    -- ------------------------------------------------------------------------
    -- derived stats
    -- ------------------------------------------------------------------------
    
    -- For rarity != normal, rarity already verified
    base_item = {
        no_copy = true,
        property = 'Has base item',
        func = function(tpl_args, frame)
            tpl_args.base_item = tpl_args.base_item_data['Has name']
        end,
    },
    base_item_id = {
        no_copy = true,
        property = 'Has base item metadata id',
        func = function(tpl_args, frame)
            tpl_args.base_item_id = tpl_args.base_item_data['Has metadata id']
        end,
    },
    base_item_page = {
        no_copy = true,
        property = 'Has base item wiki page',
        func = function(tpl_args, frame)
            tpl_args.base_item_page = tpl_args.base_item_data[1]
        end,
    },
    
    name_list = {
        no_copy = true,
        property = 'Has names',
        func = function(tpl_args, frame)
            if tpl_args.name_list ~= nil then
                tpl_args.name_list = util.string.split(tpl_args.name_list, ', ')
                tpl_args.name_list[#tpl_args.name_list+1] = tpl_args.name
            else
                tpl_args.name_list = {tpl_args.name}
            end
        end,
    },
    name_list_lower = {
        no_copy = true,
        property = 'Has lowercase names',
        func = function(tpl_args, frame)
            tpl_args.name_list_lower = {}
            for index, value in ipairs(tpl_args.name_list) do
                tpl_args.name_list_lower[index] = mw.ustring.lower(value)
            end
        end,
    },
    gem_tags_difference = {
        no_copy = true,
        property = 'Has gem tags difference',
        func = function(tpl_args, frame)
            if tpl_args.gem_tags ~= nil then
                local gtags = {}
                -- copy tags
                for _, data in ipairs(m_game.constants.item.gem_tags) do
                    if data.full and data.full ~= '' then
                        gtags[data.full] = true
                    end
                end
                -- delete existing tags
                for _, tag in ipairs(tpl_args.gem_tags) do
                    gtags[tag] = nil
                end
                
                -- add them as ordered list and not as hash table so it is consistent with the other gem tag list
                tpl_args.gem_tags_difference = {}
                for key, value in pairs(gtags) do
                    tpl_args.gem_tags_difference[#tpl_args.gem_tags_difference+1] = key
                end
            end
        end,
    },
    frame_type = {
        no_copy = true,
        property = nil,
        func = function(tpl_args, frame)
            if tpl_args.name == 'Prophecy' or tpl_args.base_item == 'Prophecy' then
                tpl_args.frame_type = 'prophecy'
                return
            end
        
            local var = core.class_specifics[tpl_args.class]
            if var ~= nil and var.frame_type ~= nil then
                tpl_args.frame_type = var.frame_type
                return
            end
            
            if tpl_args.is_relic then
                tpl_args.frame_type = 'relic'
                return
            end
            
            tpl_args.frame_type = string.lower(tpl_args.rarity)
        end,
    },
    --
    -- args populated by mod validation
    -- 
    mods = {
        no_copy = true,
        property = 'Has mod ids',
        default = {},
    },
    implicit_mods = {
        property = 'Has implicit mod ids',
        property_func = function (tpl_args)
            tpl_args.implicit_mods = util.string.split(tpl_args.implicit_mods, '<MANY>')
            for _, modid in ipairs(tpl_args.implicit_mods) do 
                tpl_args.mods[#tpl_args.mods+1] = modid
                tpl_args._mods[#tpl_args._mods+1] = {
                    result=nil,
                    modid=modid,
                    type='implicit',
                }
            end
        end,
        default = {},
    },
    explicit_mods = {
        no_copy = true,
        property = 'Has explicit mod ids',
        default = {},
    },
    physical_damage_html = {
        no_copy = true,
        property = nil,
        func = core.factory.damage_html{key='physical'},
    },
    fire_damage_html = {
        no_copy = true,
        property = nil,
        func = core.factory.damage_html{key='fire'},
    },
    cold_damage_html = {
        no_copy = true,
        property = nil,
        func = core.factory.damage_html{key='cold'},
    },
    lightning_damage_html = {
        no_copy = true,
        property = nil,
        func = core.factory.damage_html{key='lightning'},
    },
    chaos_damage_html = {
        no_copy = true,
        property = nil,
        func = core.factory.damage_html{key='chaos'},
    },
    damage_avg = {
        no_copy = true,
        property = 'Has average damage',
        func = function(tpl_args, frame)
            local dmg = {min=0, max=0}
            for key, _ in pairs(dmg) do
                for _, data in ipairs(m_game.constants.damage_types) do
                    dmg[key] = dmg[key] + tpl_args[string.format('%s_damage_%s_range_average', data.short_lower, key)]
                end
            end
            
            dmg = (dmg.min + dmg.max) / 2
            
            tpl_args.damage_avg = dmg
        end,
    },
    damage_html = {
        no_copy = true,
        property = 'Has damage HTML',
        func = function(tpl_args, frame)
            local text = {}
            for _, data in ipairs(m_game.constants.damage_types) do
                local value = tpl_args[data.short_lower .. '_damage_html']
                if value ~= nil then
                    text[#text+1] = value
                end
            end
            if #text > 0 then
                tpl_args.damage_html = table.concat(text, '<br>')
            end
        end,
    },
    item_limit = {
        no_copy = true,
        property = 'Has item limit',
        func = core.factory.number_cast('item_limit'),
    },
    jewel_radius = {
        no_copy = true,
        property = 'Has jewel radius',
        func = core.factory.copy_stats{key='jewel_radius', stat_key='local_jewel_effect_base_radius'}
    },
    jewel_radius_html = {
        no_copy = true,
        property = 'Has jewel radius HTML',
        func = function(tpl_args, frame)
            if tpl_args.jewel_radius then
                tpl_args.jewel_radius_html = string.format('%s (%i)', (m_game.constants.item.jewel_radius_to_size[tpl_args.jewel_radius] or '?'), tpl_args.jewel_radius)
            end
        end,
    },
}

core.stat_map = {
    range = {
        property = 'Has weapon range',
        stats_add = {
            'local_weapon_range_+',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    physical_damage_min = {
        property = 'Has minimum physical damage',
        stats_add = {
            'local_minimum_added_physical_damage',
        },
        stats_increased = {
            'local_physical_damage_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    physical_damage_max = {
        property = 'Has maximum physical damage',
        stats_add = {
            'local_maximum_added_physical_damage',
        },
        stats_increased = {
            'local_physical_damage_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    fire_damage_min = {
        default = 0,
        property = 'Has minimum fire damage',
        stats_add = {
            'local_minimum_added_fire_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'fire',
            fmt = '%i',
        },
    },
    fire_damage_max = {
        default = 0,
        property = 'Has maximum fire damage',
        stats_add = {
            'local_maximum_added_fire_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'fire',
            fmt = '%i',
        },
    },
    cold_damage_min = {
        default = 0,
        property = 'Has minimum cold damage',
        stats_add = {
            'local_minimum_added_cold_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'cold',
            fmt = '%i',
        },
    },
    cold_damage_max = {
        default = 0,
        property = 'Has maximum cold damage',
        stats_add = {
            'local_maximum_added_cold_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'cold',
            fmt = '%i',
        },
    },
    lightning_damage_min = {
        default = 0,
        property = 'Has minimum lightning damage',
        stats_add = {
            'local_minimum_added_lightning_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'lightning',
            fmt = '%i',
        },
    },
    lightning_damage_max = {
        default = 0,
        property = 'Has maximum lightning damage',
        stats_add = {
            'local_maximum_added_lightning_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'lightning',
            fmt = '%i',
        },
    },
    chaos_damage_min = {
        default = 0,
        property = 'Has minimum chaos damage',
        stats_add = {
            'local_minimum_added_chaos_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'chaos',
            fmt = '%i',
        },
    },
    chaos_damage_max = {
        default = 0,
        property = 'Has maximum chaos damage',
        stats_add = {
            'local_maximum_added_chaos_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'chaos',
            fmt = '%i',
        },
    },
    critical_strike_chance = {
        property = 'Has critical strike chance',
        stats_add = {
            'local_critical_strike_chance',
        },
        stats_increased = {
            'local_critical_strike_chance_+%',
        },
        stats_override = {
            ['local_weapon_always_crit'] = {min=100, max=100},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%.2f%%',
        },
    },
    attack_speed = {
        property = 'Has attack speed',
        stats_increased = {
            'local_attack_speed_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%.2f',
        },
    },
    flask_life = {
        property = 'Has flask life recovery',
        stats_add = {
            'local_flask_life_to_recover',
        },
        stats_increased = {
            'local_flask_life_to_recover_+%',
            'local_flask_amount_to_recover_+%',
        },
        html_fmt_options = {
            fmt = '%i',
        },
    },
    flask_mana = {
        property = 'Has flask mana recovery',
        stats_add = {
            'local_flask_mana_to_recover',
        },
        stats_increased = {
            'local_flask_mana_to_recover_+%',
            'local_flask_amount_to_recover_+%',
        },
    },
    flask_duration = {
        property = 'Has flask duration',
        stats_increased = {
            'local_flask_duration_+%',
        },
        stats_increased_inverse = {
            'local_flask_recovery_speed_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%.2f',
        },
    },
    charges_per_use = {
        property = 'Has flask charges per use',
        stats_increased = {
            'local_charges_used_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    charges_max = {
        property = 'Has maximum flask charges',
        stats_add = {
            'local_extra_max_charges',
        },
        stats_increased = {
            'local_max_charges_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    block = {
        property = 'Has block',
        stats_add = {
            'local_additional_block_chance_%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i%%',
        },
    },
    armour = {
        property = 'Has armour',
        stats_add = {
            'local_base_physical_damage_reduction_rating',
        },
        stats_increased = {
            'local_physical_damage_reduction_rating_+%',
            'local_armour_and_energy_shield_+%',
            'local_armour_and_evasion_+%',
            'local_armour_and_evasion_and_energy_shield_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    evasion = {
        property = 'Has evasion',
        stats_add = {
            'local_base_evasion_rating',
        },
        stats_increased = {
            'local_evasion_rating_+%',
            'local_evasion_and_energy_shield_+%',
            'local_armour_and_evasion_+%',
            'local_armour_and_evasion_and_energy_shield_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    energy_shield = {
        property = 'Has energy shield',
        stats_add = {
            'local_energy_shield'
        },
        stats_increased = {
            'local_energy_shield_+%',
            'local_armour_and_energy_shield_+%',
            'local_evasion_and_energy_shield_+%',
            'local_armour_and_evasion_and_energy_shield_+%',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    required_dexterity = {
        property = 'Has dexterity requirement',
        stats_add = {
            'local_dexterity_requirement_+'
        },
        stats_increased = {
            'local_dexterity_requirement_+%',
            'local_attribute_requirements_+%',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    required_intelligence = {
        property = 'Has intelligence requirement',
        stats_add = {
            'local_intelligence_requirement_+'
        },
        stats_increased = {
            'local_intelligence_requirement_+%',
            'local_attribute_requirements_+%',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    required_strength = {
        property = 'Has strength requirement',
        stats_add = {
            'local_strength_requirement_+'
        },
        stats_increased = {
            'local_strength_requirement_+%',
            'local_attribute_requirements_+%',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    map_area_level = {
        property = 'Has map area level',
        stats_override = {
            ['map_item_level_override'] = true,
        },
    },
}

core.dps_map = {
    {
        name = 'physical_dps',
        property = 'physical damage per second',
        damage_args = {'physical_damage', },
        label = i18n.item_table.physical_dps,
        html_fmt_options = {
            color = 'value',
            fmt = '%.1f',
        },
    },
    {
        name = 'fire_dps',
        property = 'fire damage per second',
        damage_args = {'fire_damage'},
        label = i18n.item_table.fire_dps,
        html_fmt_options = {
            color = 'fire',
            fmt = '%.1f',
        },
    },
    {
        name = 'cold_dps',
        property = 'cold damage per second',
        damage_args = {'cold_damage'},
        label = i18n.item_table.cold_dps,
        html_fmt_options = {
            color = 'cold',
            fmt = '%.1f',
        },
    },
    {
        name = 'lightning_dps',
        property = 'lightning damage per second',
        damage_args = {'lightning_damage'},
        label = i18n.item_table.lightning_dps,
        html_fmt_options = {
            color = 'lightning',
            fmt = '%.1f',
        },
    },
    {
        name = 'chaos_dps',
        property = 'chaos damage per second',
        damage_args = {'chaos_damage'},
        label = i18n.item_table.chaos_dps,
        html_fmt_options = {
            color = 'chaos',
            fmt = '%.1f',
        },
    },
    {
        name = 'elemental_dps',
        property = 'elemental damage per second',
        damage_args = {'fire_damage', 'cold_damage', 'lightning_damage'},
        label = i18n.item_table.elemental_dps,
        html_fmt_options = {
            color = 'value',
            fmt = '%.1f',
        },
    },
    {
        name = 'poison_dps',
        property = 'poison damage per second',
        damage_args = {'physical_damage', 'chaos_damage'},
        label = i18n.item_table.poison_dps,
        html_fmt_options = {
            color = 'value',
            fmt = '%.1f',
        },
    },
    {
        name = 'dps',
        property = 'damage per second',
        damage_args = {'physical_damage', 'fire_damage', 'cold_damage', 'lightning_damage', 'chaos_damage'},
        label = i18n.item_table.dps,
        html_fmt_options = {
            color = 'value',
            fmt = '%.1f',
        },
    },
}

-- TODO: Second pass for i18n item classes
-- base item is default, but will be validated later
-- Notes:
--  inventory_icon must always be before alternate_art_inventory_icons 
--  is_relic after rarity
core.default_args = {'class', 'rarity', 'name', 'name_list', 'name_list_lower', 'size_x', 'size_y', 'drop_enabled', 'drop_level', 'drop_level_maximum', 'drop_leagues', 'required_level', 'required_level_final', 'inventory_icon', 'alternate_art_inventory_icons', 'flavour_text', 'help_text', 'tags', 'metadata_id', 'is_corrupted', 'is_relic', 'mods', 'implicit_mods', 'explicit_mods',}
-- frame_type is needed in stat_text
core.late_args = {'frame_type', 'implicit_stat_text', 'explicit_stat_text', 'stat_text'}
core.prophecy_args = {'prophecy_id', 'prediction_text', 'seal_cost_normal', 'seal_cost_cruel', 'seal_cost_merciless'}
core.class_groups = {
    flasks = {
        keys = {['Life Flasks'] = true, ['Mana Flasks'] = true, ['Hybrid Flasks'] = true, ['Utility Flasks'] = true, ['Critical Utility Flasks'] = true},
        args = {'flask_duration', 'charges_max', 'charges_per_use'},
    },
    weapons = {
        keys = {['Claws'] = true, ['Daggers'] = true, ['Wands'] = true, ['One Hand Swords'] = true, ['Thrusting One Hand Swords'] = true, ['One Hand Axes'] = true, ['One Hand Maces'] = true, ['Bows'] = true, ['Staves'] = true, ['Two Hand Swords'] = true, ['Two Hand Axes'] = true, ['Two Hand Maces'] = true, ['Sceptres'] = true, ['Fishing Rods'] = true},
        args = {'required_dexterity', 'required_intelligence', 'required_strength', 'critical_strike_chance', 'attack_speed', 'physical_damage_min', 'physical_damage_max', 'range'},
        late_args = {'physical_damage_html', 'fire_damage_html', 'cold_damage_html', 'lightning_damage_html', 'chaos_damage_html', 'damage_avg', 'damage_html'},
    },
    gems = {
        keys = {['Active Skill Gems'] = true, ['Support Skill Gems'] = true},
        args = {'dexterity_percent', 'strength_percent', 'intelligence_percent', 'primary_attribute', 'gem_tags', 'gem_tags_difference'},
    },
    armor = {
        keys = {['Gloves'] = true, ['Boots'] = true, ['Body Armours'] = true, ['Helmets'] = true, ['Shields'] = true},
        args = {'required_dexterity', 'required_intelligence', 'required_strength', 'armour', 'energy_shield', 'evasion'},
    },
    stackable = {
        keys = {['Currency'] = true, ['Stackable Currency'] = true, ['Hideout Doodads'] = true, ['Microtransactions'] = true, ['Divination Card'] = true},
        args = {'stack_size', 'stack_size_currency_tab', 'description', 'cosmetic_type'},
    },
}

core.class_specifics = {
    ['Amulets'] = {
        args = {'is_talisman', 'talisman_tier'},
    },
    ['Life Flasks'] = {
        args = {'flask_life'},
    },
    ['Mana Flasks'] = {
        args = {'flask_mana'},
    },
    ['Hybrid Flasks'] = {
        args = {'flask_life', 'flask_mana'},
    },
    ['Utility Flasks'] = {
        args = {'buff_id', 'buff_values', 'buff_stat_text', 'buff_icon'},
    },
    ['Critical Utility Flasks'] = {
        args = {'buff_id', 'buff_values', 'buff_stat_text', 'buff_icon'},
    },
    ['Active Skill Gems'] = {
        args = {'skill_screenshot', 'gem_icon'},
        defaults = {
            help_text = i18n.help_text_defaults.active_gem,
            size_x = 1,
            size_y = 1,
        },
        frame_type = 'gem',
    },
    ['Support Skill Gems'] = {
        args = {'support_gem_letter_html'},
        defaults = {
            help_text = i18n.help_text_defaults.support_gem,
            size_x = 1,
            size_y = 1,
        },
        frame_type = 'gem',
    },
    ['Shields'] = {
        args = {'block'},
    },
    ['Maps'] = {
        args = {'map_tier', 'map_guild_character', 'map_area_id', 'map_area_level', 'unique_map_area_id', 'unique_map_area_level', 'unique_map_guild_character'},
        skip_stat_lines = i18n.stat_skip_patterns.maps,
    },
    ['Currency'] = {
        frame_type = 'currency',
    },
    ['Stackable Currency'] = {
        args = {'is_essence', 'essence_level_restriction', 'essence_tier'},
        frame_type = 'currency',
    },
    ['Microtransactions'] = {
        frame_type = 'currency',
    },
    ['Hideout Doodads'] = {
        args = {'is_master_doodad', 'master', 'master_level_requirement', 'master_favour_cost', 'variation_count'},
        defaults = {
            help_text = i18n.help_text_defaults.hideout_doodad,
        },
        frame_type = 'currency',
    },
    ['Jewel'] = {
        late_args = {'item_limit', 'jewel_radius', 'jewel_radius_html'},
        defaults = {
            help_text = i18n.help_text_defaults.jewel,
        },
        skip_stat_lines = i18n.stat_skip_patterns.jewels,
    },
    ['Quest Items'] = {
        frame_type = 'quest',
    },
    ['Divination Card'] = {
        args = {'card_art', 'card_reward'},
        frame_type = 'divicard',
    },
    ['Labyrinth Item'] = {
        frame_type = 'currency',
    },
    ['Labyrinth Trinket'] = {
        args = {'description', 'buff_icon'},
        frame_type = 'currency',
    },
}

-- add defaults from class specifics and class groups
core.item_classes = {}
function core.build_item_classes()
    for _, data in ipairs(m_game.constants.item.class) do
        core.item_classes[data['full']] = {
            args = xtable:new(),
            late_args = xtable:new(),
            defaults = {},
        }
    end

    for _, row in pairs(core.class_groups) do
        for k, _ in pairs(row.keys) do
            core.item_classes[k].args:insertT(row.args)
            if row.late_args ~= nil then
                core.item_classes[k].late_args:insertT(row.late_args)
            end
        end
    end


    for k, row in pairs(core.class_specifics) do
        if row.args ~= nil then
            core.item_classes[k].args:insertT(row.args)
        end
        if row.late_args ~= nil then
            core.item_classes[k].late_args:insertT(row.late_args)
        end
        if row.defaults ~= nil then
            for key, value in pairs(row.defaults) do
                core.item_classes[k].defaults[key] = value
            end
       end
    end
end

-- GroupTable -> RowTable -> formatter function 
--
--

core.display_groups = {
    -- Tags, stats, level, etc
    {
        {
            args = {'cosmetic_type'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'cosmetic_type',
                        fmt = '%s',
                        color = 'default'
                    },
                },
            },
        },
        {
            args = function(tpl_args, frame)
                if tpl_args.class == nil then 
                    return false 
                end
                
                return core.class_groups.weapons.keys[tpl_args.class] ~= nil
            end,
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'class',
                        color = 'default',
                    },
                },
            },
        },
        {
            args = {'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,
        },
        {
            args = {'support_gem_letter_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'support_gem_letter_html',
                        inline = i18n.tooltips.support_icon,
                    },
                },
            },
        },
        {
            args = {'radius'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'radius',
                        inline = i18n.tooltips.radius,
                        func = core.factory.descriptor_value{key='radius_description'},
                    },
                    [2] = {
                        key = 'radius_secondary',
                        inline = ' / %s',
                        func = core.factory.descriptor_value{key='radius_secondary_description'},
                    },
                    [3] = {
                        key = 'radius_tertiary',
                        inline = ' / %s',
                        func = core.factory.descriptor_value{key='radius_tertiary_description'},
                    },
                },
            },
        },
        -- TODO: gem level here. Maybe put max level here?
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                options = {
                    [1] = {
                        key = 'mana_cost',
                        hide_default = 100,
                        fmt = function (tpl_args, frame)
                            if tpl_args.has_percentage_mana_cost then
                                return '%i%%'
                            else
                                return '%i'
                            end
                        end,
                        inline = function (tpl_args, frame)
                            if tpl_args.has_reservation_mana_cost then
                                return i18n.tooltips.mana_reserved
                            else
                                return i18n.tooltips.mana_cost
                            end
                        end,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                options = {
                    [1] = {
                        key = 'mana_multiplier',
                        hide_default = 100,
                        fmt = '%i%%',
                        inline = i18n.tooltips.mana_multiplier,
                    },
                },
            },
        },
        -- TODO: i18n
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                options = {
                    [1] = {
                        key = 'vaal_souls_requirement',
                        hide_default = 0,
                        fmt = '%i (N) / ',
                        inline = i18n.tooltips.vaal_souls_per_use,
                    },
                    [2] = {
                        key = 'vaal_souls_requirement',
                        hide_default = 0,
                        fmt = '%i (C) / ',
                        func = function (tpl_args, frame, value)
                            return value*1.5
                        end,
                    },
                    [3] = {
                        key = 'vaal_souls_requirement',
                        hide_default = 0,
                        fmt = '%i (M)',
                        func = function (tpl_args, frame, value)
                            return value*2
                        end,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                options = {
                    [1] = {
                        key = 'vaal_stored_uses',
                        hide_default = 0,
                        fmt = '%i',
                        inline = i18n.tooltips.stored_uses,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                options = {
                    [1] = {
                        key = 'stored_uses',
                        hide_default = 0,
                        fmt = '%i',
                        inline = i18n.tooltips.stored_uses,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                options = {
                    [1] = {
                        key = 'cooldown', 
                        hide_default = 0,
                        fmt = '%.2f sec',
                        inline = i18n.tooltips.cooldown_time,
                    },
                },
            },
        },
        {
            args = {'cast_time'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'cast_time',
                        hide_default = 0,
                        fmt = '%.2f sec',
                        inline = i18n.tooltips.cast_time,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                options = {
                    [1] = {
                        key = 'critical_strike_chance',
                        hide_default = 0,
                        fmt = '%.2f%%',
                        inline = i18n.tooltips.critical_strike_chance,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                options = {
                    [1] = {
                        key = 'damage_effectiveness',
                        hide_default = 100,
                        fmt = '%i%%',
                        inline = i18n.tooltips.damage_effectiveness,
                    },
                },
            },
        },
        {
            args = {'projectile_speed'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'projectile_speed',
                        inline = i18n.tooltips.projectile_speed,
                    },
                },
            },
        },
        -- Weapon only
        {
            args = {'physical_damage_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'physical_damage_html',
                        fmt = '%s',
                        inline = i18n.tooltips.physical_damage,
                    },
                },
            },       
        },
        {
            args = nil,
            func = function(tpl_args, frame)
                local text = ''
                for _, dtype in ipairs({'fire_damage_html', 'cold_damage_html', 'lightning_damage_html'}) do
                    local value = tpl_args[dtype]
                    if value ~= nil then
                        text = text .. ' ' .. value
                    end
                end
                
                if text ~= '' then
                    return string.format(i18n.tooltips.elemental_damage, text)
                else
                    return
                end
            end,      
        },
        {
            args = {'chaos_damage_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'chaos_damage_html',
                        fmt = '%s',
                        inline = i18n.tooltips.chaos_damage,
                    },
                },
            },       
        },
        {
            args = {'critical_strike_chance_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'critical_strike_chance_html',
                        fmt = '%s',
                        inline = i18n.tooltips.critical_strike_chance,
                    },
                },
            },
        },
        {
            args = {'attack_speed_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'attack_speed_html',
                        fmt = '%s',
                        inline = i18n.tooltips.attacks_per_second,
                    },
                },
            },
        },
        {
            args = {'range_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'range_html',
                        fmt = '%s',
                        inline = i18n.tooltips.weapon_range,
                    },
                },
            },
        },
        -- Map only
        {
            args = {'map_area_level'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'map_area_level',
                        fmt = '%i',
                        inline = i18n.tooltips.map_level,
                    },
                },
            },
        },
        {
            args = {'map_tier'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'map_tier',
                        fmt = '%i',
                        inline = i18n.tooltips.map_tier,
                    },
                },
            },
        },
        {
            args = function(tpl_args, frame)
                return tpl_args.map_guild_character ~= nil and tpl_args.rarity == 'Normal'
            end,
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'map_guild_character',
                        fmt = '%s',
                        inline = i18n.tooltips.map_guild_character,
                    },
                },
            },
        },
        {
            args = function(tpl_args, frame)
                return tpl_args.unique_map_guild_character ~= nil and tpl_args.rarity == 'Unique'
            end,
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'unique_map_guild_character',
                        fmt = '%s',
                        inline = i18n.tooltips.map_guild_character,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type = 'stat',
                options = {
                    [1] = {
                        key = 'map_item_drop_quantity_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                        inline = i18n.tooltips.item_quantity,
                        hide_default = 0,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type = 'stat',
                options = {
                    [1] = {
                        key = 'map_item_drop_rarity_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                        inline = i18n.tooltips.item_rarity,
                        hide_default = 0,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type = 'stat',
                options = {
                    [1] = {
                        key = 'map_pack_size_+%',
                        fmt = '+%i%%',
                        color = 'mod',
                        inline = i18n.tooltips.monster_pack_size,
                        hide_default = 0,
                    },
                },
            },
        },
        -- Jewel Only
        {
            args = nil,
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'item_limit',
                        fmt = '%i',
                        inline = i18n.tooltips.limited_to,
                    },
                },
            },
        },
        {
            args = {'jewel_radius_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'jewel_radius_html',
                        fmt = '%s',
                        inline = i18n.tooltips.radius,
                    },
                },
            },
        },
        -- Flask only
        {
            args = {'flask_mana_html', 'flask_duration_html'},
            --func = core.factory.display_flask('flask_mana'),
            func = core.factory.display_value{
                inline = i18n.tooltips.flask_mana_recovery,
                options = {
                    [1] = {
                        key = 'flask_mana_html',
                        fmt = '%s',
                    },
                    [2] = {
                        key = 'flask_duration_html',
                        fmt = '%s',
                    },
                }
            },
        },
        {
            args = {'flask_life_html', 'flask_duration_html'},
            func = core.factory.display_value{
                inline = i18n.tooltips.flask_life_recovery,
                options = {
                    [1] = {
                        key = 'flask_life_html',
                        fmt = '%s',
                    },
                    [2] = {
                        key = 'flask_duration_html',
                        fmt = '%s',
                    },
                }
            },
        },
        {
            -- don't display for mana/life flasks
            args = 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.display_value{
                inline = i18n.tooltips.flask_duration,
                options = {
                    [1] = {
                        key = 'flask_duration_html',
                        fmt = '%s',
                    },
                },
            },
        },
        {
            args = {'charges_per_use_html', 'charges_max_html'},
            func = core.factory.display_value{
                inline = i18n.tooltips.flask_charges_per_use,
                options = {
                    [1] = {
                        key = 'charges_per_use_html',
                        fmt = '%s',
                    },
                    [2] = {
                        key = 'charges_max_html',
                        fmt = '%s',
                    },
                },
            },
        },
        {
            args = {'buff_stat_text'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'buff_stat_text',
                        color = 'mod',
                    },
                },
            },
        },
        -- armor
        {
            args = {'block_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'block_html',
                        inline = i18n.tooltips.chance_to_block,
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'block',
                    },
                },
            },
        },
        {
            args = {'armour_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'armour_html',
                        inline = i18n.tooltips.armour,
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'armour',
                    },
                },
            },
        },
        {
            args = {'evasion_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'evasion_html',
                        inline = i18n.tooltips.evasion,
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'evasion',
                    },
                },
            },
        },
        {
            args = {'energy_shield_html'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'energy_shield_html',
                        inline = i18n.tooltips.energy_shield,
                        fmt = '%s',
                        hide_default = 0,
                        hide_default_key = 'energy_shield',
                    },
                },
            },
        },
        -- Amulet only
        {
            args = {'talisman_tier'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'talisman_tier',
                        fmt = '%i',
                        inline = i18n.tooltips.talisman_tier,
                    },
                },
            },
        },
        -- Misc
        {
            args = {'stack_size'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'stack_size',
                        hide_default = 1,
                        fmt = '%i',
                        inline = i18n.tooltips.stack_size,
                    },
                },
            },
        },
        -- Essence stuff
        {
            args = {'essence_tier'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'essence_tier',
                        fmt = '%i',
                        inline = i18n.tooltips.essence_tier,
                    },
                },
            },
        },
    },
    -- Requirements
    {
        -- TODO: i18n Master name?
        {
            args = {'master', 'master_level_requirement'},
            func = function(tpl_args, frame)
                -- masters have been validated before
                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 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
        {
            args = nil,
            func = function(tpl_args, frame)
                local opt = {
                    [1] = {
                        key = 'required_level_final',
                        hide_default = 1,
                        inline = i18n.tooltips.level_inline,
                        inline_color = false,
                    },
                }
                
                for _, attr in ipairs(m_game.constants.attributes) do
                    opt[#opt+1] = {
                        key = string.format('required_%s_html', attr['long_lower']),
                        hide_default = 0,
                        hide_default_key = string.format('required_%s', attr['long_lower']),
                        inline = ', %s ' .. attr['short_upper'],
                        inline_color = false,
                    }
                end
                
                local requirements = core.factory.display_value{options = opt}(tpl_args, frame)
                
                -- return early
                if requirements == nil then
                    return
                end
                
                requirements = string.gsub(requirements, '^, ', '')
                
                return util.html.poe_color('default', string.format(i18n.tooltips.requires, requirements))
            end,
        },
    },
    -- Drop info
    {
        header = function ()
            return string.format('<b>%s</b><br>', i18n.tooltips.drop_restrictions)
        end,
        {
            args = {'drop_enabled'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'drop_level',
                        fmt = '%i',
                        inline = i18n.tooltips.level,
                    },
                    [2] = {
                        key = 'drop_level_maximum',
                        hide_default = 100,
                        fmt = '%i',
                        inline = ' / %s',
                    },
                },
            },
        },
        {
            args = {'drop_leagues'},
            func = function(tpl_args, frame)
                return string.format(i18n.tooltips.league_restriction, util.html.poe_color('value', table.concat(tpl_args.drop_leagues, ', ')))
            end
        },
        {
            args = function(tpl_args, frame)
                if tpl_args.drop_enabled == true then
                    return false
                end
                return true
            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,
        },
    },
    -- Gem description
    {
        css_class = '-textwrap tc -gemdesc',
        {
            args = {'gem_description'},
            func = core.factory.display_value_only('gem_description'),
        },
    },
    -- Gem Quality Stats
    {
        css_class = '-textwrap tc -mod',
        {
            args = {'quality_stat_text'},
            func = function(tpl_args, frame)
                lines = {}
                lines[#lines+1] = util.html.poe_color('default', i18n.tooltips.gem_quality)
                lines[#lines+1] = tpl_args.quality_stat_text
                
                return table.concat(lines, '<br>')
            end,
        },
    },
    -- Gem Implicit Stats
    {
        css_class = '-textwrap tc -mod',
        {
            args = function(tpl_args, frame)
                return core.class_groups.gems.keys[tpl_args.class] and tpl_args.stat_text
            end,
            func = function(tpl_args, frame)
                lines = {}
                lines[#lines+1] = tpl_args.stat_text
                if tpl_args.gem_tags:contains('Vaal') then
                    lines[#lines+1] = util.html.poe_color('corrupted', i18n.tooltips.corrupted) 
                end
                return table.concat(lines, '<br>')
            end,
        },
    },
    -- Implicit Stats
    {
        css_class = '-textwrap tc -mod',
        func = function(tpl_args, frame)
            if tpl_args.implicit_stat_text ~= '' then
                return {tpl_args.implicit_stat_text}
            else
                return {}
            end
        end,
    },
    -- Stats
    {
        css_class = '-textwrap tc -mod',
        func = function(tpl_args, frame)
            if tpl_args.explicit_stat_text ~= '' then
                return {tpl_args.explicit_stat_text}
            else
                return {}
            end
        end,
    },
    -- Experience
    {
        {
            args = {'experience'},
            func = core.factory.display_value{
                key = 'experience',
                options = {
                    [1] = {
                        fmt = '%i',
                    },
                },
            },
        },
    },
    -- Description (currency, doodads)
    {
        css_class = '-textwrap tc -mod',
        {
            args = {'description'},
            func = core.factory.display_value_only('description'),
        },
    },
    -- Variations (for doodads)
    {
       css_class = 'tc -mod',
        {
            args = {'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
    {
        css_class = '-textwrap tc -flavour',
        {
            args = {'flavour_text'},
            func = core.factory.display_value_only('flavour_text'),
        },
    },
    -- Prophecy text
    {
        css_class = '-textwrap tc -value',
        {
            args = {'prediction_text'},
            func = core.factory.display_value_only('prediction_text'),
        },
    },
    -- Help text
    {
        css_class = '-textwrap tc -help',
        {
            args = {'help_text'},
            func = core.factory.display_value_only('help_text'),
        },
    },
    -- Cost (i.e. vendor costs)
    {
        --css_class = '',
        {
            args = {'master_favour_cost'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'master_favour_cost',
                        inline = i18n.tooltips.favour_cost,
                        color = 'currency',
                    },
                },
            },
        },
        {
            args = {'seal_cost_normal', 'seal_cost_cruel', 'seal_cost_merciless'},
            func = core.factory.display_value{
                options = {
                    [1] = {
                        key = 'seal_cost_normal',
                        inline = i18n.tooltips.seal_cost,
                        fmt = '%dx/',
                        color = 'currency',
                    },
                    [2] = {
                        key = 'seal_cost_cruel',
                        fmt = '%dx/',
                        color = 'currency',
                    },
                    [3] = {
                        key = 'seal_cost_merciless',
                        fmt = '%dx',
                        color = 'currency',
                        inline = function (tpl_args, frame) 
                            return '%s' .. p.item_link{item_name_exact='Silver Coin'}
                        end,
                    },
                },
            },
        },
    },
}

core.item_link_params = {'name', 'inventory_icon', 'html'}

core.result = {}

-- for sort type see:
-- https://meta.wikimedia.org/wiki/Help:Sorting
core.result.generic_item = {
    {
        arg = 'base_item',
        header = i18n.item_table.base_item,
        properties = {'Has base item', 'Has base item wiki page'},
        display = function(tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has base item'])
                    :wikitext(string.format('[[%s|%s]]', data['Has base item'], data['Has base item wiki page']))
        end,
        order = 1000,
        sort_type = 'text',
    },
    {
        arg = 'class',
        header = i18n.item_table.item_class,
        properties = {'Has item class'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='[[%s]]',
            },
        }},
        order = 1001,
        sort_type = 'text',
    },
    {
        arg = 'essence',
        header = i18n.item_table.essence_tier,
        properties = {'Has essence tier'},
        display = h.tbl.display.factory.value{},
        order = 2000,
    },
    {
        arg = 'drop_level',
        header = i18n.item_table.drop_level,
        properties = {'Has drop level'},
        display = h.tbl.display.factory.value{},
        order = 3000,
    },
    {
        arg = 'stack_size',
        header = i18n.item_table.stack_size,
        properties = {'Has stack size'},
        display = h.tbl.display.factory.value{},
        order = 4000,
    },
    {
        arg = 'stack_size_currency_tab',
        header = i18n.item_table.stack_size_currency_tab,
        properties = {'Has currency tab stack size'},
        display = h.tbl.display.factory.value{},
        order = 4001,
    },
    {
        arg = 'level',
        header = m_game.level_requirement.icon,
        properties = {'Has level requirement'},
        display = h.tbl.display.factory.value{},
        order = 5000,
    },
    {
        arg = 'ar',
        header = i18n.item_table.armour,
        properties = h.tbl.range_properties('armour'),
        display = h.tbl.display.factory.range{property='armour'},
        order = 6000,
    },
    {
        arg = 'ev',
        header =i18n.item_table.evasion,
        properties = h.tbl.range_properties('evasion'),
        display = h.tbl.display.factory.range{property='evasion'},
        order = 6001,
    },
    {
        arg = 'es',
        header = i18n.item_table.energy_shield,
        properties = h.tbl.range_properties('energy shield'),
        display = h.tbl.display.factory.range{property='energy shield'},
        order = 6002,
    },
    {
        arg = 'block',
        header = i18n.item_table.block,
        properties = h.tbl.range_properties('block'),
        display = h.tbl.display.factory.range{property='block'},
        order = 6003,
    },
    --[[{
        arg = 'physical_damage_min',
        header = util.html.abbr('Min', 'Local minimum weapon damage'),
        properties = h.tbl.range_properties('minimum physical damage'),
        display = h.tbl.display.factory.range{property='minimum physical damage'},
        order = 7000,
    },
    {
        arg = 'physical_damage_max',
        header = util.html.abbr('Max', 'Local maximum weapon damage'),
        properties = h.tbl.range_properties('maximum physical damage'),
        display = h.tbl.display.factory.range{property='maximum physical damage'},
        order = 7001,
        
    },]]--
    {
        arg = {'weapon', 'damage'},
        header = i18n.item_table.damage,
        properties = {'Has damage HTML', 'Has average damage'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has average damage'])
                    :wikitext(data['Has damage HTML'])
        end,
        order = 8000,
    },
    {
        arg = {'weapon', 'aps'},
        header = i18n.item_table.attacks_per_second,
        properties = h.tbl.range_properties('attack speed'),
        display = h.tbl.display.factory.range{property='attack speed'},
        order = 8001,
    },
    {
        arg = {'weapon', 'crit'},
        header = i18n.item_table.local_critical_strike_chance,
        properties = h.tbl.range_properties('critical strike chance'),
        display = h.tbl.display.factory.range{property='critical strike chance'},
        order = 8002,
    },
    {
        arg = 'flask_life',
        header = i18n.item_table.flask_life,
        properties = h.tbl.range_properties('flask life recovery'),
        display = h.tbl.display.factory.range{property='flask life recovery'},
        order = 9000,
    },
    {
        arg = 'flask_mana',
        header = i18n.item_table.flask_mana,
        properties = h.tbl.range_properties('flask mana recovery'),
        display = h.tbl.display.factory.range{property='flask mana recovery'},
        order = 9001,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_duration,
        properties = h.tbl.range_properties('flask duration'),
        display = h.tbl.display.factory.range{property='flask duration'},
        order = 9002,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_charges_per_use,
        properties = h.tbl.range_properties('flask charges per use'),
        display = h.tbl.display.factory.range{property='flask charges per use'},
        order = 9003,
    },
    {
        arg = 'flask',
        header = i18n.item_table.flask_maximum_charges,
        properties = h.tbl.range_properties('maximum flask charges'),
        display = h.tbl.display.factory.range{property='maximum flask charges'},
        order = 9004,
    },
    {
        arg = 'item_limit',
        header = i18n.item_table.item_limit,
        properties = {'Has item limit'},
        display = h.tbl.display.factory.value{},
        order = 10000,
    },
    {
        arg = 'jewel_radius',
        header = i18n.item_table.jewel_radius,
        properties = {'Has jewel radius', 'Has jewel radius HTML'},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has jewel radius'])
                    :wikitext(data['Has jewel radius HTML'])
        end,
        order = 10001,
    },
    {
        arg = 'map_tier',
        header = i18n.item_table.map_tier,
        properties = {'Has map tier'},
        display = h.tbl.display.factory.value{},
        order = 11000,
    },
    {
        arg = 'map_level',
        header = i18n.item_table.map_level,
        properties = {'Has map area level'},
        display = h.tbl.display.factory.value{},
        order = 11010,
    },
    {
        arg = 'map_guild_character',
        header = i18n.item_table.map_guild_character,
        properties = {'Has map guild character'},
        display = h.tbl.display.factory.value{colour='value'},
        order = 11020,
        sort_type = 'text',
    },
    {
        arg = 'buff',
        header = i18n.item_table.buff_effects,
        properties = {'Has buff stat text'},
        display = h.tbl.display.factory.value{colour='mod'},
        order = 12000,
        sort_type = 'text',
    },
    {
        arg = 'stat',
        header = i18n.item_table.stats,
        properties = {'Has stat text'},
        display = h.tbl.display.factory.value{colour='mod'},
        order = 12001,
        sort_type = 'text',
    },
    {
        arg = 'description',
        header = i18n.item_table.effects,
        properties = {'Has description'},
        display = h.tbl.display.factory.value{colour='mod'},
        order = 12002,
        sort_type = 'text',
    },
    {
        arg = 'flavour_text',
        header = i18n.item_table.flavour_text,
        properties = {'Has flavour text'},
        display = h.tbl.display.factory.value{colour='flavour'},
        order = 12003,
        sort_type = 'text',
    },
    {
        arg = 'help_text',
        header = i18n.item_table.help_text,
        properties = {'Has help text'},
        display = h.tbl.display.factory.value{colour='help'},
        order = 12004,
        sort_type = 'text',
    },
    {
        arg = 'buff_icon',
        header = i18n.item_table.buff_icon,
        properties = {'Has buff icon'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='[[%s]]',
            },
        }},
        order = 13000,
        sort_type = 'text',
    },
}

for i, data in ipairs(core.dps_map) do
    table.insert(core.result.generic_item, {
        arg = data.name,
        header = data.label,
        properties = h.tbl.range_properties(data.property),
        display = h.tbl.display.factory.range{property=data.property},
        order = 8100+i, 
    })
end

core.result.skill_gem_new = {
    {
        arg = 'icon',
        header = i18n.item_table.support_gem_letter,
        properties = {'Has support gem letter HTML'},
        display = h.tbl.display.factory.value{},
        order = 1000,
        sort_type = 'text',
    },
    {
        arg = 'skill_icon',
        header = i18n.item_table.skill_icon,
        properties = {'Has skill gem icon'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='[[%s]]',
            },
        }},
        order = 1001,
        sort_type = 'text',
    },
    {
        arg = 'description',
        header = i18n.item_table.description,
        properties = {'Has description'},
        display = h.tbl.display.factory.value{},
        order = 2000,
        sort_type = 'text',
    },
    {
        arg = 'level',
        header = m_game.level_requirement.icon,
        properties = {'Has level requirement'},
        display = h.tbl.display.factory.value{},
        order = 3004,
    },
    {
        arg = 'crit',
        header = i18n.item_table.skill_critical_strike_chance,
        properties = {'Has critical strike chance'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 4000,
    },
    {
        arg = 'cast_time',
        header = i18n.item_table.cast_time,
        properties = {'Has cast time'},
        display = h.tbl.display.factory.value{},
        order = 4001,
    },
    {
        arg = 'dmgeff',
        header = i18n.item_table.damage_effectiveness,
        properties = {'Has damage effectiveness'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 4002,
    },
    {
        arg = 'mcm',
        header = i18n.item_table.mana_cost_multiplier,
        properties = {'Has mana multiplier'},
        display = h.tbl.display.factory.value{options = {
            [1] = {
                fmt='%s%%',
            },
        }},
        order = 5000,
    },
    {
        arg = 'mana',
        header = i18n.item_table.mana_cost,
        properties = {'Has mana cost', 'Has percentage mana cost', 'Has reservation mana cost'},
        display = function (tr, data)
            local appendix = ''
            if util.cast.boolean(data['Has percentage mana cost']) then
                appendix = appendix .. '%'
            end
            if util.cast.boolean(data['Has reservation mana cost']) then
                appendix = appendix .. ' ' .. i18n.item_table.reserves_mana_suffix
            end
            
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has mana cost'])
                    :wikitext(string.format('%d', data['Has mana cost']) .. appendix)
        end,
        order = 5001,
    },
    {
        arg = 'vaal',
        header = i18n.item_table.vaal_souls_requirement,
        properties = {'Has vaal souls requirement'},
        display = function (tr, data)
            local souls = tonumber(data['Has vaal souls requirement'])
            tr
                :tag('td')
                    :attr('data-sort-value', souls)
                    :wikitext(string.format('%d / %d / %d', souls, souls*1.5, souls*2))
        end,
        order = 6000,
    },
    {
        arg = 'vaal',
        header = i18n.item_table.stored_uses,
        properties = {'Has vaal stored uses'},
        display = h.tbl.display.factory.value{},
        order = 6001,
    },
    {
        arg = 'radius',
        header = i18n.item_table.primary_radius,
        properties = {'Has primary radius', 'Has primary radius description'},
        options = {[2] = {optional = true}},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has primary radius'])
                    :wikitext(core.factory.descriptor_value{tbl=data, key='Has primary radius description'}(nil, nil, data['Has primary radius']))
        end,
        order = 7000,
    },
    {
        arg = 'radius',
        header = i18n.item_table.secondary_radius,
        properties = {'Has secondary radius', 'Has secondary radius description'},
        options = {[2] = {optional = true}},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has secondary radius'])
                    :wikitext(core.factory.descriptor_value{tbl=data, key='Has secondary radius description'}(nil, nil, data['Has secondary radius']))
        end,
        order = 7001,
    },
    {
        arg = 'radius',
        header = i18n.item_table.tertiary_radius,
        properties = {'Has tertiary radius', 'Has tertiary radius description'},
        options = {[2] = {optional = true}},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data['Has tertiary radius'])
                   :wikitext(core.factory.descriptor_value{tbl=data, key='Has tertiary radius description'}(nil, nil, data['Has tertiary radius']))
        end,
        order = 7002,
    },
}

for i, attr in ipairs(m_game.constants.attributes) do
    table.insert(core.result.generic_item, 7, {
        arg = attr.short_lower,
        header = attr.icon,
        properties = h.tbl.range_properties(string.format('%s requirement', attr.long_lower)),
        display = h.tbl.display.factory.range{property=string.format('%s requirement', attr.long_lower)},
        order = 5000+i,
    })
    table.insert(core.result.skill_gem_new, 1, {
        arg = attr.short_lower,
        header = attr.icon,
        properties = {string.format('Has %s percentage', attr.long_lower)},
        display = function (tr, data)
            tr
                :tag('td')
                    :attr('data-sort-value', data[string.format('Has %s percentage', attr.long_lower)])
                    :wikitext('[[File:Yes.png|yes|link=]]')
        end,
        order = 3000+i,
    })
end

-- ----------------------------------------------------------------------------
-- Page views
-- ----------------------------------------------------------------------------

--
-- Template:Item
--

function p.itembox (frame)
    --
    -- Args/Frame
    --
    local t = os.clock()
    
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = util.misc.get_frame(frame)
    
    --
    -- Shared args
    --
    
    core.build_item_classes()
    
    tpl_args._flags = {}
    tpl_args._total_args = {}
    tpl_args._base_item_args = {}
    tpl_args._mods = {}
    tpl_args._stats = {}
    tpl_args._explicit_stats = {}
    tpl_args._subobjects = {}
    tpl_args._properties = {}
    
    -- Using general purpose function to handle release and removal versions
    util.args.version(tpl_args, {frame=frame, set_properties=true})
    
    -- Must validate some argument early. It is required for future things
    core.process_arguments(tpl_args, frame, {array=core.default_args})
    core.process_arguments(tpl_args, frame, {array=core.item_classes[tpl_args.class].args})
    
    -- set defaults
    
    for k, v in pairs(core.item_classes[tpl_args.class].defaults) do
        if tpl_args[k] == nil then
            tpl_args[k] = v
        end
    end
    
    -- Base Item

    core.process_base_item(tpl_args, frame)
    
    -- Prophecy special snowflake
    if tpl_args.base_item == 'Prophecy' then
        err = core.process_arguments(tpl_args, frame, {array=core.prophecy_args})
        if err then
            return err
        end
        
        tpl_args.inventory_icon = string.format(i18n.inventory_icon, 'Prophecy')
    end
    
    -- Mods

    for _, k in ipairs({'implicit', 'explicit'}) do
        local success = true
        local i = 1
        while success do
            success = core.validate_mod(tpl_args, frame, {key=k, i=i})
            i = i + 1
        end
    end
    
    core.process_smw_mods(tpl_args, frame)
        
    -- Add stats - this is for when mods are not set, but we still need stats to calcuate new armour values etc
    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
        h.stats_update(tpl_args, stat.id, stat, nil, '_stats')
        h.stats_update(tpl_args, stat.id, stat, nil, '_explicit_stats')
    end

    -- Transpose stats into subobjects
    for id, data in pairs(tpl_args._stats) do
        tpl_args._subobjects[id] = {
            ['Has stat id'] = id,
            ['Has minimum stat value'] = data.min,
            ['Has maximum stat value'] = data.max,
            ['Has average stat value'] = data.avg,
        }
    end
    for id, data in pairs(tpl_args._explicit_stats) do
        tpl_args._subobjects['explicit_' .. id] = {
            ['Has explicit stat id'] = id,
            ['Has minimum stat value'] = data.min,
            ['Has maximum stat value'] = data.max,
            ['Has average stat value'] = data.avg,
        }
    end
    
    -- Handle extra stats (for gems)
    
    if core.class_groups.gems.keys[tpl_args.class] then
        m_skill.skill(frame, tpl_args)
    end
    
    --
    -- Handle local stats increases/reductions/additions
    --
    
    local skip = {}
    -- override attributes for tabula rasa
    if tpl_args._stats.local_unique_tabula_rasa_no_requirement_or_energy_shield ~= nil and tpl_args._stats.local_unique_tabula_rasa_no_requirement_or_energy_shield.min == 1 then
        tpl_args.required_level_final = 1
    end
    
    -- general stats
    for k, data in pairs(core.stat_map) do
        local value = tpl_args[k]
 
        if value == nil and data.default ~= nil then
            value = data.default
            tpl_args[k] = data.default
        end
        
        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(h.range_map) do
                    tpl_args._properties[data.property .. range_data.property] = value[short_key]
                end
                
                -- process to HTML to use on list pages or other purposes
                h.handle_range_args(tpl_args, frame, k, data.property, value, data.html_fmt_options or {})
            end
            
            for short_key, range_data in pairs(h.range_map) do
                tpl_args[k .. range_data.var] = value[short_key]
            end
        end
    end

    -- calculate and handle weapon dps
    if core.class_groups.weapons.keys[tpl_args.class] then
        for _, data in ipairs(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(h.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(h.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.name, range_data.var)] = result
                -- It will set the property, even if 0. 
                -- Not sure if it is better to not set it, but on the other hand this way it can be queried for having no dps of a particular type
                tpl_args._properties[string.format('Has %s%s', data.property, range_data.property)] = result
            end
            
            if value.avg > 0 then
                h.handle_range_args(tpl_args, frame, data.name, 'Has ' .. data.property, value, data.html_fmt_options or {})
            end
        end
    end
    
    -- late processing
    core.process_arguments(tpl_args, frame, {array=core.late_args})
    core.process_arguments(tpl_args, frame, {array=core.item_classes[tpl_args.class].late_args})
    
    -- Handle upgrade from restrictions/info
    core.process_upgraded_from(tpl_args, frame)
    
    -- Setting semantic properties Part 1 (base values)
    
    local val
    
    for _, k in ipairs(tpl_args._total_args) do
        local prop = core.map[k].property
        val = tpl_args[k]
        if val == nil then
        elseif prop == nil then
            --mw.logObject(k)
        else
            tpl_args._properties[prop] = val
        end
    end
    
    util.smw.set(frame, tpl_args._properties)
    
    -- Subobjects
    local command
    for key, properties in pairs(tpl_args._subobjects) do
        command = ''
        if type(key) ~= 'number' then
            command = key
        end
        util.smw.subobject(frame, command, properties)
    end
    
    -- ------------------------------------------------------------------------
    -- Infobox handling 
    -- ------------------------------------------------------------------------
    
    tpl_args._properties = {}
    local extra_class = ''
    local container = mw.html.create('span')
        :attr( 'class', 'item-box -' .. tpl_args.frame_type)
    
    if tpl_args.class == 'Divination Card' then
        --TODO div card code
        container
            :tag('span')
                :attr( 'class', 'divicard-wrapper')
                :tag('span')
                    :attr('class', 'divicard-art')
                    :wikitext( '[[File:' .. 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.card_reward)
                        :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
        if tpl_args.base_item and tpl_args.rarity ~= 'Normal' then
            line_type = 'double'
        else
            line_type = 'single'
        end
        
        local name_line = tpl_args.name
        if tpl_args.base_item and tpl_args.base_item ~= 'Prophecy' then
            name_line = name_line .. '<br>' .. tpl_args.base_item
        end
        
        container
            :tag('span')
                :attr( 'class', 'header -' .. line_type )
				:wikitext( name_line )
            :done()
            
        local grpcont
        local valid
        local statcont = container:tag('span')
        statcont
            :attr('class', 'item-stats')
            :done()
            
        for _, group in ipairs(core.display_groups) do
            grpcont = {}
            if group.func == nil then
                for _, disp in ipairs(group) do
                    valid = true
                    -- No args to verify which means always valid
                    if disp.args == nil then
                    elseif type(disp.args) == 'table' then
                        for _, key in ipairs(disp.args) do
                            if tpl_args[key] == nil then
                                valid = false
                                break
                            end
                        end
                    elseif type(disp.args) == 'function' then
                        valid = disp.args(tpl_args, frame)
                    end
                    if valid then
                        grpcont[#grpcont+1] = disp.func(tpl_args, frame)
                    end
                end
            else
                grpcont = group.func(tpl_args, frame)
            end
            
            if #grpcont > 0 then
                local header = ''
                if group.header then
                    header = group.header()
                end
                statcont
                    :tag('span')
                    :attr('class', 'group ' .. (group.css_class or ''))
                    :wikitext(header .. table.concat(grpcont, '<br>'))
                    :done()
            end
        end
    end
    
    frame:callParserFunction('#set:', tpl_args._properties)
    
    if tpl_args.gem_icon ~= nil then
        container:wikitext(string.format('[[%s]]', tpl_args.gem_icon))
    end
    
    -- Store the infobox so it can be accessed with ease on other pages
    frame:callParserFunction('#set:', {['Has infobox HTML'] = tostring(container)})
    
    if tpl_args.inventory_icon ~= nil then
        container:wikitext(string.format('[[%s]]', tpl_args.inventory_icon))
    end
    
    local infobox = mw.html.create('span')
    infobox
        :attr('class', 'infobox-page-container')
        :node(container)
        
    if tpl_args.skill_screenshot then
        infobox:wikitext(string.format('<br>[[%s|300px]]', tpl_args.skill_screenshot))
    end
    
    local out = tostring(infobox)
    
    -- ------------------------------------------------------------------------
    -- Category handling 
    -- ------------------------------------------------------------------------
    
    local cats = {}
    if tpl_args.rarity == 'Unique' then
        cats[#cats+1] = string.format(i18n.categories.unique_affix, tpl_args.class)
    elseif tpl_args.base_item == 'Prophecy' then
        cats[#cats+1] = 'Prophecies'
    elseif tpl_args.is_talisman then
        cats[#cats+1] = 'Talismans'
    else
        cats[#cats+1] = tpl_args.class
    end
    
    for _, attr in ipairs(m_game.constants.attributes) do
        if tpl_args[attr.long_lower .. '_percent'] then
            cats[#cats+1] = string.format('%s %s', attr.long_upper, tpl_args.class)
        end
    end
    
    local affix
    if tpl_args.class == 'Active Skill Gems' or tpl_args.class == 'Support Skill Gems' 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.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 then
        cats[#cats+1] = i18n.categories.improper_modifiers
    end
    
    out = out .. util.misc.add_category(cats, {ingore_blacklist=tpl_args.debug})
    
    --
    -- 
    --

    mw.logObject(os.clock() - t)
    
    return out
end

--
-- Template:Item link & Template:Sl
--

function p.item_link (frame)
    --
    -- Args/Frame
    --
    
    local tpl_args = getArgs(frame, {
        parentFirst = true,
        removeBlanks = false,
    })
    frame = util.misc.get_frame(frame)
    
    -- Backwards compability
    tpl_args.item_name = tpl_args.item_name or tpl_args[1]
    if tpl_args.item_name ~= nil then
        tpl_args.item_name = mw.ustring.lower(tpl_args.item_name)
    end
    tpl_args.name = tpl_args.name or tpl_args[2]
    
    if util.table.has_all_value(tpl_args, {'page', 'item_name', 'item_name_exact'}) then
        error(i18n.errors.item_link_invalid_args)
    end
    
    tpl_args.large = util.cast.boolean(tpl_args.large)
    
    local img
    local result
    
    if util.table.has_one_value(tpl_args, core.item_link_params, nil) or tpl_args.item_name ~= nil then
        local query = {}
        
        if tpl_args.page ~= nil then
            -- TODO returns the result even if the + format is specified. 
            query[#query+1] = string.format('[[%s]]', tpl_args.page)
        else
            if tpl_args.item_name ~= nil then
                query[#query+1] = string.format('[[Has lowercase names::%s]]', tpl_args.item_name)
            elseif tpl_args.item_name_exact ~= nil then
                query[#query+1] = string.format('[[Has name::%s]]', tpl_args.item_name_exact)
            end
            
            query[#query] = query[#query] .. ' [[Has inventory icon::+]] [[Has infobox HTML::+]]'
            
            if tpl_args.link_type == 'skill' then
                query[#query] = query[#query] .. ' [[Concept:Skill gems]]'
            end
        end
        
        query[#query+1] = '?Has name'
        query[#query+1] = '?Has inventory icon'
        query[#query+1] = '?Has infobox HTML'
        query[#query+1] = '?Has alternate inventory icons'
        query[#query+1] = '?Has inventory width'
        query[#query+1] = '?Has inventory height'
        
        -- attributes
        result = util.smw.query(query, frame)
        
        local err
        if #result == 0 then
            err = util.misc.raise_error_or_return{raise_required=true, args=tpl_args, msg=string.format(
                i18n.errors.item_link_no_results, 
                tpl_args.page or tpl_args.item_name or tpl_args.item_name_exact 
            )}
        elseif #result > 1 then
            err = util.misc.raise_error_or_return{raise_required=true, args=tpl_args, msg=string.format(
                i18n.errors.item_link_too_many_results,
                tpl_args.page or tpl_args.item_name or tpl_args.item_name_exact
            )}
        end
        
        if err ~= nil then
            return err .. i18n.categories.broken_item_links
        end
        
        result = result[1]
    else
        result = {tpl_args.page or tpl_args.item_name_exact}
    end
    
    for _, k in ipairs(core.item_link_params) do
        local prop = core.map[k].property
        if tpl_args[k] ~= nil then
            result[prop] = tpl_args[k]
        end
    end
    
    if tpl_args.image ~= nil then
        if result['Has alternate inventory icons'] == '' then
            return util.misc.raise_error_or_return{raise_required=true, args=tpl_args, msg=string.format(
                i18n.errors.item_link_alt_art_undefined,
                result[1]
            ) .. i18n.categories.broken_item_links}
        end
        
        result['Has alternate inventory icons'] = util.string.split(result['Has alternate inventory icons'], '<MANY>')
        
        local index = tonumber(tpl_args.image)
        if index ~= nil then
            img = result['Has alternate inventory icons'][index]
        else
            -- offset 1 is needed
            local suffix = string.len(' inventory icon.png') + 1 
            -- add an extra offset by 1 to account for the space 
            local prefix = string.len(string.sub(result['Has inventory icon'], 1, -suffix)) + 2
            
            for _, filename in ipairs(result['Has alternate inventory icons']) do
                if string.sub(filename, prefix, -suffix) == tpl_args.image then
                    img = filename
                    break
                end
            end
        end
        
        if img == nil then
            return util.misc.raise_error_or_return{raise_required=true, args=tpl_args, msg=string.format(
                 i18n.errors.item_link_alt_art_invalid_index,
                tpl_args.image, result[1]
            ) .. i18n.categories.broken_item_links}
        end
    elseif result['Has inventory icon'] ~= '' then
        img = result['Has inventory icon']
    end
    
    -- output
    
    local container = mw.html.create('span')
    container:attr('class', 'inline-infobox-container')
    
    if not tpl_args.large then
        container:wikitext(string.format('[[%s]]', img))
    end
       
    container:wikitext(string.format('[[%s|%s]]', result[1], result['Has name'] or result[1]))
        
    if result['Has infobox HTML'] ~= '' then
        container
            :tag('span')
                :attr('class', 'inline-infobox-hover')
                :wikitext(result['Has infobox HTML'])
                :wikitext(string.format('[[%s]]', img))
                :done()
    end
    
    if tpl_args.large then
        local width = tonumber(result['Has inventory width']) or tonumber(tpl_args.width)
        local height = tonumber(result['Has inventory height']) or tonumber(tpl_args.height)
        
        if width and height then
            img = string.format('[[%s|%sx%spx]]', img, width*c.image_size, height*c.image_size)
        elseif width then
            img = string.format('[[%s|%spx]]', img, width*c.image_size)
        elseif height then
            img = string.format('[[%s|x%spx]]', img, height*c.image_size)
        else
            img = string.format('[[%s]]', img)
        end
    
        container:wikitext(img)
    end
        
    return tostring(container)
end

--
-- Template:Il
--

function p.item_link_compability (frame)
    if util.misc.is_frame(frame) then
        frame.args.raise = true
    else
        frame.raise = true
    end
    
    local result

    local status, err = pcall(function () 
        result = p.item_link(frame) 
    end)
    mw.logObject(err)
    
    if status then
        return result
    elseif string.match(err, ".*Too many results.*") then
        return err .. i18n.categories.broken_item_links
    else
        return require('Module:Item').itemLink(frame)
    end
end


-- ----------------------------------------------------------------------------
-- Result formatting templates for SMW queries
-- ----------------------------------------------------------------------------

--
-- Template:
--

function p.simple_item_list_row(frame)
    -- Args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = util.misc.get_frame(frame)
    
    --
    local args = util.string.split_args(tpl_args.userparam, {sep=', '})
    tpl_args.userparam = args
    
    local link = p.item_link{page=tpl_args[1], name=tpl_args['?Has name'], inventory_icon=tpl_args['?Has inventory icon'] or '', html=tpl_args['?Has infobox HTML'] or ''}
    if args.format == nil then
        return '* ' .. link
    elseif args.format == 'none' then
        return link
    elseif args.format == 'li' then
        return string.format('<li>%s</li>', link)
    else
        error(string.format(i18n.errors.generic_argument_parameter, 'format', args.format))
    end
end

-- ----------------------------------------------------------------------------
-- Reponsibile for subtemplates of Template:SMW item table
-- 

-- =p.item_table{mode='item', stat_column1_format='%s%%', conditions='[[Has item class::+]] [[Has subobject.Has stat id::~additional_*dexterity*]] [[Has rarity::Unique]]', stat_column1_header='Dexterity', stat_column1_stat_format='add', stat_column1_format='Test %s', stat_column1_stat1_id='additional_dexterity', stat_column1_stat2_id='additional_strength_and_dexterity'}
-- =p.item_table{mode='skill'}
function p.item_table(frame)
    -- args
    local tpl_args = getArgs(frame, {
            parentFirst = true
        })
    frame = util.misc.get_frame(frame)
    
    local modes = {
        skill = {
            data = core.result.skill_gem_new,
            header = i18n.item_table.skill_gem,
        },
        item = {
            data = core.result.generic_item,
            header = i18n.item_table.item,
        },
    }
    
    if tpl_args.mode == nil then
        tpl_args.mode = 'item'
    end
    
    if modes[tpl_args.mode] == nil then
        error(i18n.errors.invalid_item_table_mode)
    end
    
    local row_infos = {}
    for _, row_info in ipairs(modes[tpl_args.mode].data) do
        local enabled = false
        if row_info.arg == nil then
            enabled = true
        elseif type(row_info.arg) == 'string' and util.cast.boolean(tpl_args[row_info.arg]) then
            enabled = true
        elseif type(row_info.arg) == 'table' then 
            for _, argument in ipairs(row_info.arg) do
                if util.cast.boolean(tpl_args[argument]) then
                    enabled = true
                    break
                end
            end
        end
        
        if enabled then
            row_info.options = row_info.options or {}
            row_infos[#row_infos+1] = row_info
        end
    end
    
    -- Parse stat arguments
    local stat_columns = {}
    local query_stats = {}
    local stat_results = {}
    local i = 0
    repeat
        i = i + 1
        
        local prefix = string.format('stat_column%s_', i)
        local col_info = {
            header = tpl_args[prefix .. 'header'] or tostring(i),
            format = tpl_args[prefix .. 'format'],
            stat_format = tpl_args[prefix .. 'stat_format'] or 'separate',
            order = tonumber(tpl_args[prefix .. 'order']) or (10000000 + i),
            stats = {},
            options = {},
        }
        
        local j = 0
        repeat
            j = j +1
        
            local stat_info = {
                id = tpl_args[string.format('%sstat%s_id', prefix, j)],
            }
            
            if stat_info.id then
                col_info.stats[#col_info.stats+1] = stat_info
                query_stats[stat_info.id] = {} 
            else
                -- Stop iteration entirely if this was the first index but no stat was supplied. We assume that we stop in this case.
                if j == 1 then
                    i = nil
                end
                -- stop iteration
                j = nil
            end
        until j == nil
        
        -- Don't add this column if no stats were provided. 
        if #col_info.stats > 0 then
            stat_columns[#stat_columns+1] = col_info
        end
    until i == nil
    
    for _, col_info in ipairs(stat_columns) do
        local row_info = {
            --arg
            header = col_info.header,
            properties = {},
            display = function(tr, data, properties)
                if col_info.stat_format == 'separate' then
                    local stat_texts = {}
                    local num_stats = 0
                    local vmax = 0
                    for _, stat_info in ipairs(col_info.stats) do
                        num_stats = num_stats + 1
                        -- stat results from outside body
                        local stat = (stat_results[data[1]] or {})[stat_info.id]
                        if stat ~= nil then
                            stat_texts[#stat_texts+1] = h.format_value(tpl_args, frame, stat, {no_color=true})
                            vmax = vmax + stat.max
                        end
                    end
                    
                    if num_stats ~= #stat_texts then
                        tr:wikitext(util.html.td.na())
                    else
                        local text
                        if col_info.format then
                            text = string.format(col_info.format, unpack(stat_texts))
                        else
                            text = table.concat(stat_texts, ', ')
                        end
                    
                        tr:tag('td')
                            :attr('data-sort-value', vmax)
                            :attr('class', 'tc -mod')
                            :wikitext(text)
                    end
                 elseif col_info.stat_format == 'add' then
                    local total_stat = {
                        min = 0,
                        max = 0,
                        avg = 0,
                    }
                    for _, stat_info in ipairs(col_info.stats) do
                        local stat = (stat_results[data[1]] or {})[stat_info.id]
                        if stat ~= nil then
                            for k, v in pairs(total_stat) do
                                total_stat[k] = v + stat[k]
                            end
                        end
                    end
                    
                    if col_info.format == nil then
                        col_info.format = '%s'
                    end
                    
                    tr:tag('td')
                        :attr('data-sort-value', total_stat.max)
                        :attr('class', 'tc -mod')
                        :wikitext(string.format(col_info.format, h.format_value(tpl_args, frame, total_stat, {no_color=true})))
                 else
                    error(string.format(i18n.errors.generic_argument_parameter, 'stat_format', col_info.stat_format))
                 end
            end,
            order = col_info.order,
        }
        table.insert(row_infos, row_info)
    end
    
    -- sort the rows
    table.sort(row_infos, function (a, b)
        return (a.order or 0) < (b.order or 0)
    end)
    
    -- Parse query arguments
    local query = {tpl_args.conditions}
    for key, value in pairs(tpl_args) do 
        if string.sub(key, 0, 2) == 'q_' then
            query[string.sub(key, 3)] = value
        end
    end
    
    --Override and set defaults
    query.limit = 1000
    
    -- Set required fields
    query[#query+1] = '?Has name#'
    query[#query+1] = '?Has iventory icon#'
    query[#query+1] = '?Has infobox HTML#'
    query[#query+1] = '?Has inventory width#'
    query[#query+1] = '?Has inventory height#'
    for _, rowinfo in ipairs(row_infos) do
        if type(rowinfo.properties) == 'function' then
            rowinfo.properties = rowinfo.properties()
        end
        for index, property in ipairs(rowinfo.properties) do
            rowinfo.options[index] = rowinfo.options[index] or {}
            query[#query+1] = '?' .. property .. '#'
        end
    end
    local results = util.smw.query(query, frame)
    
    if #results == 0 and tpl_args.default ~= nil then
        return tpl_args.default
    end
    
    if #stat_columns > 0 then
        local continue = true
        local offset = 0
        while continue do
            query = {string.format('[[-Has subobject::<q>%s</q>]] [[Has stat id::+]]', tpl_args.conditions)}
            query[#query+1] = '?Has average stat value#'
            query[#query+1] = '?Has maximum stat value#'
            query[#query+1] = '?Has minimum stat value#'
            query[#query+1] = '?Has stat id#'
            query.limit = 1000
            query.offset = 0
            local temp = util.smw.query(query, frame)
            for _, row in ipairs(temp) do
                if query_stats[row['Has stat id']] ~= nil then
                    local stat = {
                        min = tonumber(row['Has minimum stat value']),
                        max = tonumber(row['Has maximum stat value']),
                        avg = tonumber(row['Has average stat value']),
                    }
                    
                    local page = util.string.split(row[1], '#')[1]
                    if stat_results[page] == nil then
                        stat_results[page] = {[row['Has stat id']] = stat}
                    else
                        stat_results[page][row['Has stat id']] = stat
                    end
                end
            end
            
            -- stop iteration if we didn't hit the query limit
            if #temp == 1000 then
                offset = offset + 1000
            else
                continue = false
            end
        end
    end
    
    local tbl = mw.html.create('table')
    tbl:attr('class', 'wikitable sortable item-table')
    
    -- Header
    
    local tr = tbl:tag('tr')
    tr
        :tag('th')
            :wikitext(modes[tpl_args.mode].header)
            :done()
            
    for _, row_info in ipairs(row_infos) do
        tr
            :tag('th')
                :attr('data-sort-type', row_info.sort_type or 'number')
                :wikitext(row_info.header)
                :done()
    end
    
    for _, row in ipairs(results) do
        tr = tbl:tag('tr')
    
        local il_args = {
            page=row[1], 
            name=row['Has name'], 
            inventory_icon=row['Has inventory icon'], 
            html=row['Has infobox HTML'],
            width=row['Has inventory width'],
            height=row['Has inventory height'],
        }
        
        if tpl_args.large then
            il_args.large = tpl_args.large
        end
        
        tr
            :tag('td')
                :wikitext(p.item_link(il_args))
                :done()
                
        for _, rowinfo in ipairs(row_infos) do
            -- this has been cast from a function in an earlier step
            local display = true
            for index, property in ipairs(rowinfo.properties) do
                -- this will bet set to an empty value not nil confusingly
                if row[property] == '' then
                    if rowinfo.options[index].optional ~= true then
                        display = false
                        break
                    else
                        row[property] = nil
                    end
                end
            end
            if display then
                rowinfo.display(tr, row, rowinfo.properties)
            else
                tr:wikitext(util.html.td.na())
            end
        end
    end
    
    return tostring(tbl)
end


item_table_factory = {}
function item_table_factory.intro(args)
    -- args:
    --  data_array
    --  header
    
    return function (frame)
        -- Args
        local tpl_args = getArgs(frame, {
            parentFirst = true
        })
        frame = util.misc.get_frame(frame)
        
        --
        tpl_args.userparam = util.string.split_args(tpl_args.userparam, {sep=', '})
        
        local tr = mw.html.create('tr')
        tr
            :tag('th')
                :wikitext(args.header)
                :done()
                
        for _, rowinfo in ipairs(args.data_array) do
            if rowinfo.arg == nil or tpl_args.userparam[rowinfo.arg] then
                tr
                    :tag('th')
                        :wikitext(rowinfo.header)
                        :done()
            end
        end
        
        return '<table class="wikitable sortable item-table">' .. tostring(tr)
    end
end

-- ----------------------------------------------------------------------------
-- Item lists
-- ----------------------------------------------------------------------------

function p.skill_gem_list_by_gem_tag(frame)
    -- Args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = util.misc.get_frame(frame)
    
    if tpl_args.class == 'Support Skill Gems' then
    elseif tpl_args.class == 'Active Skill Gems' then
    else
        error(i18n.errors.invalid_item_class)
    end

    local query = {}
    query[#query+1] = string.format('[[Has item class::%s]]', tpl_args.class)
    query[#query+1] = '?Has gem tags'
    query[#query+1] = '?Has name'
    query[#query+1] = '?Has inventory icon'
    --query[#query+1] = '?Has infobox HTML'
    query.limit = 5000
    query.sort = 'Has name'
    
    local results = util.smw.query(query, frame)
    
    local tags = {}
    
    for _, row in ipairs(results) do
        row['Has gem tags'] = util.string.split(row['Has gem tags'], '<MANY>')
        for _, tag in ipairs(row['Has gem tags']) do
            if tags[tag] == nil then
                tags[tag] = {}
            end
            table.insert(tags[tag], row)
        end
    end
    
    local tags_sorted = {}
    for tag, _ in pairs(tags) do
        table.insert(tags_sorted, tag)
    end
    table.sort(tags_sorted)
    
    local tbl = mw.html.create('table')
    tbl
        :attr('class', 'wikitable sortable')
        :tag('tr')
            :tag('th')
                :wikitext('Tag')
                :done()
            :tag('th')
                :wikitext('Skills')
                :done()
            :done()
    
    for _, tag in ipairs(tags_sorted) do
        local rows = tags[tag]
        local tr = tbl:tag('tr')
        tr
            :tag('td')
                :wikitext(tag)
            
        local td = tr:tag('td')
        for i, row in ipairs(rows) do
            td:wikitext(p.item_link{page=row[1], name=row['Has Name'], inventory_icon=row['Has inventory icon'], html=row['Has infobox HTML'] or ''})
            if i < #rows then
                td:wikitext('<br>')
            end
        end
    end
   
    return tostring(tbl)
end

-- ----------------------------------------------------------------------------
-- Misc. Item templates
-- ----------------------------------------------------------------------------

-- 
-- Template:Item class
--

function p.item_class (frame)
    -- Get args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = util.misc.get_frame(frame)
	
	if not doInfoCard then
		doInfoCard = require('Module:Infocard')._main
	end
    
    core.factory.table_cast('name', {key='full', tbl=m_game.constants.item.class})(tpl_args, frame)
    
    if tpl_args.name_list ~= nil then
        tpl_args.name_list = util.string.split(tpl_args.name_list, ', ')
    else
        tpl_args.name_list = {}
    end
    
    --
    
    local ul = mw.html.create('ul')
    for _, item in ipairs(tpl_args.name_list) do
        ul
            :tag('li')
                :wikitext(item)
                :done()
    end
    

    -- Output Infocard
    
    local tplargs = {
        ['header'] = tpl_args.name,
        ['subheader'] = i18n.item_class_infobox.page .. i18n.item_class_infobox.info,
        [1] = i18n.item_class_infobox.also_referred_to_as .. tostring(ul),
    }
    
    -- cats
    
    local cats = {
        'Item classes',
        tpl_args.name,
    }
    
    -- Done
    
    return doInfoCard(tplargs) .. util.misc.add_category(cats, {ingore_blacklist=tpl_args.debug})
end

-- ----------------------------------------------------------------------------
-- Debug stuff
-- ----------------------------------------------------------------------------

p.debug = {}
function p.debug.show_range_vars ()
    for _, prop in ipairs({'HTML', 'range text', 'range colour'}) do
        for var, data in pairs(core.stat_map) do
            mw.logObject(string.format('%s %s', data.property, prop))
        end
        
        for _, data in ipairs(core.dps_map) do
            mw.logObject(string.format('Has %s %s', data.property, prop))
        end
    end
end

function p.debug._tbl_data(tbl)
    keys = {}
    for _, data in ipairs(core.result.generic_item) do
        if type(data.arg) == 'string' then 
            keys[data.arg] = 1
        elseif type(data.arg) == 'table' then
            for _, arg in ipairs(data.arg) do
                keys[arg] = 1
            end
        end
    end
    
    local out = {}
    for key, _ in pairs(keys) do
        out[#out+1] = string.format("['%s'] = '1'", key)
    end
    
    return table.concat(out, ', ')
end

function p.debug.generic_item_all()
    return p.debug._tbl_data(core.result.generic_item)
end

function p.debug.skill_gem_all()
    return p.debug._tbl_data(core.result.skill_gem_new)
end

return p