Module:Item2/core: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
No edit summary
(This stat does not remove the level requirement)
(104 intermediate revisions by 3 users not shown)
Line 8: Line 8:
local m_cargo = require('Module:Cargo')
local m_cargo = require('Module:Cargo')


local m_game = mw.loadData('Module:Game')
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Item2')
 
local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or mw.loadData('Module:Game')


-- The cfg table contains all localisable strings and configuration, to make it
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Item2/config')
local cfg = use_sandbox and mw.loadData('Module:Item2/config/sandbox') or mw.loadData('Module:Item2/config')


local i18n = cfg.i18n
local i18n = cfg.i18n
Line 22: Line 25:
local h = {}
local h = {}


function h.process_mod_stats(tpl_args, args)
-- ----------------------------------------------------------------------------
     local lines = {}
-- Core
      
-- ----------------------------------------------------------------------------
     local skip = cfg.class_specifics[tpl_args.class_id]
 
     if skip then
local core = {}
        skip = skip.skip_stat_lines
 
     end
core.factory = {}
      
 
     local random_mods = {}
function core.factory.infobox_line(args)
      
     -- args: table
    for _, modinfo in ipairs(tpl_args._mods) do
    --  type: How to read data from tpl_args using the given keys. nil = Regular, gem = Gem progression, stat = Stats
         if modinfo.is_implicit == args.is_implicit then
    --  parts: table
             if modinfo.is_random == true then
     --  [n]: table
                if random_mods[modinfo.stat_text] then
     --    key: key to use. If type = gem and table is given, parse for subfield along path
                    table.insert(random_mods[modinfo.stat_text], modinfo)
    --    hide: Hide part if this function returns true
                 else
    --    hide_key: Alternate key to use to retrieve the value
                    random_mods[modinfo.stat_text] = {modinfo}
    --    hide_default: hide the value if this is set
                end
    --    hide_default_key: key to use if it isn't equal to the key parameter
             else
    --    ----- params from m_util.html.format_value -----
                 if modinfo.id == nil then
    --    func: Function to transform the value retrieved from the database
                     table.insert(lines, modinfo.result)
    --    fmt: Format string (or function that returns format string) to use for the value.
                -- Allows the override of the SMW fetched mod texts for this modifier via <modtype><id>_text parameter
    --      Default: '%s'
                elseif modinfo.text ~= nil then
    --    fmt_range: Format string to use for range value.
                    table.insert(lines, modinfo.text)
    --      Default: '(%s-%s)'
                else
    --    color: poe_color code to use for the value. False for no color.
                     for _, line in ipairs(m_util.string.split(modinfo.result['mods.stat_text'] or '', '<br>')) do
     --      Default: 'value' if value is unmodified; 'mod' if modified
                         if line ~= '' then
    --    class: Additional css class added to color tag
                             if skip == nil then
    --    inline: Format string to use for the output
                                table.insert(lines, line)
    --    inline_color: poe_color code to use for the output. False for no color.
                             else
    --      Default: Inherits from value color
                                local skipped = false
    --    inline_class: Additional css class added to inline color tag
                                for _, pattern in ipairs(skip) do
    --  sep: If specified, parts are joined with this separator before being formatted for output
                                    if string.match(line, pattern) then
    --  fmt: Format string to use for output. If not specified, parts are simply concatenated
                                        skipped = true
     --  color: poe_color code to use for output. Default: no color
                                        break
     --  class: Additional css class added to output
                                    end
 
                                 end
     args.parts = args.parts or {}
                                if not skipped then
     return function (tpl_args)
                                     table.insert(lines, line)
        local base_values = {}
        local temp_values = {}
         if args.type == 'gem' then
             -- Skill progression. Look for keys in tpl_args.skill_levels
            if not cfg.class_groups.gems.keys[tpl_args.class_id] then
                 -- Skip if this item is not actually a gem
                return
            end
             for i, data in ipairs(args.parts) do
                 if data.key then
                     local path = type(data.key) == 'table' and data.key or {data.key}
                    -- Check for static value
                    local value = tpl_args.skill_levels[0]
                     for _, p in ipairs(path) do -- Parse for subfield along path
                         if value[p] == nil then
                             value = nil
                            break
                        else
                            value = value[p]
                        end
                    end
                    if value ~= nil then
                        base_values[i] = value
                        temp_values[#temp_values+1] = {value={min=value,max=value}, index=i}
                    else -- Check for leveled values
                        value = {
                            min = tpl_args.skill_levels[1],
                             max = tpl_args.skill_levels[tpl_args.max_level],
                        }
                        for k, _ in pairs(value) do
                            for _, p in ipairs(path) do -- Parse for subfield along path
                                if value[k][p] == nil then
                                    value[k] = nil
                                    break
                                 else
                                     value[k] = value[k][p]
                                 end
                                 end
                             end
                             end
                        end
                        if value.min ~= nil and value.max ~= nil then
                            base_values[i] = value.min
                            temp_values[#temp_values+1] = {value=value, index=i}
                         end
                         end
                     end
                     end
                 end
                 end
            end
        elseif args.type == 'stat' then
            -- Stats. Look for key in tpl_args._stats
            for i, data in ipairs(args.parts) do
                local stat = tpl_args._stats[data.key]
                if stat then
                    local total = {min=0, max=0}
                    for _, v in ipairs(stat) do
                        if v.mod == nil or v.mod.is_implicit or v.mod.is_explicit then
                            total.min = total.min + v.min
                            total.max = total.max + v.max
                        end
                    end
                    base_values[i] = total.min
                    temp_values[#temp_values+1] = {value=total, index=i}
                end
            end
        else
            -- Regular. Look for key exactly as written in tpl_args
            for i, data in ipairs(args.parts) do
                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 and value.max ~= nil then
                    base_values[i] = value.min
                    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.parts[data.index]
            local hide = false
            if type(opt.hide) == 'function' then
                local v = data.value
                if opt.hide_key then
                    v = {
                        min = tpl_args[opt.hide_key .. '_range_minimum'],
                        max = tpl_args[opt.hide_key .. '_range_maximum'],
                    }
                    if v.min == nil or v.max == nil then
                        v = tpl_args[opt.hide_key]
                    end
                end
                hide = opt.hide(tpl_args, v)
            elseif opt.hide_default ~= nil then
                if opt.hide_default_key then
                    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
                            hide = true
                        end
                    elseif opt.hide_default == v.min and opt.hide_default == v.max then
                        hide = true
                    end
                else
                    local v = data.value
                    if opt.hide_default == v.min and opt.hide_default == v.max then
                        hide = true
                    end
                end
            end           
            if not hide then
                table.insert(final_values, data)
             end
             end
         end
         end
    end
       
   
         -- all zeros = dont display and return early
    for stat_text, modinfo_list in pairs(random_mods) do
         if #final_values == 0 then
         local text = {}
             return nil
         for _, modinfo in ipairs(modinfo_list) do
             table.insert(text, modinfo.result['mods.stat_text'])
         end
         end
   
       
         local tbl = mw.html.create('table')
        local parts = {}
        tbl
         for i, data in ipairs(final_values) do
             :attr('class', 'random-modifier-stats mw-collapsed')
            local value = data.value
             :attr('style', 'text-align: left')
            value.base = base_values[data.index]
             :tag('tr')
            local options = args.parts[data.index]
                :tag('th')
             if args.type == 'gem' and options.color == nil then
                    :attr('class', 'mw-customtoggle-31')
                -- Display skill progression range values as unmodified (white)
                    :wikitext(stat_text)
                options.color = 'value'
                    :done()
             end
                :done()
            parts[#parts+1] = m_util.html.format_value(tpl_args, value, options)
             :tag('tr')
        end
                 :attr('class', 'mw-collapsible mw-collapsed')
        if args.sep then
                 :attr('id', 'mw-customcollapsible-31')
            -- Join parts with separator before formatting
                :tag('td')
             parts = {table.concat(parts, args.sep)}
                    :wikitext(table.concat(text, '<hr style="width: 20%">'))
        end
                    :done()
 
                :done()
        -- Build output string
         table.insert(lines, tostring(tbl))
        local out
     end
        if args.fmt then
      
            out = string.format(args.fmt, unpack(parts))
     if #lines == 0 then
        else
         return
            out = table.concat(parts)
        end
        if args.color then
            out = m_util.html.poe_color(args.color, out, args.class)
        elseif args.class then
             out = tostring(mw.html.create('em')
                 :attr('class', class)
                 :wikitext(out)
            )
        end
        return out
    end
end
 
function core.add_stat(tpl_args, stat_id, value, options)
    options = options or {}
    local mod = options.mod
    local tbl = options.tbl or '_stats'
    tpl_args[tbl] = tpl_args[tbl] or {}
    tpl_args[tbl][stat_id] = tpl_args[tbl][stat_id] or {
        min = 0,
        max = 0,
         avg = 0,
     }
    local stat = {
        mod = mod,
     }
     if type(value) == 'table' then
         stat.min = value.min
        stat.max = value.max
        stat.avg = value.avg or (stat.min + stat.max) / 2
     else
     else
         return table.concat(lines, '<br>')
         stat.min = value
        stat.max = value
        stat.avg = value
     end
     end
    table.insert(tpl_args[tbl][stat_id], stat)
    -- Totals
    tpl_args[tbl][stat_id].min = tpl_args[tbl][stat_id].min + stat.min
    tpl_args[tbl][stat_id].max = tpl_args[tbl][stat_id].max + stat.max
    tpl_args[tbl][stat_id].avg = tpl_args[tbl][stat_id].avg + stat.avg
end
end


--  
--
-- Factory
-- Functions for processing tpl_args
--  
--
h.proc = {}
h.proc.factory = {}


h.factory = {}
function h.proc.factory.value(args)
    args = args or {}
    return function (tpl_args, value)
        if value == nil then
            return nil
        end
        if args.cast then
            value = args.cast(value)
        end
        if args.validate then
            value = args.validate(value)
        end
        return value
    end
end


function h.factory.cast_text(k, args)
function h.proc.factory.list(args)
     args = args or {}
     args = args or {}
     return function (tpl_args, frame)
     return function (tpl_args, value)
         tpl_args[args.key_out or k] = m_util.cast.text(tpl_args[k])
         return m_util.cast.table(value, {
            pattern = args.pattern,
            callback = args.callback,
        })
     end
     end
end
end


function h.factory.damage_html(args)
function h.proc.factory.damage_html(args)
     return function(tpl_args, frame)
     return function (tpl_args, value)
         local keys = {
         local keys = {
             min = args.key .. '_damage_min',
             min = args.type .. '_damage_min',
             max = args.key .. '_damage_max',
             max = args.type .. '_damage_max',
         }
         }
         local value = {}
         local range = {}
         for ktype, key in pairs(keys) do
         for ktype, key in pairs(keys) do
             value[ktype] = h.factory.infobox_line{
             range[ktype] = core.factory.infobox_line{
                 parts = {
                 parts = {
                     {
                     {
Line 132: Line 301:
                     }
                     }
                 }
                 }
             }(tpl_args, frame)
             }(tpl_args)
         end
         end
         if value.min and value.max then
         if range.min and range.max then
             local color = args.key or false
             local color = args.type or false
             local range_fmt
             local range_fmt
             if tpl_args[keys.min .. '_range_minimum'] ~= tpl_args[keys.min .. '_range_maximum'] or tpl_args[keys.max .. '_range_minimum'] ~= tpl_args[keys.max .. '_range_maximum'] then
             if tpl_args[keys.min .. '_range_minimum'] ~= tpl_args[keys.min .. '_range_maximum'] or tpl_args[keys.max .. '_range_minimum'] ~= tpl_args[keys.max .. '_range_maximum'] then
                 -- Variable damage range, based on modifier rolls
                 -- Variable damage range, based on modifier rolls
                 if args.key == 'physical' then
                 if args.type == 'physical' then
                     color = 'mod'
                     color = 'mod'
                 end
                 end
Line 145: Line 314:
             else
             else
                 -- Standard damage range
                 -- Standard damage range
                 if args.key == 'physical' then
                 if args.type == 'physical' then
                     color = 'value'
                     color = 'value'
                 end
                 end
                 range_fmt = i18n.fmt.standard_damage_range
                 range_fmt = i18n.fmt.standard_damage_range
             end
             end
             value = string.format(range_fmt, value.min, value.max)
             value = string.format(range_fmt, range.min, range.max)
             if color then
             if color then
                 value = m_util.html.poe_color(color, value)
                 value = m_util.html.poe_color(color, value)
             end
             end
            tpl_args[args.key .. '_damage_html'] = value
         end
         end
        return value
     end
     end
end
end


-- ----------------------------------------------------------------------------
function h.proc.factory.stat_text(args)
-- Core
    return function (tpl_args, value)
-- ----------------------------------------------------------------------------
        local type_map = {
 
            implicit = 'is_implicit',
local core = {}
            explicit = 'is_explicit',
        }
        if type_map[args.type] == nil then
            return nil
        end
        local lines = {}
        local random_mods = {}
        local skip = cfg.class_specifics[tpl_args.class_id] and cfg.class_specifics[tpl_args.class_id].skip_stat_lines or nil
        for _, mod_data in ipairs(tpl_args._mods) do
            if mod_data[type_map[args.type]] then
                if mod_data.is_random == true then
                    random_mods[mod_data.stat_text] = random_mods[mod_data.stat_text] or {}
                    table.insert(random_mods[mod_data.stat_text], mod_data)
                else
                    if mod_data.stat_text ~= nil then
                        table.insert(lines, mod_data.stat_text)
                    else
                        local text = mod_data.result['mods.stat_text']
                        if text and text ~= '' then
                            for _, line in ipairs(m_util.string.split(text, '<br>')) do
                                local skipped = false
                                if skip then
                                    for _, pattern in ipairs(skip) do
                                        if string.match(line, pattern) then
                                            skipped = true
                                            break
                                        end
                                    end
                                end
                                if not skipped then
                                    table.insert(lines, line)
                                end
                            end
                        end
                    end
                end
            end
        end
        for stat_text, mod_data_list in pairs(random_mods) do
            local text = {}
            for _, mod_data in ipairs(mod_data_list) do
                table.insert(text, mod_data.result['mods.stat_text'])
            end
            local tbl = mw.html.create('table')
            tbl
                :attr('class', 'random-modifier-stats mw-collapsed')
                :attr('style', 'text-align: left')
                :tag('tr')
                    :tag('th')
                        :attr('class', 'mw-customtoggle-31')
                        :wikitext(stat_text)
                        :done()
                    :done()
                :tag('tr')
                    :attr('class', 'mw-collapsible mw-collapsed')
                    :attr('id', 'mw-customcollapsible-31')
                    :tag('td')
                        :wikitext(table.concat(text, '<hr style="width: 20%">'))
                        :done()
                    :done()
            table.insert(lines, tostring(tbl))
        end
        return #lines > 0 and table.concat(lines, '<br>') or nil
    end
end


--
h.proc.text = h.proc.factory.value{cast = m_util.cast.text}
-- argument mapping
h.proc.boolean = h.proc.factory.value{cast = m_util.cast.boolean}
--
h.proc.number = h.proc.factory.value{cast = m_util.cast.number}
-- format:
 
-- tpl_args key = {
h.proc.percentage = h.proc.factory.value{
--  no_copy = true or nil          -- When loading an base item, dont copy this key
     cast = m_util.cast.number,
--  property = 'prop',              -- Property associated with this key
     validate = m_util.validate.factory.number_in_range{
--  property_func = function or nil -- Function to unpack the property into a native lua value.  
         min = 0,
--                                      If not specified, func is used.  
         max = 100,
--                                      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,
        field = 'html',
        type = 'Text',
         func = nil,
     },
     },
     html_extra = {
}
         no_copy = true,
 
         field = 'html_extra',
h.proc.size = h.proc.factory.value{
    cast = m_util.cast.number,
    validate = m_util.validate.factory.number_in_range{
        min = 1,
        max = 4,
    },
}
 
h.proc.character = h.proc.factory.value{
     validate = m_util.validate.factory.string_length{
         min = 1,
        max = 1,
    },
}
 
h.proc.list = h.proc.factory.list()
 
--
-- Argument mapping
--
-- [<tpl_args key>] = {
--  inherit  boolean  Whether the item will inherit this key from its base item. Default: true
--  field  string  Cargo field name
--  type  string  Cargo field type
--  func  function  Function to unpack the argument into a native lua value and validate it
--  default  varies  Default value if parameter is not set
--  deprecated  boolean  Set to true for deprecated parameters
-- }
core.map = {
    -- special params
    html = {
        inherit = false,
        field = 'html',
        type = 'Text',
        func = nil,
    },
    infobox_html = {
        inherit = false,
        field = 'infobox_html',
        type = 'Text',
        func = nil,
    },
    metabox_html = {
        inherit = false,
         field = 'metabox_html',
         type = 'Text',
         type = 'Text',
         func = nil,
         func = nil,
     },
     },
     implicit_stat_text = {
     implicit_stat_text = {
        inherit = false,
         field = 'implicit_stat_text',
         field = 'implicit_stat_text',
         type = 'Text',
         type = 'Text',
         func = function(tpl_args, frame)
         func = h.proc.factory.stat_text{type='implicit'},
            tpl_args.implicit_stat_text = h.process_mod_stats(tpl_args, {is_implicit=true})
        end,
     },
     },
     explicit_stat_text = {
     explicit_stat_text = {
        inherit = false,
         field = 'explicit_stat_text',
         field = 'explicit_stat_text',
         type = 'Text',
         type = 'Text',
         func = function(tpl_args, frame)
         func = h.proc.factory.stat_text{type='explicit'},
            tpl_args.explicit_stat_text = h.process_mod_stats(tpl_args, {is_implicit=false})
           
            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 = i18n.tooltips.corrupted
                else
                    tpl_args.explicit_stat_text = (tpl_args.explicit_stat_text or '') .. '<br>' .. i18n.tooltips.corrupted
                end
            end
        end,
     },
     },
     stat_text = {
     stat_text = {
        inherit = false,
         field = 'stat_text',
         field = 'stat_text',
         type = 'Text',
         type = 'Text',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             local sep = ''
             if tpl_args.implicit_stat_text or tpl_args.explicit_stat_text then
            if tpl_args.implicit_stat_text and tpl_args.explicit_stat_text then
                local stats = {}
                 sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
                table.insert(stats, tpl_args.implicit_stat_text) -- No-op if value is nil
                table.insert(stats, tpl_args.explicit_stat_text)
                if tpl_args.is_corrupted then
                    table.insert(stats, m_util.html.poe_color('corrupted', i18n.tooltips.corrupted))
                 elseif tpl_args.is_mirrored then
                    table.insert(stats, i18n.tooltips.mirrored)
                elseif tpl_args.is_unmodifiable then
                    table.insert(stats, i18n.tooltips.unmodifiable)
                end
                local sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
                value = table.concat(stats, sep)
            end
            return value
        end,
    },
    class_id = {
        inherit = false,
        field = 'class_id',
        type = 'String',
        func = function (tpl_args, value)
            if value == nil then
                error(string.format(i18n.errors.generic_required_parameter, 'class_id'))
             end
             end
             local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
             if not m_util.table.has_key(m_game.constants.item.classes, value) or m_game.constants.item.classes[value].disabled then
           
                error(string.format(i18n.errors.invalid_class_id, tostring(value)))
            if string.len(text) > 0 then
                tpl_args.stat_text = text
             end
             end
            return value
         end,
         end,
     },
     },
     class = {
     class = {
         no_copy = true,
         inherit = false,
         field = 'class',
         field = 'class',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame)
         func = function (tpl_args, value)
             tpl_args.class = m_game.constants.item.classes[tpl_args.class_id]['long_upper']
             local class = m_game.constants.item.classes[tpl_args.class_id].long_upper
             -- Avoids errors with empty item class names later on
             -- Avoids errors with empty item class names later on
             if tpl_args.class == '' then
             if class == '' then
                 tpl_args.class = nil
                 class = nil
             end
             end
            return class
         end,
         end,
        deprecated = true,
     },
     },
     -- processed in build_item_classes
     -- generic
     class_id = {
     is_in_game = {
         no_copy = true,
         inherit = false,
         field = 'class_id',
         field = 'is_in_game',
         type = 'String',
         type = 'Boolean',
         func = function (tpl_args, frame)
         func = h.proc.boolean,
            if m_game.constants.item.classes[tpl_args.class_id] == nil then
         default = true,
                error(string.format(i18n.errors.invalid_class_id, tostring(tpl_args.class_id)))
            end
         end
     },
     },
    -- generic
     rarity_id = {
     rarity_id = {
         no_copy = true,
         inherit = false,
         field = 'rarity_id',
         field = 'rarity_id',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame)
         func = h.proc.factory.value{
             if m_game.constants.rarities[tpl_args.rarity_id] == nil then
             validate = m_util.validate.factory.in_table_keys{
                 error(string.format(i18n.errors.invalid_rarity_id, tostring(tpl_args.rarity_id)))
                tbl = m_game.constants.rarities,
             end
                 errmsg = i18n.errors.invalid_rarity_id,
         end
             },
         },
     },
     },
     rarity = {
     rarity = {
         no_copy = true,
         inherit = false,
         field = 'rarity',
         field = 'rarity',
         type = 'String',
         type = 'String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             tpl_args.rarity = m_game.constants.rarities[tpl_args.rarity_id]['long_upper']
             return m_game.constants.rarities[tpl_args.rarity_id].long_upper
         end
         end,
        deprecated = true,
     },
     },
     name = {
     name = {
         no_copy = true,
         inherit = false,
         field = 'name',
         field = 'name',
         type = 'String',
         type = 'String',
Line 281: Line 557:
         field = 'size_x',
         field = 'size_x',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('size_x'),
         func = h.proc.size,
        default = 1,
     },
     },
     size_y = {
     size_y = {
         field = 'size_y',
         field = 'size_y',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('size_y'),
         func = h.proc.size,
        default = 1,
     },
     },
     drop_rarities_ids = {
     drop_rarities_ids = {
         no_copy = true,
         inherit = false,
         field = 'drop_rarity_ids',
         field = 'drop_rarity_ids',
         type = 'List (,) of Text',
         type = 'List (,) of String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
            tpl_args.drop_rarities_ids = nil
             -- Drop rarities only matter for base items
            if true then return end
             if tpl_args._flags.is_derived then
             -- Drop rarities only matter for base items.
                 return nil -- Use default
             if tpl_args.rarity_id ~= 'normal' then
                 return
             end
             end
              
             return m_util.cast.table(value, {
            if tpl_args.drop_rarities_ids == nil then
                 callback = m_util.validate.factory.in_table_keys{
                tpl_args.drop_rarities_ids = {}
                    tbl = m_game.constants.rarities,
                return
                     errmsg = i18n.errors.invalid_rarity_id,
            end
                    errlvl = 4,
                  
                },
            tpl_args.drop_rarities_ids = m_util.string.split(tpl_args.drop_rarities_ids, ',%s*')
            })
            for _, rarity_id in ipairs(tpl_args.drop_rarities_ids) do
         end,
                if m_game.constants.rarities[rarity_id] == nil then
         default = {},
                     error(string.format(i18n.errors.invalid_rarity_id, tostring(rarity_id)))
                end
            end
        end,
    },
    drop_rarities = {
         no_copy = true,
        field = nil,
         type = 'List (,) of Text',
        func = function(tpl_args, frame)
            tpl_args.drop_rarities = nil
            if true then return end
            -- Drop rarities only matter for base items.
            if tpl_args.rarity_id ~= 'normal' then
                return
            end
           
            local rarities = {}
            for _, rarity_id in ipairs(tpl_args.drop_rarities_ids) do
                rarities[#rarities+1] = m_game.constants.rarities[rarity_id].full
            end
            tpl_args.drop_rarities = rarities
        end,
     },
     },
     drop_enabled = {
     drop_enabled = {
         no_copy = true,
         inherit = false,
         field = 'drop_enabled',
         field = 'drop_enabled',
         type = 'Boolean',
         type = 'Boolean',
         func = m_util.cast.factory.boolean('drop_enabled'),
         func = function (tpl_args, value)
         default = true,
            if value == nil then
                return nil -- Use default
            end
            return tpl_args.is_in_game and m_util.cast.boolean(value)
        end,
         default = function (tpl_args)
            return tpl_args.is_in_game
        end,
     },
     },
     drop_level = {
     drop_level = {
         no_copy = true,
         inherit = false,
         field = 'drop_level',
         field = 'drop_level',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('drop_level'),
         func = h.proc.number,
     },
     },
     drop_level_maximum = {
     drop_level_maximum = {
         no_copy = true,
         inherit = false,
         field = 'drop_level_maximum',
         field = 'drop_level_maximum',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('drop_level_maximum'),
         func = h.proc.number,
     },
     },
     drop_leagues = {
     acquisition_tags = {
         no_copy = true,
         inherit = false,
         field = 'drop_leagues',
         field = 'acquisition_tags',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = m_util.cast.factory.assoc_table('drop_leagues', {tbl=m_game.constants.leagues, errmsg=i18n.errors.invalid_league}),
         func = h.proc.factory.list{
            callback = m_util.validate.factory.in_table_keys{
                tbl = cfg.acquisition_tags,
                errmsg = i18n.errors.invalid_acquisition_tag,
                errlvl = 4,
            },
        },
        default = {},
     },
     },
     drop_areas = {
     drop_areas = {
         no_copy = true,
         inherit = false,
         field = 'drop_areas',
         field = 'drop_areas',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             if tpl_args.drop_areas ~= nil then
             value = m_util.cast.table(value)
                tpl_args.drop_areas = m_util.string.split(tpl_args.drop_areas, ',%s*')
            if type(value) == 'table' and #value > 0 then
                 tpl_args.drop_areas_data = m_cargo.array_query{
                 tpl_args._drop_areas_data = m_cargo.array_query{
                     tables={'areas'},
                     tables = {'areas'},
                     fields={'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
                     fields = {
                     id_field='areas.id',
                        'areas._pageName=_pageName',
                     id_array=tpl_args.drop_areas,
                        'areas.id=id',
                     query={limit=5000},
                        'areas.name=name',
                        'areas.main_page=main_page',
                        'areas.is_legacy_map_area=is_legacy_map_area'
                    },
                     id_field = 'areas.id',
                     id_array = value,
                     query = {limit=5000},
                 }
                 }
                if tpl_args._drop_areas_data then
                    tpl_args._legacy_drop_areas = tpl_args._legacy_drop_areas or {}
                    for _, v in ipairs(tpl_args._drop_areas_data) do
                        if m_util.cast.boolean(v.is_legacy_map_area) then
                            tpl_args._flags.has_legacy_drop_areas = true
                            table.insert(tpl_args._legacy_drop_areas, v.id)
                        end
                    end
                end
             end
             end
              
             return value
            -- find areas based on item tags for atlas bases
        end,
            local query_data
        default = {},
            for _, tag in ipairs(tpl_args.tags or {}) do
     },
                query_data = nil
                if string.match(tag, '[%w_]*atlas[%w_]*') ~= nil and tag ~= 'atlas_base_type' then
                    query_data = m_cargo.query(
                        {'atlas_maps', 'maps', 'areas', 'atlas_base_item_types'},
                        {'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
                        {
                            join='atlas_maps._pageID=maps._pageID, maps.area_id=areas.id, atlas_maps.region_id=atlas_base_item_types.region_id',
                            where=string.format([[
                                    atlas_base_item_types.tag = "%s"
                                    AND atlas_base_item_types.weight > 0
                                    AND (
                                        atlas_maps.map_tier0 >= tier_min AND atlas_maps.map_tier0 <= atlas_base_item_types.tier_max
                                        OR atlas_maps.map_tier1 >= tier_min AND atlas_maps.map_tier1 <= atlas_base_item_types.tier_max
                                        OR atlas_maps.map_tier2 >= tier_min AND atlas_maps.map_tier2 <= atlas_base_item_types.tier_max
                                        OR atlas_maps.map_tier3 >= tier_min AND atlas_maps.map_tier3 <= atlas_base_item_types.tier_max
                                        OR atlas_maps.map_tier4 >= tier_min AND atlas_maps.map_tier4 <= atlas_base_item_types.tier_max
                                    )]],
                                tag),
                            groupBy='areas.id',
                        }
                    )
                end
               
                if query_data ~= nil then
                    -- in case no manual drop areas have been set
                    if tpl_args.drop_areas == nil then
                        tpl_args.drop_areas = {}
                        tpl_args.drop_areas_data = {}
                    end
                    local drop_areas_assoc = {}
                    for _, id in ipairs(tpl_args.drop_areas) do
                        drop_areas_assoc[id] = true
                    end
                   
                    local duplicates = {}
                   
                    for _, row in ipairs(query_data) do
                        if drop_areas_assoc[row['areas.id']] == nil then
                            tpl_args.drop_areas[#tpl_args.drop_areas+1] = row['areas.id']
                            tpl_args.drop_areas_data[#tpl_args.drop_areas_data+1] = row
                        else
                            duplicates[#duplicates+1] = row['areas.id']
                        end
                    end
                   
                    if #duplicates > 0 then
                        tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors.duplicate_area_id_from_query, table.concat(duplicates, ', '))
                        tpl_args._flags.duplicate_query_area_ids = true
                    end
                end
            end
        end,
     },
     drop_monsters = {
     drop_monsters = {
         no_copy = true,
         inherit = false,
         field = 'drop_monsters',
         field = 'drop_monsters',
         type = 'List (,) of Text',
         type = 'List (,) of Text',
         func = function (tpl_args, frame)
         func = h.proc.list,
            if tpl_args.drop_monsters ~= nil then
         default = {},
                tpl_args.drop_monsters = m_util.string.split(tpl_args.drop_monsters, ',%s*')
            end
         end,
     },
     },
     drop_text = {
     drop_text = {
         no_copy = true,
         inherit = false,
         field = 'drop_text',
         field = 'drop_text',
         type = 'Text',
         type = 'Text',
         func = h.factory.cast_text('drop_text'),
         func = h.proc.text,
     },
     },
     required_level = {
     required_level = {
         field = 'required_level_base',
         field = 'required_level_base',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('required_level'),
         func = h.proc.number,
         default = 1,
         default = 1,
     },
     },
Line 454: Line 680:
         field = 'required_level',
         field = 'required_level',
         type = 'Integer',
         type = 'Integer',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             tpl_args.required_level_final = tpl_args.required_level
             value = tpl_args.required_level
            if value < cfg.base_item_required_level_threshold and cfg.base_item_required_level_threshold_classes[tpl_args.class_id] then
                value = 1
            end
            return value
         end,
         end,
         default = 1,
         default = 1,
Line 462: Line 692:
         field = 'required_dexterity',
         field = 'required_dexterity',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('required_dexterity'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 468: Line 698:
         field = 'required_strength',
         field = 'required_strength',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('required_strength'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 474: Line 704:
         field = 'required_intelligence',
         field = 'required_intelligence',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('required_intelligence'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
     inventory_icon = {
     inventory_icon = {
         no_copy = true,
         inherit = false,
         field = 'inventory_icon',
         field = 'inventory_icon',
         type = 'String',
         type = 'String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             if tpl_args.class_id == 'DivinationCard' then
             if not value then
                 tpl_args.inventory_icon = tpl_args.inventory_icon or 'Divination card'
                -- Certain types of items have default inventory icons
                if i18n.default_inventory_icons[tpl_args.class_id] then
                    value = i18n.default_inventory_icons[tpl_args.class_id]
                 else
                    for k, v in pairs(tpl_args._flags) do
                        value = v and i18n.default_inventory_icons[k]
                    end
                end
             end
             end
             tpl_args.inventory_icon_id = tpl_args.inventory_icon or tpl_args.name
             tpl_args.inventory_icon_id = value or tpl_args.name
             tpl_args.inventory_icon = string.format(i18n.inventory_icon, tpl_args.inventory_icon_id)  
             return string.format(i18n.files.inventory_icon, tpl_args.inventory_icon_id)  
         end,
         end,
     },
     },
     -- note: this must be called after inventory item to work correctly as it depends on tpl_args.inventory_icon_id being set
     -- note: this must be called after inventory_icon 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,
         inherit = false,
         field = 'alternate_art_inventory_icons',
         field = 'alternate_art_inventory_icons',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             local icons = {}
             return m_util.cast.table(value, {
            if tpl_args.alternate_art_inventory_icons ~= nil then
                 callback = function (value)
                local names = m_util.string.split(tpl_args.alternate_art_inventory_icons, ',%s*')
                     return string.format(i18n.files.inventory_icon, string.format('%s %s', tpl_args.inventory_icon_id, tostring(value)))
                  
                 end,
                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,
         end,
         default = function (tpl_args, frame) return {} end,
         default = {},
     },
     },
     cannot_be_traded_or_modified = {
     cannot_be_traded_or_modified = {
         no_copy = true,
         inherit = false,
         field = 'cannot_be_traded_or_modified',
         field = 'cannot_be_traded_or_modified',
         type = 'Boolean',
         type = 'Boolean',
         func = m_util.cast.factory.boolean('cannot_be_traded_or_modified'),
         func = h.proc.boolean,
         default = false,
         default = false,
     },
     },
     help_text = {
     help_text = {
        debug_ignore_nil = true,
         field = 'help_text',
         field = 'help_text',
         type = 'Text',
         type = 'Text',
         func = h.factory.cast_text('help_text'),
         func = h.proc.text,
     },
     },
     flavour_text = {
    is_account_bound = {
         no_copy = true,
        inherit = false,
        field = 'is_account_bound',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
     flavour_text = {
         inherit = false,
         field = 'flavour_text',
         field = 'flavour_text',
         type = 'Text',
         type = 'Text',
         func = h.factory.cast_text('flavour_text'),
         func = h.proc.factory.value{
            cast = function (value)
                value = m_util.cast.text(value)
                -- Parse harbinger glyphs
                value = string.gsub(value, '<<([Hh][Bb][Gg]%w+)>>', '<span class="glyph %1"></span>')
                return value
            end,
        },
     },
     },
     flavour_text_id = {
     flavour_text_id = {
         no_copy = true,
         inherit = false,
         field = 'flavour_text_id',
         field = 'flavour_text_id',
         type = 'String',
         type = 'String',
Line 535: Line 781:
         field = 'tags',
         field = 'tags',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = m_util.cast.factory.assoc_table('tags', {
         func = h.proc.factory.list{
            tbl = m_game.constants.tags,
            callback = m_util.validate.factory.in_table_keys{
            errmsg = i18n.errors.invalid_tag,
                tbl = m_game.constants.tags,
         }),
                errmsg = i18n.errors.invalid_tag,
                errlvl = 4,
            },
        },
         default = {},
     },
     },
     metadata_id = {
     metadata_id = {
         no_copy = true,
         inherit = false,
         field = 'metadata_id',
         field = 'metadata_id',
         type = 'String',
         type = 'String',
        --type = 'String(unique; size=200)',
         func = function (tpl_args, value)
         func = function(tpl_args, frame)
             if value == nil then
             if tpl_args.metadata_id == nil then
                 return nil
                 return
             end
             end
             local results = m_cargo.query(
             -- Unless we're in testing mode, validate that metadata_id is unique
                {'items'},
            if not tpl_args.test then
                {'items._pageName'},
                local results = m_cargo.query(
                {
                    {'items'},
                    where=string.format('items.metadata_id="%s" AND items._pageName != "%s"', tpl_args.metadata_id, mw.title.getCurrentTitle().fullText)
                    {'items._pageName'},
                }
                    {
            )
                        where = string.format(
            if #results > 0 and tpl_args.debug_id == nil and not tpl_args.debug then
                            'items.metadata_id = "%s" AND items._pageName != "%s"',
                error(string.format(i18n.errors.duplicate_metadata, tpl_args.metadata_id, results[1]['items._pageName']))
                            value,
                            m_cargo.addslashes(mw.title.getCurrentTitle().prefixedText)
                        )
                    }
                )
                if #results > 0 then
                    error(string.format(i18n.errors.duplicate_metadata, value, results[1]['items._pageName']))
                end
             end
             end
            return value
         end,
         end,
     },
     },
     influences = {
     influences = {
         no_copy = true,
         inherit = false,
         field = 'influences',
         field = 'influences',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = m_util.cast.factory.assoc_table('influences', {
         func = h.proc.factory.list{
            tbl = m_game.constants.influences,
            callback = m_util.validate.factory.in_table_keys{
            errmsg = i18n.errors.invalid_influence,
                tbl = m_game.constants.influences,
         }),
                errmsg = i18n.errors.invalid_influence,
                errlvl = 4,
            },
        },
         default = {},
     },
     },
     is_fractured = {
     is_fractured = {
         no_copy = true,
         inherit = false,
         field = 'is_fractured',
         field = 'is_fractured',
         type = 'Boolean',
         type = 'Boolean',
         func = m_util.cast.factory.boolean('is_fractured'),
         func = h.proc.boolean,
         default = false,
         default = false,
     },
     },
     is_synthesised = {
     is_synthesised = {
         no_copy = true,
         inherit = false,
         field = 'is_synthesised',
         field = 'is_synthesised',
         type = 'Boolean',
         type = 'Boolean',
         func = m_util.cast.factory.boolean('is_synthesised'),
         func = h.proc.boolean,
        default = false,
    },
    is_searing_exarch_item = {
        inherit = false,
        field = 'is_searing_exarch_item',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    is_eater_of_worlds_item = {
        inherit = false,
        field = 'is_eater_of_worlds_item',
        type = 'Boolean',
        func = h.proc.boolean,
         default = false,
         default = false,
     },
     },
     is_veiled = {
     is_veiled = {
         no_copy = true,
         inherit = false,
         field = 'is_veiled',
         field = 'is_veiled',
         type = 'Boolean',
         type = 'Boolean',
         func = m_util.cast.factory.boolean('is_veiled'),
         func = h.proc.boolean,
         default = false,
         default = false,
     },
     },
     is_replica = {
     is_replica = {
         no_copy = true,
         inherit = false,
         field = 'is_replica',
         field = 'is_replica',
         type = 'Boolean',
         type = 'Boolean',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             m_util.cast.factory.boolean('is_replica')(tpl_args, frame)
             value = m_util.cast.boolean(value)
             if tpl_args.is_replica == true and tpl_args.rarity_id ~= 'unique' then
             if value == true and tpl_args.rarity_id ~= 'unique' then
                 error(string.format(i18n.errors.non_unique_flag, 'is_replica'))
                 error(string.format(i18n.errors.non_unique_flag, 'is_replica'))
             end
             end
            return value
         end,
         end,
         default = false,
         default = false,
     },
     },
     is_corrupted = {
     is_corrupted = {
         no_copy = true,
         inherit = false,
         field = 'is_corrupted',
         field = 'is_corrupted',
         type = 'Boolean',
         type = 'Boolean',
         func = m_util.cast.factory.boolean('is_corrupted'),
         func = h.proc.boolean,
         default = false,
         default = false,
     },
     },
     is_relic = {
     is_mirrored = {
         no_copy = true,
         inherit = false,
         field = 'is_relic',
         field = nil,
         type = 'Boolean',
         type = nil,
         func = function(tpl_args, frame)
         func = h.proc.boolean,
            m_util.cast.factory.boolean('is_relic')(tpl_args, frame)
            if tpl_args.is_relic == true and tpl_args.rarity_id ~= 'unique' then
                error(string.format(i18n.errors.non_unique_flag, 'is_relic'))
            end
        end,
         default = false,
         default = false,
     },
     },
     is_fated = {
     is_unmodifiable = {
         no_copy = true,
         inherit = false,
         field = 'is_fated',
         field = 'is_unmodifiable',
         type = 'Boolean',
         type = 'Boolean',
         func = function(tpl_args, frame)
         func = h.proc.boolean,
            m_util.cast.factory.boolean('is_fated')(tpl_args, frame)
            if tpl_args.is_fated == true and tpl_args.rarity_id ~= 'unique' then
                error(string.format(i18n.errors.non_unique_flag, 'is_fated'))
            end
        end,
         default = false,
         default = false,
     },
     },
    is_prophecy = {
     is_drop_restricted = {
        no_copy = true,
         inherit = false,
        field = nil,
        type = nil,
        func = function(tpl_args, frame)
            tpl_args._flags.is_prophecy = (tpl_args.metadata_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' or tpl_args.base_item == 'Prophecy' or tpl_args.base_item_page == 'Prophecy' or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy')
        end
    },
    is_blight_item = {
        no_copy = true,
        field = nil,
        type = nil,
        func = function(tpl_args, frame)
            tpl_args._flags.is_blight_item = (tpl_args.blight_item_tier ~= nil)
        end
    },
     is_drop_restricted = {
         no_copy  = true,
         field = 'is_drop_restricted',
         field = 'is_drop_restricted',
         type = 'Boolean',
         type = 'Boolean',
         func = m_util.cast.factory.boolean('is_drop_restricted'),
         func = h.proc.boolean,
         default = function(tpl_args, frame)
         default = function (tpl_args)
             -- Generally items that are obtained only from specific monsters can't be obtained via cards or other means unless specifically specified
             -- Divination cards have drop restrictions by design.
             for _, var in ipairs({'is_talisman', 'is_fated', 'is_relic', 'drop_monsters'}) do
            if tpl_args.class_id == 'DivinationCard' then
                 if tpl_args[var] then
                return true
            end
             for _, key in ipairs({'is_replica', '_drop_areas_data', 'drop_monsters'}) do
                -- arg must be truthy and NOT an empty table
                 if tpl_args[key] and not (type(tpl_args[key]) == 'table' and #tpl_args[key] == 0) then
                     return true
                     return true
                 end
                 end
             end
             end
             for _, flag in ipairs({'is_prophecy', 'is_blight_item'}) do
             local flags = {
                'is_talisman',
                'is_essence',
                'is_blight_item',
                'is_fossil',
                'is_tattoo',
                'is_delirium_orb',
                'is_catalyst',
            }
            for _, flag in ipairs(flags) do
                 if tpl_args._flags[flag] then
                 if tpl_args._flags[flag] then
                     return true
                     return true
Line 671: Line 934:
     },
     },
     purchase_costs = {
     purchase_costs = {
         func = function(tpl_args, frame)
        field = nil,
        type = nil,
         func = function (tpl_args, value)
             local purchase_costs = {}
             local purchase_costs = {}
             for _, rarity_id in ipairs(m_game.constants.rarity_order) do
             for _, rarity_id in ipairs(m_game.constants.rarity_order) do
Line 687: Line 952:
                         rtbl[#rtbl+1] = values
                         rtbl[#rtbl+1] = values
                         i = i + 1
                         i = i + 1
                          
                         table.insert(tpl_args._store_data, {
                        tpl_args._subobjects[#tpl_args._subobjects+1] = {
                             _table = 'item_purchase_costs',
                             _table = 'item_purchase_costs',
                             amount = values.amount,
                             amount = values.amount,
                             name = values.name,
                             name = values.name,
                             rarity = values.rarity,
                             rarity = values.rarity,
                         }
                         })
                     else
                     else
                         i = -1
                         i = -1
                     end
                     end
                 end
                 end
               
                 purchase_costs[rarity_id] = rtbl
                 purchase_costs[rarity_id] = rtbl
             end
             end
              
             return purchase_costs
            tpl_args.purchase_costs = purchase_costs
         end,
         end,
         func_fetch = function(tpl_args, frame)
         func_fetch = function (tpl_args)
             if tpl_args.rarity_id ~= 'unique' then
             if tpl_args.rarity_id ~= 'unique' then
                 return
                 return
Line 726: Line 988:
                 local datavar = tpl_args.purchase_costs[string.lower(values.rarity)]
                 local datavar = tpl_args.purchase_costs[string.lower(values.rarity)]
                 datavar[#datavar+1] = values
                 datavar[#datavar+1] = values
                  
                 table.insert(tpl_args._store_data, {
                tpl_args._subobjects[#tpl_args._subobjects+1] = {
                     _table = 'item_purchase_costs',
                     _table = 'item_purchase_costs',
                     amount = values.amount,
                     amount = values.amount,
                     name = values.name,
                     name = values.name,
                     rarity = values.rarity,
                     rarity = values.rarity,
                 }
                 })
            end
        end,
    },
    is_sellable = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            if value == nil then
                return nil -- Use default
             end
             end
            return tpl_args.is_in_game and m_util.cast.boolean(value)
        end,
        default = function (tpl_args)
            return tpl_args.is_in_game
         end,
         end,
     },
     },
     sell_prices_override = {
     sell_prices_override = {
         no_copy = true,
         inherit = false,
         func = function(tpl_args, frame)
        field = nil,
        type = nil,
         func = function (tpl_args, value)
             -- these variables are also used by mods when setting automatic sell prices
             -- these variables are also used by mods when setting automatic sell prices
             tpl_args.sell_prices = {}
             tpl_args.sell_prices = {}
             tpl_args.sell_price_order = {}
             tpl_args.sell_price_order = {}
              
             if not tpl_args.is_sellable then
              
                return nil
             end
             local name
             local name
             local amount
             local amount
Line 755: Line 1,033:
                     tpl_args.sell_price_order[#tpl_args.sell_price_order+1] = name
                     tpl_args.sell_price_order[#tpl_args.sell_price_order+1] = name
                     tpl_args.sell_prices[name] = amount
                     tpl_args.sell_prices[name] = amount
                     tpl_args._subobjects[#tpl_args._subobjects+1] = {
                     table.insert(tpl_args._store_data, {
                         _table = 'item_sell_prices',
                         _table = 'item_sell_prices',
                         amount = amount,
                         amount = amount,
                         name = name,
                         name = name,
                     }
                     })
                 end
                 end
             until name == nil or amount == nil  
             until name == nil or amount == nil  
           
             -- if sell prices are set, the override is active
             -- if sell prices are set, the override is active
             for _, _ in pairs(tpl_args.sell_prices) do
             for _, _ in pairs(tpl_args.sell_prices) do
Line 768: Line 1,045:
                 break
                 break
             end
             end
            return value
         end,
         end,
     },
     },
Line 773: Line 1,051:
     -- specific section
     -- specific section
     --
     --
   
 
     -- Most item classes
     -- Most item classes
     quality = {
     quality = {
         no_copy = true,
         inherit = false,
         field = 'quality',
         field = 'quality',
         type = 'Integer',
         type = 'Integer',
         -- Can be set manually, but default to Q20 for unique weapons/body armours
         -- Must copy to stat for the stat adjustments to work properly
        -- Also must copy to stat for the stat adjustments to work properly
         func = function (tpl_args, value)
         func = function(tpl_args, frame)
             local value = tonumber(value)
             local quality = tonumber(tpl_args.quality)
             if value then
             --
                core.add_stat(tpl_args, 'quality', value)
            if quality == nil then
                 if tpl_args.class_id == 'UtilityFlask' then
                 if tpl_args.rarity_id ~= 'unique' then
                     core.add_stat(tpl_args, 'quality_flask_duration', value)
                     quality = 0
                 elseif tpl_args.class_id == 'Map' then
                 elseif cfg.class_groups.weapons.keys[tpl_args.class_id] or cfg.class_groups.armor.keys[tpl_args.class_id] then
                     -- quality is added to quantity for maps
                     quality = 20
                     core.add_stat(tpl_args, 'map_item_drop_quantity_+%', value)
                else
                     quality = 0
                 end
                 end
             end
             end
              
             return value
            tpl_args.quality = quality
           
            local stat = {
                min = quality,
                max = quality,
                avg = quality,
            }
           
            h.stats_update(tpl_args, 'quality', stat, nil, '_stats')
           
            if tpl_args.class_id == 'UtilityFlask' or tpl_args.class_id == 'UtilityFlaskCritical' then
                h.stats_update(tpl_args, 'quality_flask_duration', stat, nil, '_stats')
            -- quality is added to quantity for maps
            elseif tpl_args.class_id == 'Map' then
                h.stats_update(tpl_args, 'map_item_drop_quantity_+%', stat, nil, '_stats')
            end
         end,
         end,
        default = 0,
     },
     },
     -- amulets
     -- amulets
Line 816: Line 1,077:
         field = 'is_talisman',
         field = 'is_talisman',
         type = 'Boolean',
         type = 'Boolean',
         func = m_util.cast.factory.boolean('is_talisman'),
         func = function (tpl_args, value)
            value = m_util.cast.boolean(value)
            tpl_args._flags.is_talisman = value
            return value
        end,
         default = false,
         default = false,
     },
     },
   
     talisman_tier = {
     talisman_tier = {
         field = 'talisman_tier',
         field = 'talisman_tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('talisman_tier'),
         func = h.proc.number,
     },
     },
   
     -- flasks
     -- flasks
     charges_max = {
     charges_max = {
         field = 'charges_max',
         field = 'charges_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('charges_max'),
         func = h.proc.number,
     },
     },
     charges_per_use = {
     charges_per_use = {
         field = 'charges_per_use',
         field = 'charges_per_use',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('charges_per_use'),
         func = h.proc.number,
     },
     },
     flask_mana = {
     flask_mana = {
         field = 'mana',
         field = 'mana',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('flask_mana'),
         func = h.proc.number,
     },
     },
     flask_life = {
     flask_life = {
         field = 'life',
         field = 'life',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('flask_life'),
         func = h.proc.number,
     },
     },
     flask_duration = {
     flask_duration = {
         field = 'duration',
         field = 'duration',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('flask_duration'),
         func = h.proc.number,
     },
     },
     buff_id = {
     buff_id = {
Line 860: Line 1,123:
         field = 'buff_values',
         field = 'buff_values',
         type = 'List (,) of Integer',
         type = 'List (,) of Integer',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             local values = {}
             local values = {}
             local i = 0
             local i = 0
Line 872: Line 1,135:
             -- needed so the values copyied from unique item base isn't overriden
             -- needed so the values copyied from unique item base isn't overriden
             if #values >= 1 then
             if #values >= 1 then
                 tpl_args.buff_values = values
                 value = values
             end
             end
            return value
         end,
         end,
         func_copy = function(tpl_args, frame)
         func_copy = function (tpl_args, value)
             tpl_args.buff_values = m_util.string.split(tpl_args.buff_values, ',%s*')
             tpl_args.buff_values = m_util.string.split(value, ',%s*')
         end,
         end,
         default = function (tpl_args, frame) return {} end,
         default = {},
     },
     },
     buff_stat_text = {
     buff_stat_text = {
Line 888: Line 1,152:
         field = 'icon',
         field = 'icon',
         type = 'String',
         type = 'String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             tpl_args.buff_icon = string.format(i18n.status_icon, tpl_args.name)  
             return string.format(i18n.files.status_icon, tpl_args.name)
         end,
         end,
     },
     },
Line 897: Line 1,161:
         field = 'critical_strike_chance',
         field = 'critical_strike_chance',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('critical_strike_chance'),
         func = h.proc.number,
     },
     },
     attack_speed = {
     attack_speed = {
         field = 'attack_speed',
         field = 'attack_speed',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('attack_speed'),
         func = h.proc.number,
     },
     },
     weapon_range = {
     weapon_range = {
         field = 'weapon_range',
         field = 'weapon_range',
         type = 'Integer',
         type = 'Float',
         func = m_util.cast.factory.number('weapon_range'),
         func = h.proc.number,
     },
     },
     physical_damage_min = {
     physical_damage_min = {
         field = 'physical_damage_min',
         field = 'physical_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('physical_damage_min'),
         func = h.proc.number,
        default = 0,
     },
     },
     physical_damage_max = {
     physical_damage_max = {
         field = 'physical_damage_max',
         field = 'physical_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('physical_damage_max'),
         func = h.proc.number,
        default = 0,
     },
     },
     fire_damage_min = {
     fire_damage_min = {
         field = 'fire_damage_min',
         field = 'fire_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('fire_damage_min'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 928: Line 1,194:
         field = 'fire_damage_max',
         field = 'fire_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('fire_damage_max'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 934: Line 1,200:
         field = 'cold_damage_min',
         field = 'cold_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('cold_damage_min'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 940: Line 1,206:
         field = 'cold_damage_max',
         field = 'cold_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('cold_damage_max'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 946: Line 1,212:
         field = 'lightning_damage_min',
         field = 'lightning_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('lightning_damage_min'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 952: Line 1,218:
         field = 'lightning_damage_max',
         field = 'lightning_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('lightning_damage_max'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 958: Line 1,224:
         field = 'chaos_damage_min',
         field = 'chaos_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('chaos_damage_min'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 964: Line 1,230:
         field = 'chaos_damage_max',
         field = 'chaos_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('chaos_damage_max'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
     -- armor-type stuff
     -- armor-type stuff
     armour = {
     armour = {
        inherit = false,
         field = 'armour',
         field = 'armour',
        type = nil,
        func = nil,
    },
    armour_min = {
        field = 'armour_min',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('armour'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
     energy_shield = {
     armour_max = {
         field = 'energy_shield',
         field = 'armour_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('energy_shield'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
     evasion = {
     evasion = {
        inherit = false,
         field = 'evasion',
         field = 'evasion',
        type = nil,
        func = nil,
    },
    evasion_min = {
        field = 'evasion_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    evasion_max = {
        field = 'evasion_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    energy_shield = {
        inherit = false,
        field = 'energy_shield',
        type = nil,
        func = nil,
    },
    energy_shield_min = {
        field = 'energy_shield_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    energy_shield_max = {
        field = 'energy_shield_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    ward = {
        inherit = false,
        field = 'ward',
        type = nil,
        func = nil,
    },
    ward_min = {
        field = 'ward_min',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('evasion'),
         func = h.proc.number,
        default = 0,
    },
    ward_max = {
        field = 'ward_max',
        type = 'Integer',
        func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 990: Line 1,310:
         field = 'movement_speed',
         field = 'movement_speed',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('movement_speed'),
         func = h.proc.number,
         default = 0,
         default = 0,
     },
     },
Line 997: Line 1,317:
         field = 'block',
         field = 'block',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('block'),
         func = h.proc.number,
     },
     },
     -- skill gem stuff
     -- skill gem stuff
Line 1,003: Line 1,323:
         field = 'gem_description',
         field = 'gem_description',
         type = 'Text',
         type = 'Text',
         func = h.factory.cast_text('gem_description'),
         func = h.proc.text,
     },
     },
     dexterity_percent = {
     dexterity_percent = {
         field = 'dexterity_percent',
         field = 'dexterity_percent',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.percentage('dexterity_percent'),
         func = h.proc.percentage,
     },
     },
     strength_percent = {
     strength_percent = {
         field = 'strength_percent',
         field = 'strength_percent',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.percentage('strength_percent'),
         func = h.proc.percentage,
     },
     },
     intelligence_percent = {
     intelligence_percent = {
         field = 'intelligence_percent',
         field = 'intelligence_percent',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.percentage('intelligence_percent'),
         func = h.proc.percentage,
     },
     },
     primary_attribute = {
     primary_attribute = {
         field = 'primary_attribute',
         field = 'primary_attribute',
         type = 'String',
         type = 'String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             for _, attr in ipairs(m_game.constants.attribute_order) do
             for _, attr in ipairs(m_game.constants.attribute_order) do
                 local val = tpl_args[attr .. '_percent']  
                 local val = tpl_args[attr .. '_percent']  
                 if val and val >= 60 then
                 if val and val >= 60 then
                     tpl_args['primary_attribute'] = attr
                     return attr
                    return
                 end
                 end
             end
             end
             tpl_args['primary_attribute'] = 'none'
             return 'none'
         end,
         end,
     },
     },
Line 1,037: Line 1,356:
         field = 'gem_tags',
         field = 'gem_tags',
         type = 'List (,) of String',
         type = 'List (,) of String',
        -- TODO: default rework
         func = h.proc.factory.list{
         func = function(tpl_args, frame)
             callback = m_util.validate.factory.in_table_keys{
             if tpl_args.gem_tags then
                 tbl = m_game.constants.item.gem_tags_lookup,
                 tpl_args.gem_tags = m_util.string.split(tpl_args.gem_tags, ',%s*')
                errmsg = i18n.errors.invalid_gem_tag,
             end
                errlvl = 4,
         end,
             },
         default = function (tpl_args, frame) return {} end,
         },
         default = {},
    },
    is_vaal_skill_gem = {
        field = 'is_vaal_skill_gem',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    vaal_variant_id = {
        inherit = true,
        field = 'vaal_variant_id',
        type = 'String',
        func = nil,
     },
     },
     -- Support gems only
     -- Support gems only
     support_gem_letter = {
     support_gem_letter = {
         field = 'support_gem_letter',
         field = 'support_gem_letter',
         type = 'String(size=1)',
         type = 'String',
         func = nil,
         func = h.proc.character,
     },
     },
     support_gem_letter_html = {
     support_gem_letter_html = {
         field = 'support_gem_letter_html',
         field = 'support_gem_letter_html',
         type = 'Text',
         type = 'Text',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             if tpl_args.support_gem_letter == nil then
             if tpl_args.support_gem_letter == nil then
                 return
                 return nil
             end
             end
       
             for k, v in pairs(m_game.constants.attributes) do
            -- TODO replace this with a loop possibly
                 local key = string.format('%s_percent', v.long_lower)
            local css_map = {
                 if tpl_args[key] and tpl_args[key] > 50 then
                strength = 'red',
                     value = tostring(
                intelligence = 'blue',
                        mw.html.create('span')
                dexterity = 'green',
                            :attr('class', string.format('support-gem-id-%s', v.color))
            }
                            :wikitext(tpl_args.support_gem_letter)
            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
                     break
                 end
                 end
             end
             end
              
             return value
            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,
         end,
    },
    is_awakened_support_gem = {
        field = 'is_awakened_support_gem',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    awakened_variant_id = {
        field = 'awakened_variant_id',
        type = 'String',
        func = nil,
    },
    --
    -- Jewels
    --
    jewel_limit = {
        inherit = false,
        field = 'jewel_limit',
        type = 'String',
        func = nil,
     },
     },
     --
     --
Line 1,090: Line 1,430:
         field = 'tier',
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('map_tier'),
         func = h.proc.number,
     },
     },
     map_guild_character = {
     map_guild_character = {
         field = 'guild_character',
         field = 'guild_character',
         type = 'String(size=1)',
         type = 'String',
         func = nil,
         func = h.proc.character,
     },
     },
     map_area_id = {
     map_area_id = {
Line 1,105: Line 1,445:
         field = 'area_level',
         field = 'area_level',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('map_area_level'),
         func = h.proc.number,
     },
     },
     unique_map_guild_character = {
     unique_map_guild_character = {
         field = 'unique_guild_character',
         field = 'unique_guild_character',
         type = 'String(size=1)',
         type = 'String',
         func_copy = function(tpl_args, frame)
        func = h.proc.character,
             tpl_args.map_guild_character = tpl_args.unique_map_guild_character
         func_copy = function (tpl_args, value)
             tpl_args.map_guild_character = value
         end,
         end,
        func = nil,
     },
     },
     unique_map_area_id = {
     unique_map_area_id = {
Line 1,119: Line 1,459:
         type = 'String',
         type = 'String',
         func = nil, -- TODO: Validate against a query?
         func = nil, -- TODO: Validate against a query?
         func_copy = function(tpl_args, frame)
         func_copy = function (tpl_args, value)
             tpl_args.map_area_id = tpl_args.unique_map_area_id
             tpl_args.map_area_id = value
         end,
         end,
     },
     },
Line 1,126: Line 1,466:
         field = 'unique_area_level',
         field = 'unique_area_level',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('unique_map_area_level'),
         func = h.proc.number,
         func_copy = function(tpl_args, frame)
         func_copy = function (tpl_args, value)
             tpl_args.map_area_level = tpl_args.unique_map_area_level
             tpl_args.map_area_level = value
         end,
         end,
     },
     },
Line 1,134: Line 1,474:
         field = 'series',
         field = 'series',
         type = 'String',
         type = 'String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             if tpl_args.rarity == 'normal' and tpl_args.map_series == nil then
             if tpl_args.rarity == 'normal' and value == nil then
                 error(string.format(i18n.errors.generic_required_parameter, 'map_series'))
                 error(string.format(i18n.errors.generic_required_parameter, 'map_series'))
             end
             end
            return value
         end,
         end,
     },
     },
     -- atlas info is only for the current map series
     -- atlas info is only for the current map series
     atlas_x = {
     atlas_x = {
        inherit = false,
         field = 'x',
         field = 'x',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_x'),
         func = h.proc.number,
     },
     },
     atlas_y = {
     atlas_y = {
        inherit = false,
         field = 'y',
         field = 'y',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_y'),
         func = h.proc.number,
     },
     },
     atlas_region_id = {
     atlas_region_id = {
        inherit = false,
         field = 'region_id',
         field = 'region_id',
         type = 'String',
         type = 'String',
Line 1,157: Line 1,501:
     },
     },
     atlas_region_minimum = {
     atlas_region_minimum = {
        inherit = false,
         field = 'region_minimum',
         field = 'region_minimum',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_region_minimum'),
         func = h.proc.number,
     },
     },
     atlas_x0 = {
     atlas_x0 = {
        inherit = false,
         field = 'x0',
         field = 'x0',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_x0'),
         func = h.proc.number,
     },
     },
     atlas_x1 = {
     atlas_x1 = {
        inherit = false,
         field = 'x1',
         field = 'x1',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_x1'),
         func = h.proc.number,
     },
     },
     atlas_x2 = {
     atlas_x2 = {
        inherit = false,
         field = 'x2',
         field = 'x2',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_x2'),
         func = h.proc.number,
     },
     },
     atlas_x3 = {
     atlas_x3 = {
        inherit = false,
         field = 'x3',
         field = 'x3',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_x3'),
         func = h.proc.number,
     },
     },
     atlas_x4 = {
     atlas_x4 = {
        inherit = false,
         field = 'x4',
         field = 'x4',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_x4'),
         func = h.proc.number,
     },
     },
     atlas_y0 = {
     atlas_y0 = {
        inherit = false,
         field = 'y0',
         field = 'y0',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_0'),
         func = h.proc.number,
     },
     },
     atlas_y1 = {
     atlas_y1 = {
        inherit = false,
         field = 'y1',
         field = 'y1',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_y1'),
         func = h.proc.number,
     },
     },
     atlas_y2 = {
     atlas_y2 = {
        inherit = false,
         field = 'y2',
         field = 'y2',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_y2'),
         func = h.proc.number,
     },
     },
     atlas_y3 = {
     atlas_y3 = {
        inherit = false,
         field = 'y3',
         field = 'y3',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_y3'),
         func = h.proc.number,
     },
     },
     atlas_y4 = {
     atlas_y4 = {
        inherit = false,
         field = 'y4',
         field = 'y4',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_y4'),
         func = h.proc.number,
     },
     },
     atlas_map_tier0 = {
     atlas_map_tier0 = {
         field = 'map_tier0',
         field = 'map_tier0',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier0'),
         func = h.proc.number,
     },
     },
     atlas_map_tier1 = {
     atlas_map_tier1 = {
         field = 'map_tier1',
         field = 'map_tier1',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier1'),
         func = h.proc.number,
     },
     },
     atlas_map_tier2 = {
     atlas_map_tier2 = {
         field = 'map_tier2',
         field = 'map_tier2',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier2'),
         func = h.proc.number,
     },
     },
     atlas_map_tier3 = {
     atlas_map_tier3 = {
         field = 'map_tier3',
         field = 'map_tier3',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier3'),
         func = h.proc.number,
     },
     },
     atlas_map_tier4 = {
     atlas_map_tier4 = {
         field = 'map_tier4',
         field = 'map_tier4',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier4'),
         func = h.proc.number,
     },
     },
     atlas_connections = {
     atlas_connections = {
        inherit = false,
         field = nil,
         field = nil,
         type = nil,
         type = nil,
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             tpl_args.atlas_connections = {}
             value = {}
           
             local cont = true
             local cont = true
             local i = 1
             local i = 1
Line 1,262: Line 1,617:
                     end
                     end
                      
                      
                     tpl_args.atlas_connections[data.map2] = data
                     value[data.map2] = data
                     table.insert(tpl_args._subobjects, data)
                     table.insert(tpl_args._store_data, data)
                 else
                 else
                     cont = false
                     cont = false
                     if i == 1 then
                     if i == 1 then
                         tpl_args.atlas_connections = nil
                         value = nil
                     end
                     end
                 end
                 end
               
                 i = i + 1
                 i = i + 1
             end
             end
            return value
        end,
    },
    --
    -- Map fragments
    --
    is_scarab = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_scarab = m_util.table.contains(tpl_args.tags, 'scarab')
            return value
         end,
         end,
        default = nil,
     },
     },
     --
     --
     -- Currency-like items
     -- Stackable items
     --
     --
     stack_size = {
     stack_size = {
        inherit = false,
         field = 'stack_size',
         field = 'stack_size',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('stack_size'),
         func = h.proc.number,
     },
     },
     stack_size_currency_tab = {
     stack_size_currency_tab = {
        inherit = false,
         field = 'stack_size_currency_tab',
         field = 'stack_size_currency_tab',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('stack_size_currency_tab'),
         func = h.proc.number,
     },
     },
     description = {
     description = {
        inherit = false,
         field = 'description',
         field = 'description',
         type = 'Text',
         type = 'Text',
         func = h.factory.cast_text('description'),
         func = h.proc.text,
    },
    cosmetic_type = {
        field = 'cosmetic_type',
        type = 'String',
        func = h.factory.cast_text('cosmetic_type'),
     },
     },
     -- for essences
     -- Essences
     is_essence = {
     is_essence = {
        inherit = false,
         field = nil,
         field = nil,
         func = m_util.cast.factory.boolean('is_essence'),
        type = nil,
         func = function (tpl_args, value)
            value = m_util.cast.boolean(value)
            tpl_args._flags.is_essence = value
            return value
        end,
         default = false,
         default = false,
     },
     },
     essence_level_restriction = {
     essence_level_restriction = {
        inherit = false,
         field = 'level_restriction',
         field = 'level_restriction',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('essence_level_restriction'),
         func = h.proc.number,
     },
     },
     essence_level = {
     essence_level = {
        inherit = false,
         field = 'level',
         field = 'level',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('essence_level'),
         func = h.proc.number,
     },
     },
     essence_type = {
     essence_type = {
        inherit = false,
         field = 'type',
         field = 'type',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('essence_type'),
         func = h.proc.number,
     },
     },
     essence_category = {
     essence_category = {
        inherit = false,
         field = 'category',
         field = 'category',
         type = 'String',
         type = 'String',
         func = nil,
         func = nil,
     },
     },
     -- blight crafting items (i.e. oils)
     -- Oils
    is_blight_item = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_blight_item = tpl_args.blight_item_tier ~= nil
            return value
        end,
    },
     blight_item_tier = {
     blight_item_tier = {
        inherit = false,
         field = 'tier',
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('blight_item_tier'),
         func = h.proc.number,
     },
     },
     -- harvest seeds
     -- Fossils
     seed_type_id = {
     is_fossil = {
         field = 'type_id',
        inherit = false,
         type = 'String',
         field = nil,
         type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_fossil = tpl_args.metadata_id and string.find(tpl_args.metadata_id, 'CurrencyDelve', 1, true) ~= nil
            return value
        end,
     },
     },
     seed_type = {
     -- Tattoos
         field = 'type',
    is_tattoo = {
        type = 'String',
         inherit = false,
        func = function (tpl_args, frame)
            if tpl_args.seed_type_id ~= 'none' or tpl_args.seed_type_id ~= nil then
                tpl_args.seed_type = m_game.seed_types[tpl_args.seed_type_id]
            end
        end
    },
    seed_type_html = {
         field = nil,
         field = nil,
         type = nil,
         type = nil,
         func = function (tpl_args, frame)
         func = function (tpl_args, value)
             if tpl_args.seed_type ~= nil then
             tpl_args._flags.is_tattoo = tpl_args.tattoo_target ~= nil
                tpl_args.seed_type_html = m_util.html.poe_color(tpl_args.seed_type_id, tpl_args.seed_type)
             return value
             end
         end,
         end
     },
     },
     seed_effect = {
     tattoo_target = {
         field = 'effect',
        inherit = false,
         type = 'Text',
         field = 'target',
         type = 'String',
        func = nil,
     },
     },
     seed_tier = {
     tattoo_tribe = {
         field = 'tier',
        inherit = false,
         field = 'tribe',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('seed_tier'),
         func = h.proc.number,
    },
    tattoo_limit = {
        inherit = false,
        field = 'tattoo_limit',
        type = 'String',
        func = nil,
     },
     },
     seed_growth_cycles = {
     tattoo_min_adjacent = {
         field = 'growth_cycles',
        inherit = false,
         field = 'min_adjacent',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('seed_growth_cycles'),
         func = h.proc.number,
        default = 0,
     },
     },
     seed_required_nearby_seed_tier = {
     tattoo_max_adjacent = {
         field = 'required_nearby_seed_tier',
        inherit = false,
         field = 'max_adjacent',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('seed_required_nearby_seed_tier'),
         func = h.proc.number,
        default = 0,
     },
     },
     seed_required_nearby_seed_amount = {
     tattoo_skill_id = {
         field = 'required_nearby_seed_amount',
        inherit = false,
         type = 'Integer',
         field = 'skill_id',
         func = m_util.cast.factory.number('seed_required_nearby_seed_amount'),
         type = 'String',
         func = nil,
     },
     },
     seed_consumed_wild_lifeforce_percentage = {
     -- Delirium orbs
         field = 'consumed_wild_lifeforce_percentage',
    is_delirium_orb = {
         type = 'Integer',
        inherit = false,
         func = m_util.cast.factory.number('seed_consumed_wild_lifeforce_percentage'),
         field = nil,
         default = 0,
         type = nil,
         func = function (tpl_args, value)
            tpl_args._flags.is_delirium_orb = tpl_args.metadata_id and string.find(tpl_args.metadata_id, 'CurrencyAfflictionOrb', 1, true) ~= nil
            return value
         end,
     },
     },
     seed_consumed_vivid_lifeforce_percentage = {
     -- Catalysts
         field = 'consumed_vivid_lifeforce_percentage',
    is_catalyst = {
         type = 'Integer',
        inherit = false,
         func = m_util.cast.factory.number('seed_consumed_vivid_lifeforce_percentage'),
         field = nil,
         default = 0,
         type = nil,
         func = function (tpl_args, value)
            tpl_args._flags.is_catalyst = m_util.table.contains(tpl_args.tags, 'catalyst')
            return value
         end,
     },
     },
     seed_consumed_primal_lifeforce_percentage = {
     -- Cosmetic items
         field = 'consumed_primal_lifeforce_percentage',
    cosmetic_type = {
         type = 'Integer',
        inherit = false,
         func = m_util.cast.factory.number('seed_consumed_primal_lifeforce_percentage'),
         field = 'cosmetic_type',
         default = 0,
         type = 'String',
         func = h.proc.factory.value{
            validate = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.item.cosmetic_item_types,
                errmsg = i18n.errors.invalid_cosmetic_type,
            },
         },
     },
     },
     seed_granted_craft_option_ids = {
     cosmetic_theme = {
         field = 'granted_craft_option_ids',
        inherit = false,
        field = 'theme',
        type = 'String',
        func = nil,
    },
    cosmetic_target = {
        inherit = false,
         field = 'target',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = m_util.cast.factory.number('seed_grandted_craft_option_ids'),
         func = h.proc.list,
         default = 0,
         default = {},
     },
     },
     --
     --
     -- harvest planet boosters
     -- Harvest seeds
     --
     --
     plant_booster_radius = {
     seed_type_id = {
         field = 'radius',
        inherit = false,
         type = 'Integer',
         field = 'type_id',
         func = m_util.cast.factory.number('plant_booster_radius'),
         type = 'String',
         func = nil,
     },
     },
     plant_booster_lifeforce = {
     seed_type = {
         field = 'lifeforce',
        inherit = false,
         type = 'Integer',
         field = 'type',
         func = m_util.cast.factory.number('plant_booster_lifeforce'),
         type = 'String',
         func = function (tpl_args, value)
            if tpl_args.seed_type_id ~= 'none' or tpl_args.seed_type_id ~= nil then
                value = m_game.seed_types[tpl_args.seed_type_id]
            end
            return value
        end,
     },
     },
     plant_booster_additional_crafting_options = {
     seed_type_html = {
         field = 'additional_crafting_options',
        inherit = false,
         type = 'Integer',
         field = nil,
         func = m_util.cast.factory.number('plant_booster_additional_crafting_options'),
         type = nil,
         func = function (tpl_args, value)
            if tpl_args.seed_type ~= nil then
                value = m_util.html.poe_color(tpl_args.seed_type_id, tpl_args.seed_type)
            end
            return value
        end,
    },
    seed_effect = {
        inherit = false,
        field = 'effect',
        type = 'Text',
        func = nil,
     },
     },
     plant_booster_extra_chances = {
     seed_tier = {
         field = 'extra_chances',
        inherit = false,
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('plant_booster_extra_chances'),
         func = h.proc.number,
     },
     },
     --
     seed_growth_cycles = {
    -- Heist properties
        inherit = false,
    --
         field = 'growth_cycles',
    heist_required_job_id = {
         type = 'Integer',
         field = 'required_job_id',
         func = h.proc.number,
         type = 'String',
         func = h.factory.cast_text('heist_required_job_id'),
     },
     },
     heist_required_job_level = {
     seed_required_nearby_seed_tier = {
         field = 'required_job_level',
        inherit = false,
         field = 'required_nearby_seed_tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('heist_required_job_level'),
         func = h.proc.number,
     },
     },
     heist_data = {
     seed_required_nearby_seed_amount = {
         func = function (tpl_args, frame)
         inherit = false,
            if tpl_args.heist_required_job_level then
        field = 'required_nearby_seed_amount',
                if tpl_args.heist_required_job_id then
        type = 'Integer',
                    local results = m_cargo.query(
        func = h.proc.number,
                        {'heist_jobs', 'heist_npcs', 'heist_npc_skills'},
                        {'heist_npcs.name', 'heist_jobs.name'},
                        {
                            join = 'heist_npc_skills.job_id=heist_jobs.id, heist_npc_skills.npc_id=heist_npcs.id',
                            where = string.format('heist_npc_skills.job_id = "%s" AND heist_npc_skills.level >= %s', tpl_args.heist_required_job_id, tpl_args.heist_required_job_level),
                        }
                    )
                   
                    local npcs = {}
                   
                    for _, row in ipairs(results) do
                        npcs[#npcs+1] = row['heist_npcs.name']
                    end
                   
                    tpl_args.heist_required_npcs = table.concat(npcs, ', ')
                    tpl_args.heist_required_job = results[1]['heist_jobs.name']
                else
                    tpl_args.heist_required_job = i18n.tooltips.heist_any_job
                end
            end
        end,
     },
     },
     --
     seed_consumed_wild_lifeforce_percentage = {
    -- hideout doodads (HideoutDoodads.dat)
        inherit = false,
    --
         field = 'consumed_wild_lifeforce_percentage',
    is_master_doodad = {
         type = 'Integer',
         field = 'is_master_doodad',
         func = h.proc.number,
         type = 'Boolean',
        default = 0,
         func = m_util.cast.factory.boolean('is_master_doodad'),
     },
     },
     master = {
     seed_consumed_vivid_lifeforce_percentage = {
         field = 'master',
        inherit = false,
         type = 'String',
         field = 'consumed_vivid_lifeforce_percentage',
        -- todo validate against list of master names
         type = 'Integer',
         func = m_util.cast.factory.table('master', {key='full', tbl=m_game.constants.masters}),
         func = h.proc.number,
        default = 0,
     },
     },
     master_level_requirement = {
     seed_consumed_primal_lifeforce_percentage = {
         field = 'level_requirement',
        inherit = false,
         field = 'consumed_primal_lifeforce_percentage',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('master_level_requirement'),
         func = h.proc.number,
        default = 0,
    },
    seed_granted_craft_option_ids = {
        inherit = false,
        field = 'granted_craft_option_ids',
        type = 'List (,) of String',
        func = h.proc.list,
        default = {},
     },
     },
     master_favour_cost = {
     --
         field = 'favour_cost',
    -- Harvest planet boosters
    --
    plant_booster_radius = {
        inherit = false,
         field = 'radius',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('master_favour_cost'),
         func = h.proc.number,
     },
     },
     variation_count = {
     plant_booster_lifeforce = {
         field = 'variation_count',
        inherit = false,
         field = 'lifeforce',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('variation_count'),
         func = h.proc.number,
     },
     },
     -- Propehcy
     plant_booster_additional_crafting_options = {
    prophecy_id = {
        inherit = false,
         field = 'prophecy_id',
         field = 'additional_crafting_options',
         type = 'String',
         type = 'Integer',
         func = nil,
         func = h.proc.number,
     },
     },
     prediction_text = {
     plant_booster_extra_chances = {
         field = 'prediction_text',
         inherit = false,
        type = 'Text',
         field = 'extra_chances',
        func = h.factory.cast_text('prediction_text'),
    },
    seal_cost = {
         field = 'seal_cost',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('seal_cost'),
         func = h.proc.number,
     },
     },
     prophecy_reward = {
     --
         field = 'reward',
    -- Heist properties
         type = 'Text',
    --
         func = h.factory.cast_text('prophecy_reward'),
    heist_required_job_id = {
         field = 'required_job_id',
         type = 'String',
         func = h.proc.text,
     },
     },
     prophecy_objective = {
     heist_required_job_level = {
         field = 'objective',
         field = 'required_job_level',
         type = 'Text',
         type = 'Integer',
      func = h.factory.cast_text('prophecy_objective'),
        func = h.proc.number,
     },
     },
     -- Divination cards
     heist_data = {
    card_art = {
        inherit = false,
         field = 'card_art',
         field = nil,
         type = 'Page',
         type = nil,
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             tpl_args.card_art = string.format(i18n.divination_card_art, tpl_args.card_art or tpl_args.name)
             if tpl_args.heist_required_job_level then
                if tpl_args.heist_required_job_id then
                    local results = m_cargo.query(
                        {'heist_npc_skills', 'heist_jobs', 'heist_npcs'},
                        {'heist_npcs.name', 'heist_jobs.name'},
                        {
                            join = 'heist_npc_skills.job_id=heist_jobs.id, heist_npc_skills.npc_id=heist_npcs.id',
                            where = string.format('heist_npc_skills.job_id = "%s" AND heist_npc_skills.level >= %s', tpl_args.heist_required_job_id, tpl_args.heist_required_job_level),
                        }
                    )
                    local npcs = {}
                    for _, row in ipairs(results) do
                        npcs[#npcs+1] = row['heist_npcs.name']
                    end
                    tpl_args.heist_required_npcs = table.concat(npcs, ', ')
                    tpl_args.heist_required_job = results[1]['heist_jobs.name']
                else
                    tpl_args.heist_required_job = i18n.tooltips.heist_any_job
                end
            end
            return value
         end,
         end,
     },
     },
     -- ------------------------------------------------------------------------
     --
     -- derived stats
     -- Hideout decorations
     -- ------------------------------------------------------------------------
     --
      
     is_master_doodad = {
     -- For rarity != normal, rarity already verified
        inherit = false,
     base_item = {
        field = 'is_master_doodad',
         no_copy = true,
        type = 'Boolean',
         field = 'base_item',
        func = h.proc.boolean,
         type = 'String',
        default = false,
         func = function(tpl_args, frame)
     },
             tpl_args.base_item = tpl_args.base_item_data['items.name']
     variation_count = {
         inherit = false,
         field = 'variation_count',
         type = 'Integer',
        func = h.proc.number,
    },
    --
    -- Prophecies
    --
    is_prophecy = {
        inherit = false,
        field = nil,
        type = nil,
         func = function (tpl_args, value)
             tpl_args._flags.is_prophecy = tpl_args.prophecy_id ~= nil
            return value
         end,
         end,
        default = false,
     },
     },
     base_item_id = {
     prophecy_id = {
         no_copy = true,
         inherit = false,
         field = 'base_item_id',
         field = 'prophecy_id',
         type = 'String',
         type = 'String',
         func = function(tpl_args, frame)
         func = nil,
            tpl_args.base_item_id = tpl_args.base_item_data['items.metadata_id']
        end,
     },
     },
     base_item_page = {
     prediction_text = {
         no_copy = true,
         inherit = false,
         field = 'base_item_page',
         field = 'prediction_text',
        type = 'Text',
        func = h.proc.text,
    },
    seal_cost = {
        inherit = false,
        field = 'seal_cost',
        type = 'Integer',
        func = h.proc.number,
    },
    prophecy_reward = {
        inherit = false,
        field = 'reward',
        type = 'Text',
        func = h.proc.text,
    },
    prophecy_objective = {
        inherit = false,
        field = 'objective',
        type = 'Text',
        func = h.proc.text,
    },
    --
    -- Divination cards
    --
    card_art = {
        inherit = false,
        field = 'card_art',
         type = 'Page',
         type = 'Page',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             tpl_args.base_item_page = tpl_args.base_item_data['items._pageName']
             return string.format(i18n.files.divination_card_art, value or tpl_args.name)
         end,
         end,
     },
     },
     name_list = {
     card_background = {
         no_copy = true,
         inherit = false,
         field = 'name_list',
         field = 'card_background',
         type = 'List () of String',
         type = 'List (,) of Integer',
         func = function(tpl_args, frame)
        func = h.proc.factory.list{
             if tpl_args.name_list ~= nil then
            callback = m_util.cast.number,
                 tpl_args.name_list = m_util.string.split(tpl_args.name_list, ',%s*')
        },
                tpl_args.name_list[#tpl_args.name_list+1] = tpl_args.name
        default = {},
            else
    },
                 tpl_args.name_list = {tpl_args.name}
    --
    -- Sentinels
    --
    sentinel_duration = {
        field = 'duration',
        type = 'Integer',
        func = h.proc.number,
    },
    sentinel_empowers = {
        field = 'empowers',
        type = 'Integer',
        func = h.proc.number,
    },
    sentinel_empowerment = {
        field = 'empowerment',
        type = 'Integer',
        func = h.proc.number,
    },
    sentinel_charge = {
        field = 'charge',
        type = 'Integer',
        func = h.proc.number,
    },
    sentinel_monster = {
        inherit = false,
        field = 'monster',
        type = 'String',
        func = nil,
    },
    sentinel_monster_level = {
        inherit = false,
        field = 'monster_level',
        type = 'Integer',
        func = h.proc.number,
    },
    --
    -- Corpse items
    --
    corpse_tier = {
        inherit = false,
        field = 'tier',
        type = 'Integer',
         func = function (tpl_args, value)
             if tpl_args.metadata_id ~= nil then
                 for k, v in ipairs({'Low', 'Mid', 'High'}) do
                    if string.find(tpl_args.metadata_id, 'Metadata/Items/ItemisedCorpses/%w+' .. v) then
                        return k
                    end
                 end
             end
             end
            return nil
         end,
         end,
     },
     },
     frame_type = {
     monster_category = {
         no_copy = true,
         inherit = false,
         field = 'frame_type',
         field = 'monster_category',
         type = 'String',
         type = 'String',
        property = nil,
         func = h.proc.factory.value{
         func = function(tpl_args, frame)
             validate = m_util.validate.factory.in_table_keys{
            if tpl_args._flags.is_prophecy then
                tbl = m_game.constants.monster.categories,
                tpl_args.frame_type = 'prophecy'
                 errmsg = i18n.errors.invalid_monster_category,
                return
             },
            end
         },
       
     },
             local var = cfg.class_specifics[tpl_args.class_id]
     monster_category_html = {
            if var ~= nil and var.frame_type ~= nil then
        inherit = false,
                tpl_args.frame_type = var.frame_type
        field = 'monster_category_html',
                 return
         type = 'Text',
            end
         func = function (tpl_args, value)
           
             if tpl_args.monster_category ~= nil then
            if tpl_args.is_relic then
                 value = mw.html.create()
                tpl_args.frame_type = 'relic'
                     :tag('span')
                return
                        :addClass('mon-cat -' .. m_game.constants.monster.categories[tpl_args.monster_category].long_lower)
             end
                        :done()
           
                     :wikitext(m_game.constants.monster.categories[tpl_args.monster_category].long_upper)
            tpl_args.frame_type = tpl_args.rarity_id
                value = m_util.html.poe_color('value', tostring(value))
         end,
     },
     --
    -- args populated by mod validation
    --
    mods = {
         default = function (tpl_args, frame) return {} end,
         func_fetch = function (tpl_args, frame)
             local results = m_cargo.query(
                {'items' ,'item_mods'},
                {'item_mods.id', 'item_mods.is_implicit', 'item_mods.is_random', 'item_mods.text'},
                 {
                    join = 'items._pageID=item_mods._pageID',
                     where = string.format('items._pageName="%s" AND item_mods.is_implicit=1', tpl_args.base_item_page),
                }
            )
            for _, row in ipairs(results) do
                -- Handle text-only mods
                local result
                if row['item_mods.id'] == nil then
                    result = row['item_mods.text']
                end
                tpl_args._mods[#tpl_args._mods+1] = {
                    result=result,
                     id=row['item_mods.id'],
                    stat_text=row['item_mods.text'],
                    is_implicit=m_util.cast.boolean(row['item_mods.is_implicit']),
                    is_random=m_util.cast.boolean(row['item_mods.is_random']),
                }
             end
             end
            return value
         end,
         end,
     },
     },
     physical_damage_html = {
     monster_abilities = {
         no_copy = true,
         inherit = false,
         field = 'physical_damage_html',
         field = 'monster_abilities',
         type = 'Text',
         type = 'Text',
         func = h.factory.damage_html{key='physical'},
         func = nil,
     },
     },
     fire_damage_html = {
     --
         no_copy = true,
    -- Embers of the Allflame
         field = 'fire_damage_html',
    --
         type = 'Text',
    pack_id = {
         func = h.factory.damage_html{key='fire'},
         inherit = false,
         field = 'pack_id',
         type = 'String',
         func = nil,
    },
    pack_size = {
        inherit = false,
        field = 'pack_size',
        type = nil,
        func = nil,
     },
     },
     cold_damage_html = {
     pack_min_size = {
         no_copy = true,
         inherit = false,
         field = 'cold_damage_html',
         field = 'pack_min_size',
         type = 'Text',
         type = 'Integer',
         func = h.factory.damage_html{key='cold'},
         func = h.proc.number,
        default = 1,
     },
     },
     lightning_damage_html = {
     pack_max_size = {
         no_copy = true,
         inherit = false,
         field = 'lightning_damage_html',
         field = 'pack_max_size',
         type = 'Text',
         type = 'Integer',
         func = h.factory.damage_html{key='lightning'},
         func = h.proc.number,
        default = 1,
     },
     },
     chaos_damage_html = {
     pack_leader_chance = {
         no_copy = true,
         inherit = false,
         field = 'chaos_damage_html',
         field = 'pack_leader_chance',
         type = 'Text',
         type = 'Float',
         func = h.factory.damage_html{key='chaos'},
         func = h.proc.number,
     },
     },
     damage_avg = {
 
         no_copy = true,
    -- ------------------------------------------------------------------------
         field = 'damage_avg',
    -- derived stats
         type = 'Text',
    -- ------------------------------------------------------------------------
         func = function(tpl_args, frame)
   
            local dmg = {min=0, max=0}
    -- Populated by processing base item
            for key, _ in pairs(dmg) do
     base_item_id = {
                for _, dkey in ipairs(m_game.constants.damage_type_order) do
         inherit = false,
                    dmg[key] = dmg[key] + tpl_args[string.format('%s_damage_%s_range_average', dkey, key)]
         field = 'base_item_id',
                end
         type = 'String',
            end
         func = nil,
           
    },
             dmg = (dmg.min + dmg.max) / 2
    base_item_page = {
              
        inherit = false,
             tpl_args.damage_avg = dmg
        field = 'base_item_page',
        type = 'Page',
        func = nil,
    },
    base_item = {
        inherit = false,
        field = 'base_item',
        type = 'String',
        func = nil,
    },
    name_list = {
        inherit = false,
        field = 'name_list',
        type = 'List (�) of String',
        func = function (tpl_args, value)
             value = m_util.cast.table(value)
             value[#value+1] = tpl_args.name
             return value
         end,
         end,
        default = {},
     },
     },
     damage_html = {
     frame_type = {
         no_copy = true,
         inherit = false,
         field = 'damage_html',
         field = 'frame_type',
         type = 'Text',
         type = 'String',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             local text = {}
             if value then
             for _, dkey in ipairs(m_game.constants.damage_type_order) do
                return value
                local value = tpl_args[dkey .. '_damage_html']
             end
                if value ~= nil then
            if tpl_args._flags.is_prophecy then
                    text[#text+1] = value
                 return 'prophecy'
                 end
             end
             end
             if #text > 0 then
            local var = cfg.class_specifics[tpl_args.class_id]
                 tpl_args.damage_html = table.concat(text, '<br>')
             if var ~= nil and var.frame_type ~= nil then
                 return var.frame_type
             end
             end
            return tpl_args.rarity_id
         end,
         end,
     },
     },
     item_limit = {
     --
         no_copy = true,
    -- args populated by mod validation
         field = 'item_limit',
    --
         type = 'Integer',
    physical_damage_html = {
         func = m_util.cast.factory.number('item_limit'),
         inherit = false,
         field = 'physical_damage_html',
         type = 'Text',
         func = h.proc.factory.damage_html{type = 'physical'},
     },
     },
     jewel_radius_html = {
     fire_damage_html = {
         no_copy = true,
         inherit = false,
         field = 'radius_html',
         field = 'fire_damage_html',
         type = 'Text',
         type = 'Text',
         func = function(tpl_args, frame)
         func = h.proc.factory.damage_html{type = 'fire'},
            -- Get radius from stats
    },
            local radius = tpl_args._stats.local_jewel_effect_base_radius
    cold_damage_html = {
            if radius then
        inherit = false,
                radius = radius.min
        field = 'cold_damage_html',
                local size = m_game.constants.item.jewel_radius_to_size[radius] or radius
        type = 'Text',
                local color = radius == 0 and 'mod' or 'value'
        func = h.proc.factory.damage_html{type = 'cold'},
                tpl_args.jewel_radius_html = m_util.html.poe_color(color, size)
    },
            end
    lightning_damage_html = {
        end,
        inherit = false,
        field = 'lightning_damage_html',
        type = 'Text',
        func = h.proc.factory.damage_html{type = 'lightning'},
     },
     },
     incubator_effect = {
     chaos_damage_html = {
         no_copy = true,
         inherit = false,
         field = 'effect',
         field = 'chaos_damage_html',
         type = 'Text',
         type = 'Text',
         func = nil,
         func = h.proc.factory.damage_html{type = 'chaos'},
     },
     },
     drop_areas_html = {
     damage_avg = {
         no_copy = true,
         inherit = false,
         field = 'drop_areas_html',
         field = 'damage_avg',
         type = 'Text',
         type = 'Text',
         func = function(tpl_args, frame)
         func = function (tpl_args, value)
             if tpl_args.drop_areas_data == nil then
             local min = 0
                return
             local max = 0
             end
             for _, damage_type in ipairs(m_game.constants.damage_type_order) do
              
                min = min + tpl_args[damage_type .. '_damage_min_range_average']
            if tpl_args.drop_areas_html ~= nil then
                 max = max + tpl_args[damage_type .. '_damage_max_range_average']
                 return
             end
             end
              
             value = (min + max) / 2
            local areas = {}
             return value
            for _, data in pairs(tpl_args.drop_areas_data) do
                -- skip legacy maps in the drop html listing
                if not string.match(data['areas.id'], '^Map.*') or string.match(data['areas.id'], '^MapWorlds.*') or string.match(data['areas.id'], '^MapAtziri.*') then
                    areas[#areas+1] = string.format('[[%s|%s]]', data['areas.main_page'] or data['areas._pageName'], data['areas.main_page'] or data['areas.name'])
                end
            end
           
             tpl_args.drop_areas_html = table.concat(areas, ' • ')
         end,
         end,
     },
     },
     release_version = {
     damage_html = {
         no_copy = true,
         inherit = false,
         field = 'release_version',
         field = 'damage_html',
         type = 'String'
         type = 'Text',
        func = function (tpl_args, value)
            local text = {}
            for _, dkey in ipairs(m_game.constants.damage_type_order) do
                local range = tpl_args[dkey .. '_damage_html']
                if range ~= nil then
                    text[#text+1] = range
                end
            end
            if #text > 0 then
                value = table.concat(text, '<br>')
            end
            return value
        end,
     },
     },
     removal_version = {
     jewel_radius_html = {
         no_copy = true,
         inherit = false,
         field = 'removal_version',
         field = 'radius_html',
         type = 'String',
         type = 'Text',
        func = function (tpl_args, value)
            -- Get radius from stats
            local radius = tpl_args._stats.local_jewel_effect_base_radius
            if radius then
                radius = radius.min
                local size = m_game.constants.item.jewel_radius_to_size[radius] or radius
                local color =  radius == 0 and 'mod' or 'value'
                value = m_util.html.poe_color(color, size)
            end
            return value
        end,
     },
     },
     --
    drop_areas_html = {
        inherit = false,
        field = 'drop_areas_html',
        type = 'Text',
        func = function (tpl_args, value)
            if tpl_args._drop_areas_data == nil then
                return value
            end
            if value ~= nil then
                return value
            end
            local areas = {}
            for _, data in pairs(tpl_args._drop_areas_data) do
                -- skip legacy maps in the drop html listing
                if not string.match(data.id, '^Map.+') or string.match(data.id, '^MapWorlds.+') or string.match(data.id, '^MapAtziri.+') then
                    areas[#areas+1] = string.format('[[%s|%s]]', data.main_page or data._pageName, data.main_page or data.name)
                end
            end
            return table.concat(areas, ' • ')
        end,
    },
    release_version = {
        inherit = false,
        field = 'release_version',
        type = 'String',
        func = nil,
    },
    removal_version = {
        inherit = false,
        field = 'removal_version',
        type = 'String',
        func = nil,
    },
     --
     -- args governing use of the template itself
     -- args governing use of the template itself
     --  
     --  
     suppress_improper_modifiers_category = {
     suppress_improper_modifiers_category = {
         no_copy = true,
         inherit = false,
         field = nil,
         field = nil,
         func = m_util.cast.factory.boolean('suppress_improper_modifiers_category'),
         func = h.proc.boolean,
         default = false,
         default = false,
     },
     },
     upgraded_from_disabled = {
     disable_automatic_recipes = {
         no_copy = true,
         inherit = false,
         field = nil,
         field = nil,
         func = m_util.cast.factory.boolean('upgraded_from_disabled'),
         func = h.proc.boolean,
         default = false,
         default = false,
     },
     },
Line 1,772: Line 2,365:
         stats_add = {
         stats_add = {
             'local_level_requirement_+',
             'local_level_requirement_+',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=1, max=1},
         },
         },
         minimum = 1,
         minimum = 1,
Line 1,783: Line 2,373:
     weapon_range = {
     weapon_range = {
         field = 'weapon_range',
         field = 'weapon_range',
         stats_add = {
         stats_add_distance = {
             'local_weapon_range_+',
             'local_weapon_range_+',
         },
         },
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
             fmt = '%i',
             fmt = '%.1f',
         },
         },
     },
     },
Line 1,832: Line 2,422:
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            fmt = '%i',
             color = 'fire',
             color = 'fire',
            fmt = '%i',
         },
         },
     },
     },
Line 1,843: Line 2,433:
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            fmt = '%i',
             color = 'fire',
             color = 'fire',
            fmt = '%i',
         },
         },
     },
     },
Line 1,854: Line 2,444:
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            fmt = '%i',
             color = 'cold',
             color = 'cold',
            fmt = '%i',
         },
         },
     },
     },
Line 1,865: Line 2,455:
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            fmt = '%i',
             color = 'cold',
             color = 'cold',
            fmt = '%i',
         },
         },
     },
     },
Line 1,876: Line 2,466:
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            fmt = '%i',
             color = 'lightning',
             color = 'lightning',
            fmt = '%i',
         },
         },
     },
     },
Line 1,887: Line 2,477:
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            fmt = '%i',
             color = 'lightning',
             color = 'lightning',
            fmt = '%i',
         },
         },
     },
     },
Line 1,898: Line 2,488:
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            fmt = '%i',
             color = 'chaos',
             color = 'chaos',
            fmt = '%i',
         },
         },
     },
     },
Line 1,909: Line 2,499:
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            fmt = '%i',
             color = 'chaos',
             color = 'chaos',
            fmt = '%i',
         },
         },
     },
     },
Line 1,922: Line 2,512:
         },
         },
         stats_override = {
         stats_override = {
             ['local_weapon_always_crit'] = {min=100, max=100},
             ['local_weapon_crit_chance_is_100'] = {min=100, max=100},
         },
         },
         minimum = 0,
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
             fmt = '%.2f%%',
             fmt = '%.2f',
            inline = '%s%%',
         },
         },
     },
     },
Line 1,962: Line 2,553:
             'local_flask_amount_to_recover_+%',
             'local_flask_amount_to_recover_+%',
             'quality',
             'quality',
        },
        html_fmt_options = {
            fmt = '%i',
         },
         },
     },
     },
Line 2,002: Line 2,596:
         },
         },
     },
     },
     block = {
     armour = {
         field = 'block',
         arg = {
        stats_add = {
             min = 'armour_min',
             'local_additional_block_chance_%',
             max = 'armour_max',
        },
        minimum = 0,
        html_fmt_options = {
             fmt = '%i%%',
         },
         },
    },
    armour = {
         field = 'armour',
         field = 'armour',
         stats_add = {
         stats_add = {
Line 2,030: Line 2,618:
     },
     },
     evasion = {
     evasion = {
        arg = {
            min = 'evasion_min',
            max = 'evasion_max',
        },
         field = 'evasion',
         field = 'evasion',
         stats_add = {
         stats_add = {
Line 2,048: Line 2,640:
     },
     },
     energy_shield = {
     energy_shield = {
        arg = {
            min = 'energy_shield_min',
            max = 'energy_shield_max',
        },
         field = 'energy_shield',
         field = 'energy_shield',
         stats_add = {
         stats_add = {
Line 2,061: Line 2,657:
         },
         },
         stats_override = {
         stats_override = {
            ['local_no_energy_shield'] = {min=0, max=0},
             ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
             ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
         },
         },
Line 2,066: Line 2,663:
         html_fmt_options = {
         html_fmt_options = {
             fmt = '%i',
             fmt = '%i',
        },
    },
    ward = {
        arg = {
            min = 'ward_min',
            max = 'ward_max',
        },
        field = 'ward',
        stats_add = {
            'local_ward',
        },
        stats_increased = {
            'local_ward_+%',
            'quality',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    block = {
        field = 'block',
        stats_add = {
            'local_additional_block_chance_%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            inline = '%s%%',
         },
         },
     },
     },
Line 2,128: Line 2,754:
         },
         },
     },
     },
}
    sentinel_duration = {
 
         field = 'duration',
core.dps_map = {
         stats_add = {
    {
            'local_sentinel_duration_+',
         name = 'physical_dps',
         },
         field = 'physical_dps',
        stats_increased = {
         damage_args = {'physical_damage', },
            'local_sentinel_drone_duration_+%',
         label_infobox = i18n.tooltips.physical_dps,
        },
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            color = 'value',
             fmt = '%i',
             fmt = '%.1f',
         },
         },
     },
     },
     {
     sentinel_empowers = {
         name = 'fire_dps',
         field = 'empowers',
         field = 'fire_dps',
         stats_add = {
         damage_args = {'fire_damage'},
            'local_sentinel_tag_limit_+',
         label_infobox = i18n.tooltips.fire_dps,
         },
        stats_increased = {
            'local_sentinel_tag_limit_+%',
        },
         minimum = 0,
         html_fmt_options = {
         html_fmt_options = {
            color = 'fire',
             fmt = '%i',
             fmt = '%.1f',
         },
         },
     },
     },
     {
     sentinel_empowerment = {
        name = 'cold_dps',
         field = 'empowerment',
         field = 'cold_dps',
         stats_add = {
         damage_args = {'cold_damage'},
             'local_sentinel_drone_difficulty_+',
        label_infobox = i18n.tooltips.cold_dps,
        html_fmt_options = {
            color = 'cold',
             fmt = '%.1f',
         },
         },
     },
        stats_increased = {
     {
            'local_sentinel_drone_difficulty_+%',
         name = 'lightning_dps',
        },
         field = 'lightning_dps',
        minimum = 0,
         damage_args = {'lightning_damage'},
        html_fmt_options = {
         label_infobox = i18n.tooltips.lightning_dps,
            fmt = '%i',
        },
    },
    sentinel_charge = {
        field = 'charge',
        stats_add = {
            'local_sentinel_drone_charge_+',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
     },
     pack_size = {
         arg = {
            min = 'pack_min_size',
            max = 'pack_max_size',
        },
        field = 'pack_size',
        minimum = 1,
        html_fmt_options = {
            fmt = '%i',
            fmt_range = '%s-%s',
        },
    },
}
 
core.dps_map = {
    physical_dps = {
         field = 'physical_dps',
         damage_args = {'physical_damage'},
         label_infobox = i18n.tooltips.physical_dps,
         html_fmt_options = {
         html_fmt_options = {
            color = 'lightning',
             fmt = '%.1f',
             fmt = '%.1f',
            color = 'value',
        },
    },
    fire_dps = {
        field = 'fire_dps',
        damage_args = {'fire_damage'},
        label_infobox = i18n.tooltips.fire_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'fire',
         },
         },
     },
     },
     {
     cold_dps = {
         name = 'chaos_dps',
         field = 'cold_dps',
         field = 'chaos_dps',
        damage_args = {'cold_damage'},
         damage_args = {'chaos_damage'},
        label_infobox = i18n.tooltips.cold_dps,
         label_infobox = i18n.tooltips.chaos_dps,
        html_fmt_options = {
         html_fmt_options = {
            fmt = '%.1f',
             color = 'chaos',
            color = 'cold',
             fmt = '%.1f',
        },
         },
    },
     },
    lightning_dps = {
     {
         field = 'lightning_dps',
         name = 'elemental_dps',
         damage_args = {'lightning_damage'},
         field = 'elemental_dps',
         label_infobox = i18n.tooltips.lightning_dps,
         damage_args = {'fire_damage', 'cold_damage', 'lightning_damage'},
         html_fmt_options = {
         label_infobox = i18n.tooltips.elemental_dps,
             fmt = '%.1f',
         html_fmt_options = {
             color = 'lightning',
             color = 'value',
         },
             fmt = '%.1f',
     },
         },
     chaos_dps = {
     },
         field = 'chaos_dps',
     {
        damage_args = {'chaos_damage'},
         name = 'poison_dps',
        label_infobox = i18n.tooltips.chaos_dps,
         field = 'poison_dps',
        html_fmt_options = {
         damage_args = {'physical_damage', 'chaos_damage'},
            fmt = '%.1f',
         label_infobox = i18n.tooltips.poison_dps,
            color = 'chaos',
        },
    },
    elemental_dps = {
         field = 'elemental_dps',
         damage_args = {'fire_damage', 'cold_damage', 'lightning_damage'},
         label_infobox = i18n.tooltips.elemental_dps,
         html_fmt_options = {
             fmt = '%.1f',
             color = 'value',
         },
     },
     poison_dps = {
         field = 'poison_dps',
        damage_args = {'physical_damage', 'chaos_damage'},
        label_infobox = i18n.tooltips.poison_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'value',
        },
    },
    dps = {
         field = 'dps',
         damage_args = {'physical_damage', 'fire_damage', 'cold_damage', 'lightning_damage', 'chaos_damage'},
         label_infobox = i18n.tooltips.dps,
         html_fmt_options = {
         html_fmt_options = {
            color = 'value',
             fmt = '%.1f',
             fmt = '%.1f',
        },
    },
    {
        name = 'dps',
        field = 'dps',
        damage_args = {'physical_damage', 'fire_damage', 'cold_damage', 'lightning_damage', 'chaos_damage'},
        label_infobox = i18n.tooltips.dps,
        html_fmt_options = {
             color = 'value',
             color = 'value',
            fmt = '%.1f',
         },
         },
     },
     },

Revision as of 08:23, 2 May 2024

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


Lua logo

This module depends on the following other modules:

This submodule contains core configuration and functions for use in Module:Item2 and its other submodules.


-------------------------------------------------------------------------------
-- 
-- Core confirguation and functions for Module:Item2 and submodules
-- 
-------------------------------------------------------------------------------

local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')

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

local m_game = use_sandbox and mw.loadData('Module:Game/sandbox') or mw.loadData('Module:Game')

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Item2/config/sandbox') or mw.loadData('Module:Item2/config')

local i18n = cfg.i18n

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

local h = {}

-- ----------------------------------------------------------------------------
-- Core
-- ----------------------------------------------------------------------------

local core = {}

core.factory = {}

function core.factory.infobox_line(args)
    -- args: table
    --  type: How to read data from tpl_args using the given keys. nil = Regular, gem = Gem progression, stat = Stats
    --  parts: table
    --   [n]: table
    --    key: key to use. If type = gem and table is given, parse for subfield along path
    --    hide: Hide part if this function returns true
    --    hide_key: Alternate key to use to retrieve the value
    --    hide_default: hide the value if this is set
    --    hide_default_key: key to use if it isn't equal to the key parameter
    --    ----- params from m_util.html.format_value -----
    --    func: Function to transform the value retrieved from the database
    --    fmt: Format string (or function that returns format string) to use for the value.
    --      Default: '%s'
    --    fmt_range: Format string to use for range value.
    --      Default: '(%s-%s)'
    --    color: poe_color code to use for the value. False for no color.
    --      Default: 'value' if value is unmodified; 'mod' if modified
    --    class: Additional css class added to color tag
    --    inline: Format string to use for the output
    --    inline_color: poe_color code to use for the output. False for no color.
    --      Default: Inherits from value color
    --    inline_class: Additional css class added to inline color tag
    --  sep: If specified, parts are joined with this separator before being formatted for output
    --  fmt: Format string to use for output. If not specified, parts are simply concatenated
    --  color: poe_color code to use for output. Default: no color
    --  class: Additional css class added to output

    args.parts = args.parts or {}
    return function (tpl_args)
        local base_values = {}
        local temp_values = {}
        if args.type == 'gem' then
            -- Skill progression. Look for keys in tpl_args.skill_levels
            if not cfg.class_groups.gems.keys[tpl_args.class_id] then
                -- Skip if this item is not actually a gem
                return
            end
            for i, data in ipairs(args.parts) do
                if data.key then
                    local path = type(data.key) == 'table' and data.key or {data.key}
                    -- Check for static value
                    local value = tpl_args.skill_levels[0]
                    for _, p in ipairs(path) do -- Parse for subfield along path
                        if value[p] == nil then
                            value = nil
                            break
                        else
                            value = value[p]
                        end
                    end
                    if value ~= nil then
                        base_values[i] = value
                        temp_values[#temp_values+1] = {value={min=value,max=value}, index=i}
                    else -- Check for leveled values
                        value = {
                            min = tpl_args.skill_levels[1],
                            max = tpl_args.skill_levels[tpl_args.max_level],
                        }
                        for k, _ in pairs(value) do
                            for _, p in ipairs(path) do -- Parse for subfield along path
                                if value[k][p] == nil then
                                    value[k] = nil
                                    break
                                else
                                    value[k] = value[k][p]
                                end
                            end
                        end
                        if value.min ~= nil and value.max ~= nil then
                            base_values[i] = value.min
                            temp_values[#temp_values+1] = {value=value, index=i}
                        end
                    end
                end
            end
        elseif args.type == 'stat' then
            -- Stats. Look for key in tpl_args._stats
            for i, data in ipairs(args.parts) do
                local stat = tpl_args._stats[data.key]
                if stat then
                    local total = {min=0, max=0}
                    for _, v in ipairs(stat) do
                        if v.mod == nil or v.mod.is_implicit or v.mod.is_explicit then
                            total.min = total.min + v.min
                            total.max = total.max + v.max
                        end
                    end
                    base_values[i] = total.min
                    temp_values[#temp_values+1] = {value=total, index=i}
                end
            end
        else
            -- Regular. Look for key exactly as written in tpl_args
            for i, data in ipairs(args.parts) do
                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 and value.max ~= nil then
                    base_values[i] = value.min
                    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.parts[data.index]
            local hide = false
            if type(opt.hide) == 'function' then
                local v = data.value
                if opt.hide_key then
                    v = {
                        min = tpl_args[opt.hide_key .. '_range_minimum'],
                        max = tpl_args[opt.hide_key .. '_range_maximum'],
                    }
                    if v.min == nil or v.max == nil then
                        v = tpl_args[opt.hide_key]
                    end
                end
                hide = opt.hide(tpl_args, v)
            elseif opt.hide_default ~= nil then
                if opt.hide_default_key then
                    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
                            hide = true
                        end
                    elseif opt.hide_default == v.min and opt.hide_default == v.max then
                        hide = true
                    end
                else
                    local v = data.value
                    if opt.hide_default == v.min and opt.hide_default == v.max then
                        hide = true
                    end
                end
            end            
            if not hide then
                table.insert(final_values, data)
            end
        end
        
        -- all zeros = dont display and return early
        if #final_values == 0 then
            return nil
        end
        
        local parts = {}
        for i, data in ipairs(final_values) do
            local value = data.value
            value.base = base_values[data.index]
            local options = args.parts[data.index]
            if args.type == 'gem' and options.color == nil then
                -- Display skill progression range values as unmodified (white)
                options.color = 'value'
            end
            parts[#parts+1] = m_util.html.format_value(tpl_args, value, options)
        end
        if args.sep then
            -- Join parts with separator before formatting
            parts = {table.concat(parts, args.sep)}
        end

        -- Build output string
        local out
        if args.fmt then
            out = string.format(args.fmt, unpack(parts))
        else
            out = table.concat(parts)
        end
        if args.color then
            out = m_util.html.poe_color(args.color, out, args.class)
        elseif args.class then
            out = tostring(mw.html.create('em')
                :attr('class', class)
                :wikitext(out)
            )
        end
        return out
    end
end

function core.add_stat(tpl_args, stat_id, value, options)
    options = options or {}
    local mod = options.mod
    local tbl = options.tbl or '_stats'
    tpl_args[tbl] = tpl_args[tbl] or {}
    tpl_args[tbl][stat_id] = tpl_args[tbl][stat_id] or {
        min = 0,
        max = 0,
        avg = 0,
    }
    local stat = {
        mod = mod,
    }
    if type(value) == 'table' then
        stat.min = value.min
        stat.max = value.max
        stat.avg = value.avg or (stat.min + stat.max) / 2
    else
        stat.min = value
        stat.max = value
        stat.avg = value
    end
    table.insert(tpl_args[tbl][stat_id], stat)
    -- Totals
    tpl_args[tbl][stat_id].min = tpl_args[tbl][stat_id].min + stat.min
    tpl_args[tbl][stat_id].max = tpl_args[tbl][stat_id].max + stat.max
    tpl_args[tbl][stat_id].avg = tpl_args[tbl][stat_id].avg + stat.avg
end

--
-- Functions for processing tpl_args
--
h.proc = {}
h.proc.factory = {}

function h.proc.factory.value(args)
    args = args or {}
    return function (tpl_args, value)
        if value == nil then
            return nil
        end
        if args.cast then
            value = args.cast(value)
        end
        if args.validate then
            value = args.validate(value)
        end
        return value
    end
end

function h.proc.factory.list(args)
    args = args or {}
    return function (tpl_args, value)
        return m_util.cast.table(value, {
            pattern = args.pattern,
            callback = args.callback,
        })
    end
end

function h.proc.factory.damage_html(args)
    return function (tpl_args, value)
        local keys = {
            min = args.type .. '_damage_min',
            max = args.type .. '_damage_max',
        }
        local range = {}
        for ktype, key in pairs(keys) do
            range[ktype] = core.factory.infobox_line{
                parts = {
                    {
                        key = key,
                        color = false,
                        hide_default = 0,
                    }
                }
            }(tpl_args)
        end
        if range.min and range.max then
            local color = args.type or false
            local range_fmt
            if tpl_args[keys.min .. '_range_minimum'] ~= tpl_args[keys.min .. '_range_maximum'] or tpl_args[keys.max .. '_range_minimum'] ~= tpl_args[keys.max .. '_range_maximum'] then
                -- Variable damage range, based on modifier rolls
                if args.type == 'physical' then
                    color = 'mod'
                end
                range_fmt = i18n.fmt.variable_damage_range
            else
                -- Standard damage range
                if args.type == 'physical' then
                    color = 'value'
                end
                range_fmt = i18n.fmt.standard_damage_range
            end
            value = string.format(range_fmt, range.min, range.max)
            if color then
                value = m_util.html.poe_color(color, value)
            end
        end
        return value
    end
end

function h.proc.factory.stat_text(args)
    return function (tpl_args, value)
        local type_map = {
            implicit = 'is_implicit',
            explicit = 'is_explicit',
        }
        if type_map[args.type] == nil then
            return nil
        end
        local lines = {}
        local random_mods = {}
        local skip = cfg.class_specifics[tpl_args.class_id] and cfg.class_specifics[tpl_args.class_id].skip_stat_lines or nil
        for _, mod_data in ipairs(tpl_args._mods) do
            if mod_data[type_map[args.type]] then
                if mod_data.is_random == true then
                    random_mods[mod_data.stat_text] = random_mods[mod_data.stat_text] or {}
                    table.insert(random_mods[mod_data.stat_text], mod_data)
                else
                    if mod_data.stat_text ~= nil then
                        table.insert(lines, mod_data.stat_text)
                    else
                        local text = mod_data.result['mods.stat_text']
                        if text and text ~= '' then
                            for _, line in ipairs(m_util.string.split(text, '<br>')) do
                                local skipped = false
                                if skip then
                                    for _, pattern in ipairs(skip) do
                                        if string.match(line, pattern) then
                                            skipped = true
                                            break
                                        end
                                    end
                                end
                                if not skipped then
                                    table.insert(lines, line)
                                end
                            end
                        end
                    end
                end
            end
        end
        for stat_text, mod_data_list in pairs(random_mods) do
            local text = {}
            for _, mod_data in ipairs(mod_data_list) do
                table.insert(text, mod_data.result['mods.stat_text'])
            end
            local tbl = mw.html.create('table')
            tbl
                :attr('class', 'random-modifier-stats mw-collapsed')
                :attr('style', 'text-align: left')
                :tag('tr')
                    :tag('th')
                        :attr('class', 'mw-customtoggle-31')
                        :wikitext(stat_text)
                        :done()
                    :done()
                :tag('tr')
                    :attr('class', 'mw-collapsible mw-collapsed')
                    :attr('id', 'mw-customcollapsible-31')
                    :tag('td')
                        :wikitext(table.concat(text, '<hr style="width: 20%">'))
                        :done()
                    :done()
            table.insert(lines, tostring(tbl))
        end
        return #lines > 0 and table.concat(lines, '<br>') or nil
    end
end

h.proc.text = h.proc.factory.value{cast = m_util.cast.text}
h.proc.boolean = h.proc.factory.value{cast = m_util.cast.boolean}
h.proc.number = h.proc.factory.value{cast = m_util.cast.number}

h.proc.percentage = h.proc.factory.value{
    cast = m_util.cast.number,
    validate = m_util.validate.factory.number_in_range{
        min = 0,
        max = 100,
    },
}

h.proc.size = h.proc.factory.value{
    cast = m_util.cast.number,
    validate = m_util.validate.factory.number_in_range{
        min = 1,
        max = 4,
    },
}

h.proc.character = h.proc.factory.value{
    validate = m_util.validate.factory.string_length{
        min = 1,
        max = 1,
    },
}

h.proc.list = h.proc.factory.list()

--
-- Argument mapping
--
-- [<tpl_args key>] = {
--   inherit  boolean  Whether the item will inherit this key from its base item. Default: true
--   field  string  Cargo field name
--   type  string  Cargo field type
--   func  function  Function to unpack the argument into a native lua value and validate it
--   default  varies  Default value if parameter is not set
--   deprecated  boolean  Set to true for deprecated parameters
-- }
core.map = {
    -- special params
    html = {
        inherit = false,
        field = 'html',
        type = 'Text',
        func = nil,
    },
    infobox_html = {
        inherit = false,
        field = 'infobox_html',
        type = 'Text',
        func = nil,
    },
    metabox_html = {
        inherit = false,
        field = 'metabox_html',
        type = 'Text',
        func = nil,
    },
    implicit_stat_text = {
        inherit = false,
        field = 'implicit_stat_text',
        type = 'Text',
        func = h.proc.factory.stat_text{type='implicit'},
    },
    explicit_stat_text = {
        inherit = false,
        field = 'explicit_stat_text',
        type = 'Text',
        func = h.proc.factory.stat_text{type='explicit'},
    },
    stat_text = {
        inherit = false,
        field = 'stat_text',
        type = 'Text',
        func = function (tpl_args, value)
            if tpl_args.implicit_stat_text or tpl_args.explicit_stat_text then
                local stats = {}
                table.insert(stats, tpl_args.implicit_stat_text) -- No-op if value is nil
                table.insert(stats, tpl_args.explicit_stat_text)
                if tpl_args.is_corrupted then
                    table.insert(stats, m_util.html.poe_color('corrupted', i18n.tooltips.corrupted))
                elseif tpl_args.is_mirrored then
                    table.insert(stats, i18n.tooltips.mirrored)
                elseif tpl_args.is_unmodifiable then
                    table.insert(stats, i18n.tooltips.unmodifiable)
                end
                local sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
                value = table.concat(stats, sep)
            end
            return value
        end,
    },
    class_id = {
        inherit = false,
        field = 'class_id',
        type = 'String',
        func = function (tpl_args, value)
            if value == nil then
                error(string.format(i18n.errors.generic_required_parameter, 'class_id'))
            end
            if not m_util.table.has_key(m_game.constants.item.classes, value) or m_game.constants.item.classes[value].disabled then
                error(string.format(i18n.errors.invalid_class_id, tostring(value)))
            end
            return value
        end,
    },
    class = {
        inherit = false,
        field = 'class',
        type = 'String',
        func = function (tpl_args, value)
            local class = m_game.constants.item.classes[tpl_args.class_id].long_upper
            -- Avoids errors with empty item class names later on
            if class == '' then
                class = nil
            end
            return class
        end,
        deprecated = true,
    },
    -- generic
    is_in_game = {
        inherit = false,
        field = 'is_in_game',
        type = 'Boolean',
        func = h.proc.boolean,
        default = true,
    },
    rarity_id = {
        inherit = false,
        field = 'rarity_id',
        type = 'String',
        func = h.proc.factory.value{
            validate = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.rarities,
                errmsg = i18n.errors.invalid_rarity_id,
            },
        },
    },
    rarity = {
        inherit = false,
        field = 'rarity',
        type = 'String',
        func = function (tpl_args, value)
            return m_game.constants.rarities[tpl_args.rarity_id].long_upper
        end,
        deprecated = true,
    },
    name = {
        inherit = false,
        field = 'name',
        type = 'String',
        func = nil,
    },
    size_x = {
        field = 'size_x',
        type = 'Integer',
        func = h.proc.size,
        default = 1,
    },
    size_y = {
        field = 'size_y',
        type = 'Integer',
        func = h.proc.size,
        default = 1,
    },
    drop_rarities_ids = {
        inherit = false,
        field = 'drop_rarity_ids',
        type = 'List (,) of String',
        func = function (tpl_args, value)
            -- Drop rarities only matter for base items
            if tpl_args._flags.is_derived then
                return nil -- Use default
            end
            return m_util.cast.table(value, {
                callback = m_util.validate.factory.in_table_keys{
                    tbl = m_game.constants.rarities,
                    errmsg = i18n.errors.invalid_rarity_id,
                    errlvl = 4,
                },
            })
        end,
        default = {},
    },
    drop_enabled = {
        inherit = false,
        field = 'drop_enabled',
        type = 'Boolean',
        func = function (tpl_args, value)
            if value == nil then
                return nil -- Use default
            end
            return tpl_args.is_in_game and m_util.cast.boolean(value)
        end,
        default = function (tpl_args)
            return tpl_args.is_in_game
        end,
    },
    drop_level = {
        inherit = false,
        field = 'drop_level',
        type = 'Integer',
        func = h.proc.number,
    },
    drop_level_maximum = {
        inherit = false,
        field = 'drop_level_maximum',
        type = 'Integer',
        func = h.proc.number,
    },
    acquisition_tags = {
        inherit = false,
        field = 'acquisition_tags',
        type = 'List (,) of String',
        func = h.proc.factory.list{
            callback = m_util.validate.factory.in_table_keys{
                tbl = cfg.acquisition_tags,
                errmsg = i18n.errors.invalid_acquisition_tag,
                errlvl = 4,
            },
        },
        default = {},
    },
    drop_areas = {
        inherit = false,
        field = 'drop_areas',
        type = 'List (,) of String',
        func = function (tpl_args, value)
            value = m_util.cast.table(value)
            if type(value) == 'table' and #value > 0 then
                tpl_args._drop_areas_data = m_cargo.array_query{
                    tables = {'areas'},
                    fields = {
                        'areas._pageName=_pageName',
                        'areas.id=id',
                        'areas.name=name',
                        'areas.main_page=main_page',
                        'areas.is_legacy_map_area=is_legacy_map_area'
                    },
                    id_field = 'areas.id',
                    id_array = value,
                    query = {limit=5000},
                }
                if tpl_args._drop_areas_data then
                    tpl_args._legacy_drop_areas = tpl_args._legacy_drop_areas or {}
                    for _, v in ipairs(tpl_args._drop_areas_data) do
                        if m_util.cast.boolean(v.is_legacy_map_area) then
                            tpl_args._flags.has_legacy_drop_areas = true
                            table.insert(tpl_args._legacy_drop_areas, v.id)
                        end
                    end
                end
            end
            return value
        end,
        default = {},
    },
    drop_monsters = {
        inherit = false,
        field = 'drop_monsters',
        type = 'List (,) of Text',
        func = h.proc.list,
        default = {},
    },
    drop_text = {
        inherit = false,
        field = 'drop_text',
        type = 'Text',
        func = h.proc.text,
    },
    required_level = {
        field = 'required_level_base',
        type = 'Integer',
        func = h.proc.number,
        default = 1,
    },
    required_level_final = {
        field = 'required_level',
        type = 'Integer',
        func = function (tpl_args, value)
            value = tpl_args.required_level
            if value < cfg.base_item_required_level_threshold and cfg.base_item_required_level_threshold_classes[tpl_args.class_id] then
                value = 1
            end
            return value
        end,
        default = 1,
    },
    required_dexterity = {
        field = 'required_dexterity',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    required_strength = {
        field = 'required_strength',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    required_intelligence = {
        field = 'required_intelligence',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    inventory_icon = {
        inherit = false,
        field = 'inventory_icon',
        type = 'String',
        func = function (tpl_args, value)
            if not value then
                -- Certain types of items have default inventory icons
                if i18n.default_inventory_icons[tpl_args.class_id] then
                    value = i18n.default_inventory_icons[tpl_args.class_id]
                else
                    for k, v in pairs(tpl_args._flags) do
                        value = v and i18n.default_inventory_icons[k]
                    end
                end
            end
            tpl_args.inventory_icon_id = value or tpl_args.name
            return string.format(i18n.files.inventory_icon, tpl_args.inventory_icon_id) 
        end,
    },
    -- note: this must be called after inventory_icon to work correctly as it depends on tpl_args.inventory_icon_id being set
    alternate_art_inventory_icons = {
        inherit = false,
        field = 'alternate_art_inventory_icons',
        type = 'List (,) of String',
        func = function (tpl_args, value)
            return m_util.cast.table(value, {
                callback = function (value)
                    return string.format(i18n.files.inventory_icon, string.format('%s %s', tpl_args.inventory_icon_id, tostring(value)))
                end,
            })
        end,
        default = {},
    },
    cannot_be_traded_or_modified = {
        inherit = false,
        field = 'cannot_be_traded_or_modified',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    help_text = {
        field = 'help_text',
        type = 'Text',
        func = h.proc.text,
    },
    is_account_bound = {
        inherit = false,
        field = 'is_account_bound',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    flavour_text = {
        inherit = false,
        field = 'flavour_text',
        type = 'Text',
        func = h.proc.factory.value{
            cast = function (value)
                value = m_util.cast.text(value)
                -- Parse harbinger glyphs
                value = string.gsub(value, '<<([Hh][Bb][Gg]%w+)>>', '<span class="glyph %1"></span>')
                return value
            end,
        },
    },
    flavour_text_id = {
        inherit = false,
        field = 'flavour_text_id',
        type = 'String',
        func = nil,
    },
    tags = {
        field = 'tags',
        type = 'List (,) of String',
        func = h.proc.factory.list{
            callback = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.tags,
                errmsg = i18n.errors.invalid_tag,
                errlvl = 4,
            },
        },
        default = {},
    },
    metadata_id = {
        inherit = false,
        field = 'metadata_id',
        type = 'String',
        func = function (tpl_args, value)
            if value == nil then
                return nil
            end
            -- Unless we're in testing mode, validate that metadata_id is unique
            if not tpl_args.test then
                local results = m_cargo.query(
                    {'items'},
                    {'items._pageName'},
                    {
                        where = string.format(
                            'items.metadata_id = "%s" AND items._pageName != "%s"',
                            value,
                            m_cargo.addslashes(mw.title.getCurrentTitle().prefixedText)
                        )
                    }
                )
                if #results > 0 then
                    error(string.format(i18n.errors.duplicate_metadata, value, results[1]['items._pageName']))
                end
            end
            return value
        end,
    },
    influences = {
        inherit = false,
        field = 'influences',
        type = 'List (,) of String',
        func = h.proc.factory.list{
            callback = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.influences,
                errmsg = i18n.errors.invalid_influence,
                errlvl = 4,
            },
        },
        default = {},
    },
    is_fractured = {
        inherit = false,
        field = 'is_fractured',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    is_synthesised = {
        inherit = false,
        field = 'is_synthesised',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    is_searing_exarch_item = {
        inherit = false,
        field = 'is_searing_exarch_item',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    is_eater_of_worlds_item = {
        inherit = false,
        field = 'is_eater_of_worlds_item',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    is_veiled = {
        inherit = false,
        field = 'is_veiled',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    is_replica = {
        inherit = false,
        field = 'is_replica',
        type = 'Boolean',
        func = function (tpl_args, value)
            value = m_util.cast.boolean(value)
            if value == true and tpl_args.rarity_id ~= 'unique' then
                error(string.format(i18n.errors.non_unique_flag, 'is_replica'))
            end
            return value
        end,
        default = false,
    },
    is_corrupted = {
        inherit = false,
        field = 'is_corrupted',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    is_mirrored = {
        inherit = false,
        field = nil,
        type = nil,
        func = h.proc.boolean,
        default = false,
    },
    is_unmodifiable = {
        inherit = false,
        field = 'is_unmodifiable',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    is_drop_restricted = {
        inherit = false,
        field = 'is_drop_restricted',
        type = 'Boolean',
        func = h.proc.boolean,
        default = function (tpl_args)
            -- Divination cards have drop restrictions by design.
            if tpl_args.class_id == 'DivinationCard' then
                return true
            end
            for _, key in ipairs({'is_replica', '_drop_areas_data', 'drop_monsters'}) do
                -- arg must be truthy and NOT an empty table
                if tpl_args[key] and not (type(tpl_args[key]) == 'table' and #tpl_args[key] == 0) then
                    return true
                end
            end
            local flags = {
                'is_talisman',
                'is_essence',
                'is_blight_item',
                'is_fossil',
                'is_tattoo',
                'is_delirium_orb',
                'is_catalyst',
            }
            for _, flag in ipairs(flags) do
                if tpl_args._flags[flag] then
                    return true
                end
            end
            return false
        end,
    },
    purchase_costs = {
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            local purchase_costs = {}
            for _, rarity_id in ipairs(m_game.constants.rarity_order) do
                local rtbl = {}
                local prefix = string.format('purchase_cost_%s', rarity_id)
                local i = 1
                while i ~= -1 do
                    local iprefix = prefix .. i
                    local values = {
                        name = tpl_args[iprefix .. '_name'],
                        amount = tonumber(tpl_args[iprefix .. '_amount']),
                        rarity = rarity_id,
                    }
                    if values.name ~= nil and values.amount ~= nil then
                        rtbl[#rtbl+1] = values
                        i = i + 1
                        table.insert(tpl_args._store_data, {
                            _table = 'item_purchase_costs',
                            amount = values.amount,
                            name = values.name,
                            rarity = values.rarity,
                        })
                    else
                        i = -1
                    end
                end
                purchase_costs[rarity_id] = rtbl
            end
            return purchase_costs
        end,
        func_fetch = function (tpl_args)
            if tpl_args.rarity_id ~= 'unique' then
                return
            end
            
            local results = m_cargo.query(
                {'items' ,'item_purchase_costs'},
                {'item_purchase_costs.amount', 'item_purchase_costs.name', 'item_purchase_costs.rarity'},
                {
                    join = 'items._pageID=item_purchase_costs._pageID',
                    where = string.format('items._pageName="%s" AND item_purchase_costs.rarity="unique"', tpl_args.base_item_page),
                }
            )
            
            for _, row in ipairs(results) do
                local values = {
                    rarity = row['item_purchase_costs.rarity'],
                    name = row['item_purchase_costs.name'],
                    amount = tonumber(row['item_purchase_costs.amount']),
                }
                local datavar = tpl_args.purchase_costs[string.lower(values.rarity)]
                datavar[#datavar+1] = values
                table.insert(tpl_args._store_data, {
                    _table = 'item_purchase_costs',
                    amount = values.amount,
                    name = values.name,
                    rarity = values.rarity,
                })
            end
        end,
    },
    is_sellable = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            if value == nil then
                return nil -- Use default
            end
            return tpl_args.is_in_game and m_util.cast.boolean(value)
        end,
        default = function (tpl_args)
            return tpl_args.is_in_game
        end,
    },
    sell_prices_override = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            -- these variables are also used by mods when setting automatic sell prices
            tpl_args.sell_prices = {}
            tpl_args.sell_price_order = {}
            if not tpl_args.is_sellable then
                return nil
            end
            local name
            local amount
            local i = 0
            repeat
                i = i + 1
                name = tpl_args[string.format('sell_price%s_name', i)]
                amount = tpl_args[string.format('sell_price%s_amount', i)]
                
                if name ~= nil and amount ~= nil then
                    tpl_args.sell_price_order[#tpl_args.sell_price_order+1] = name
                    tpl_args.sell_prices[name] = amount
                    table.insert(tpl_args._store_data, {
                        _table = 'item_sell_prices',
                        amount = amount,
                        name = name,
                    })
                end
            until name == nil or amount == nil 
            -- if sell prices are set, the override is active
            for _, _ in pairs(tpl_args.sell_prices) do
                tpl_args._flags.sell_prices_override = true
                break
            end
            return value
        end,
    },
    --
    -- specific section
    --

    -- Most item classes
    quality = {
        inherit = false,
        field = 'quality',
        type = 'Integer',
        -- Must copy to stat for the stat adjustments to work properly
        func = function (tpl_args, value)
            local value = tonumber(value)
            if value then
                core.add_stat(tpl_args, 'quality', value)
                if tpl_args.class_id == 'UtilityFlask' then
                    core.add_stat(tpl_args, 'quality_flask_duration', value)
                elseif tpl_args.class_id == 'Map' then
                    -- quality is added to quantity for maps
                    core.add_stat(tpl_args, 'map_item_drop_quantity_+%', value)
                end
            end
            return value
        end,
        default = 0,
    },
    -- amulets
    is_talisman = {
        field = 'is_talisman',
        type = 'Boolean',
        func = function (tpl_args, value)
            value = m_util.cast.boolean(value)
            tpl_args._flags.is_talisman = value
            return value
        end,
        default = false,
    },
    talisman_tier = {
        field = 'talisman_tier',
        type = 'Integer',
        func = h.proc.number,
    },
    -- flasks
    charges_max = {
        field = 'charges_max',
        type = 'Integer',
        func = h.proc.number,
    },
    charges_per_use = {
        field = 'charges_per_use',
        type = 'Integer',
        func = h.proc.number,
    },
    flask_mana = {
        field = 'mana',
        type = 'Integer',
        func = h.proc.number,
    },
    flask_life = {
        field = 'life',
        type = 'Integer',
        func = h.proc.number,
    },
    flask_duration = {
        field = 'duration',
        type = 'Float',
        func = h.proc.number,
    },
    buff_id = {
        field = 'id',
        type = 'String',
        func = nil,
    },
    buff_values = {
        field = 'buff_values',
        type = 'List (,) of Integer',
        func = function (tpl_args, value)
            local values = {}
            local i = 0
            repeat 
                i = i + 1
                local key = 'buff_value' .. i
                values[i] = tonumber(tpl_args[key])
                tpl_args[key] = nil
            until values[i] == nil
            
            -- needed so the values copyied from unique item base isn't overriden
            if #values >= 1 then
                value = values
            end
            return value
        end,
        func_copy = function (tpl_args, value)
            tpl_args.buff_values = m_util.string.split(value, ',%s*')
        end,
        default = {},
    },
    buff_stat_text = {
        field = 'stat_text',
        type = 'String',
        func = nil,
    },
    buff_icon = {
        field = 'icon',
        type = 'String',
        func = function (tpl_args, value)
            return string.format(i18n.files.status_icon, tpl_args.name)
        end,
    },
    
    -- weapons
    critical_strike_chance = {
        field = 'critical_strike_chance',
        type = 'Float',
        func = h.proc.number,
    },
    attack_speed = {
        field = 'attack_speed',
        type = 'Float',
        func = h.proc.number,
    },
    weapon_range = {
        field = 'weapon_range',
        type = 'Float',
        func = h.proc.number,
    },
    physical_damage_min = {
        field = 'physical_damage_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    physical_damage_max = {
        field = 'physical_damage_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    fire_damage_min = {
        field = 'fire_damage_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    fire_damage_max = {
        field = 'fire_damage_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    cold_damage_min = {
        field = 'cold_damage_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    cold_damage_max = {
        field = 'cold_damage_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    lightning_damage_min = {
        field = 'lightning_damage_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    lightning_damage_max = {
        field = 'lightning_damage_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    chaos_damage_min = {
        field = 'chaos_damage_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    chaos_damage_max = {
        field = 'chaos_damage_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    -- armor-type stuff
    armour = {
        inherit = false,
        field = 'armour',
        type = nil,
        func = nil,
    },
    armour_min = {
        field = 'armour_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    armour_max = {
        field = 'armour_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    evasion = {
        inherit = false,
        field = 'evasion',
        type = nil,
        func = nil,
    },
    evasion_min = {
        field = 'evasion_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    evasion_max = {
        field = 'evasion_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    energy_shield = {
        inherit = false,
        field = 'energy_shield',
        type = nil,
        func = nil,
    },
    energy_shield_min = {
        field = 'energy_shield_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    energy_shield_max = {
        field = 'energy_shield_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    ward = {
        inherit = false,
        field = 'ward',
        type = nil,
        func = nil,
    },
    ward_min = {
        field = 'ward_min',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    ward_max = {
        field = 'ward_max',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    -- This is the inherent penality from the armour piece if any
    movement_speed = {
        field = 'movement_speed',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    -- shields
    block = {
        field = 'block',
        type = 'Integer',
        func = h.proc.number,
    },
    -- skill gem stuff
    gem_description = {
        field = 'gem_description',
        type = 'Text',
        func = h.proc.text,
    },
    dexterity_percent = {
        field = 'dexterity_percent',
        type = 'Integer',
        func = h.proc.percentage,
    },
    strength_percent = {
        field = 'strength_percent',
        type = 'Integer',
        func = h.proc.percentage,
    },
    intelligence_percent = {
        field = 'intelligence_percent',
        type = 'Integer',
        func = h.proc.percentage,
    },
    primary_attribute = {
        field = 'primary_attribute',
        type = 'String',
        func = function (tpl_args, value)
            for _, attr in ipairs(m_game.constants.attribute_order) do
                local val = tpl_args[attr .. '_percent'] 
                if val and val >= 60 then
                    return attr
                end
            end
            return 'none'
        end,
    },
    gem_tags = {
        field = 'gem_tags',
        type = 'List (,) of String',
        func = h.proc.factory.list{
            callback = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.item.gem_tags_lookup,
                errmsg = i18n.errors.invalid_gem_tag,
                errlvl = 4,
            },
        },
        default = {},
    },
    is_vaal_skill_gem = {
        field = 'is_vaal_skill_gem',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    vaal_variant_id = {
        inherit = true,
        field = 'vaal_variant_id',
        type = 'String',
        func = nil,
    },
    -- Support gems only
    support_gem_letter = {
        field = 'support_gem_letter',
        type = 'String',
        func = h.proc.character,
    },
    support_gem_letter_html = {
        field = 'support_gem_letter_html',
        type = 'Text',
        func = function (tpl_args, value)
            if tpl_args.support_gem_letter == nil then
                return nil
            end
            for k, v in pairs(m_game.constants.attributes) do
                local key = string.format('%s_percent', v.long_lower)
                if tpl_args[key] and tpl_args[key] > 50 then
                    value = tostring(
                        mw.html.create('span')
                            :attr('class', string.format('support-gem-id-%s', v.color))
                            :wikitext(tpl_args.support_gem_letter)
                    )
                    break
                end
            end
            return value
        end,
    },
    is_awakened_support_gem = {
        field = 'is_awakened_support_gem',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    awakened_variant_id = {
        field = 'awakened_variant_id',
        type = 'String',
        func = nil,
    },
    --
    -- Jewels
    --
    jewel_limit = {
        inherit = false,
        field = 'jewel_limit',
        type = 'String',
        func = nil,
    },
    --
    -- Maps
    --
    map_tier = {
        field = 'tier',
        type = 'Integer',
        func = h.proc.number,
    },
    map_guild_character = {
        field = 'guild_character',
        type = 'String',
        func = h.proc.character,
    },
    map_area_id = {
        field = 'area_id',
        type = 'String',
        func = nil, -- TODO: Validate against a query?
    },
    map_area_level = {
        field = 'area_level',
        type = 'Integer',
        func = h.proc.number,
    },
    unique_map_guild_character = {
        field = 'unique_guild_character',
        type = 'String',
        func = h.proc.character,
        func_copy = function (tpl_args, value)
            tpl_args.map_guild_character = value
        end,
    },
    unique_map_area_id = {
        field = 'unique_area_id',
        type = 'String',
        func = nil, -- TODO: Validate against a query?
        func_copy = function (tpl_args, value)
            tpl_args.map_area_id = value
        end,
    },
    unique_map_area_level = {
        field = 'unique_area_level',
        type = 'Integer',
        func = h.proc.number,
        func_copy = function (tpl_args, value)
            tpl_args.map_area_level = value
        end,
    },
    map_series = {
        field = 'series',
        type = 'String',
        func = function (tpl_args, value)
            if tpl_args.rarity == 'normal' and value == nil then
                error(string.format(i18n.errors.generic_required_parameter, 'map_series'))
            end
            return value
        end,
    },
    -- atlas info is only for the current map series
    atlas_x = {
        inherit = false,
        field = 'x',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y = {
        inherit = false,
        field = 'y',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_region_id = {
        inherit = false,
        field = 'region_id',
        type = 'String',
        func = nil,
    },
    atlas_region_minimum = {
        inherit = false,
        field = 'region_minimum',
        type = 'Integer',
        func = h.proc.number,
    },
    atlas_x0 = {
        inherit = false,
        field = 'x0',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_x1 = {
        inherit = false,
        field = 'x1',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_x2 = {
        inherit = false,
        field = 'x2',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_x3 = {
        inherit = false,
        field = 'x3',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_x4 = {
        inherit = false,
        field = 'x4',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y0 = {
        inherit = false,
        field = 'y0',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y1 = {
        inherit = false,
        field = 'y1',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y2 = {
        inherit = false,
        field = 'y2',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y3 = {
        inherit = false,
        field = 'y3',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y4 = {
        inherit = false,
        field = 'y4',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_map_tier0 = {
        field = 'map_tier0',
        type = 'Integer',
        func = h.proc.number,
    },
    atlas_map_tier1 = {
        field = 'map_tier1',
        type = 'Integer',
        func = h.proc.number,
    },
    atlas_map_tier2 = {
        field = 'map_tier2',
        type = 'Integer',
        func = h.proc.number,
    },
    atlas_map_tier3 = {
        field = 'map_tier3',
        type = 'Integer',
        func = h.proc.number,
    },
    atlas_map_tier4 = {
        field = 'map_tier4',
        type = 'Integer',
        func = h.proc.number,
    },
    atlas_connections = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            value = {}
            local cont = true
            local i = 1
            while cont do
                local prefix = string.format('atlas_connection%s_', i)
                local regions = tpl_args[prefix .. 'tier']
                local data = {
                    _table = 'atlas_connections',
                    map1 = string.format('%s (%s)', tpl_args.name, tpl_args.map_series or ''),
                    map2 = tpl_args[prefix .. 'target'],
                }
                
                if regions and data.map2 then
                    regions = m_util.string.split(regions, ',%s*')
                    if #regions ~= 5 then
                        error(string.format(i18n.errors.invalid_region_upgrade_count, i, #regions))
                    end
                    for index, value in ipairs(regions) do
                        data['region' .. (index - 1)] = m_util.cast.boolean(value)
                    end
                    
                    value[data.map2] = data
                    table.insert(tpl_args._store_data, data)
                else
                    cont = false
                    if i == 1 then
                        value = nil
                    end
                end
                i = i + 1
            end
            return value
        end,
    },
    --
    -- Map fragments
    --
    is_scarab = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_scarab = m_util.table.contains(tpl_args.tags, 'scarab')
            return value
        end,
    },
    --
    -- Stackable items
    --
    stack_size = {
        inherit = false,
        field = 'stack_size',
        type = 'Integer',
        func = h.proc.number,
    },
    stack_size_currency_tab = {
        inherit = false,
        field = 'stack_size_currency_tab',
        type = 'Integer',
        func = h.proc.number,
    },
    description = {
        inherit = false,
        field = 'description',
        type = 'Text',
        func = h.proc.text,
    },
    -- Essences
    is_essence = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            value = m_util.cast.boolean(value)
            tpl_args._flags.is_essence = value
            return value
        end,
        default = false,
    },
    essence_level_restriction = {
        inherit = false,
        field = 'level_restriction',
        type = 'Integer',
        func = h.proc.number,
    },
    essence_level = {
        inherit = false,
        field = 'level',
        type = 'Integer',
        func = h.proc.number,
    },
    essence_type = {
        inherit = false,
        field = 'type',
        type = 'Integer',
        func = h.proc.number,
    },
    essence_category = {
        inherit = false,
        field = 'category',
        type = 'String',
        func = nil,
    },
    -- Oils
    is_blight_item = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_blight_item = tpl_args.blight_item_tier ~= nil
            return value
        end,
    },
    blight_item_tier = {
        inherit = false,
        field = 'tier',
        type = 'Integer',
        func = h.proc.number,
    },
    -- Fossils
    is_fossil = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_fossil = tpl_args.metadata_id and string.find(tpl_args.metadata_id, 'CurrencyDelve', 1, true) ~= nil
            return value
        end,
    },
    -- Tattoos
    is_tattoo = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_tattoo = tpl_args.tattoo_target ~= nil
            return value
        end,
    },
    tattoo_target = {
        inherit = false,
        field = 'target',
        type = 'String',
        func = nil,
    },
    tattoo_tribe = {
        inherit = false,
        field = 'tribe',
        type = 'Integer',
        func = h.proc.number,
    },
    tattoo_limit = {
        inherit = false,
        field = 'tattoo_limit',
        type = 'String',
        func = nil,
    },
    tattoo_min_adjacent = {
        inherit = false,
        field = 'min_adjacent',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    tattoo_max_adjacent = {
        inherit = false,
        field = 'max_adjacent',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    tattoo_skill_id = {
        inherit = false,
        field = 'skill_id',
        type = 'String',
        func = nil,
    },
    -- Delirium orbs
    is_delirium_orb = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_delirium_orb = tpl_args.metadata_id and string.find(tpl_args.metadata_id, 'CurrencyAfflictionOrb', 1, true) ~= nil
            return value
        end,
    },
    -- Catalysts
    is_catalyst = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_catalyst = m_util.table.contains(tpl_args.tags, 'catalyst')
            return value
        end,
    },
    -- Cosmetic items
    cosmetic_type = {
        inherit = false,
        field = 'cosmetic_type',
        type = 'String',
        func = h.proc.factory.value{
            validate = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.item.cosmetic_item_types,
                errmsg = i18n.errors.invalid_cosmetic_type,
            },
        },
    },
    cosmetic_theme = {
        inherit = false,
        field = 'theme',
        type = 'String',
        func = nil,
    },
    cosmetic_target = {
        inherit = false,
        field = 'target',
        type = 'List (,) of String',
        func = h.proc.list,
        default = {},
    },
    --
    -- Harvest seeds
    --
    seed_type_id = {
        inherit = false,
        field = 'type_id',
        type = 'String',
        func = nil,
    },
    seed_type = {
        inherit = false,
        field = 'type',
        type = 'String',
        func = function (tpl_args, value)
            if tpl_args.seed_type_id ~= 'none' or tpl_args.seed_type_id ~= nil then
                value = m_game.seed_types[tpl_args.seed_type_id]
            end
            return value
        end,
    },
    seed_type_html = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            if tpl_args.seed_type ~= nil then
                value = m_util.html.poe_color(tpl_args.seed_type_id, tpl_args.seed_type)
            end
            return value
        end,
    },
    seed_effect = {
        inherit = false,
        field = 'effect',
        type = 'Text',
        func = nil,
    },
    seed_tier = {
        inherit = false,
        field = 'tier',
        type = 'Integer',
        func = h.proc.number,
    },
    seed_growth_cycles = {
        inherit = false,
        field = 'growth_cycles',
        type = 'Integer',
        func = h.proc.number,
    },
    seed_required_nearby_seed_tier = {
        inherit = false,
        field = 'required_nearby_seed_tier',
        type = 'Integer',
        func = h.proc.number,
    },
    seed_required_nearby_seed_amount = {
        inherit = false,
        field = 'required_nearby_seed_amount',
        type = 'Integer',
        func = h.proc.number,
    },
    seed_consumed_wild_lifeforce_percentage = {
        inherit = false,
        field = 'consumed_wild_lifeforce_percentage',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    seed_consumed_vivid_lifeforce_percentage = {
        inherit = false,
        field = 'consumed_vivid_lifeforce_percentage',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    seed_consumed_primal_lifeforce_percentage = {
        inherit = false,
        field = 'consumed_primal_lifeforce_percentage',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    seed_granted_craft_option_ids = {
        inherit = false,
        field = 'granted_craft_option_ids',
        type = 'List (,) of String',
        func = h.proc.list,
        default = {},
    },
    --
    -- Harvest planet boosters
    --
    plant_booster_radius = {
        inherit = false,
        field = 'radius',
        type = 'Integer',
        func = h.proc.number,
    },
    plant_booster_lifeforce = {
        inherit = false,
        field = 'lifeforce',
        type = 'Integer',
        func = h.proc.number,
    },
    plant_booster_additional_crafting_options = {
        inherit = false,
        field = 'additional_crafting_options',
        type = 'Integer',
        func = h.proc.number,
    },
    plant_booster_extra_chances = {
        inherit = false,
        field = 'extra_chances',
        type = 'Integer',
        func = h.proc.number,
    },
    --
    -- Heist properties
    --
    heist_required_job_id = {
        field = 'required_job_id',
        type = 'String',
        func = h.proc.text,
    },
    heist_required_job_level = {
        field = 'required_job_level',
        type = 'Integer',
        func = h.proc.number,
    },
    heist_data = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            if tpl_args.heist_required_job_level then
                if tpl_args.heist_required_job_id then
                    local results = m_cargo.query(
                        {'heist_npc_skills', 'heist_jobs', 'heist_npcs'},
                        {'heist_npcs.name', 'heist_jobs.name'},
                        {
                            join = 'heist_npc_skills.job_id=heist_jobs.id, heist_npc_skills.npc_id=heist_npcs.id',
                            where = string.format('heist_npc_skills.job_id = "%s" AND heist_npc_skills.level >= %s', tpl_args.heist_required_job_id, tpl_args.heist_required_job_level),
                        }
                    )
                    local npcs = {}
                    for _, row in ipairs(results) do
                        npcs[#npcs+1] = row['heist_npcs.name']
                    end
                    tpl_args.heist_required_npcs = table.concat(npcs, ', ')
                    tpl_args.heist_required_job = results[1]['heist_jobs.name']
                else
                    tpl_args.heist_required_job = i18n.tooltips.heist_any_job
                end
            end
            return value
        end,
    },
    --
    -- Hideout decorations
    --
    is_master_doodad = {
        inherit = false,
        field = 'is_master_doodad',
        type = 'Boolean',
        func = h.proc.boolean,
        default = false,
    },
    variation_count = {
        inherit = false,
        field = 'variation_count',
        type = 'Integer',
        func = h.proc.number,
    },
    --
    -- Prophecies
    --
    is_prophecy = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, value)
            tpl_args._flags.is_prophecy = tpl_args.prophecy_id ~= nil
            return value
        end,
        default = false,
    },
    prophecy_id = {
        inherit = false,
        field = 'prophecy_id',
        type = 'String',
        func = nil,
    },
    prediction_text = {
        inherit = false,
        field = 'prediction_text',
        type = 'Text',
        func = h.proc.text,
    },
    seal_cost = {
        inherit = false,
        field = 'seal_cost',
        type = 'Integer',
        func = h.proc.number,
    },
    prophecy_reward = {
        inherit = false,
        field = 'reward',
        type = 'Text',
        func = h.proc.text,
    },
    prophecy_objective = {
        inherit = false,
        field = 'objective',
        type = 'Text',
        func = h.proc.text,
    },
    --
    -- Divination cards
    --
    card_art = {
        inherit = false,
        field = 'card_art',
        type = 'Page',
        func = function (tpl_args, value)
            return string.format(i18n.files.divination_card_art, value or tpl_args.name)
        end,
    },
    card_background = {
        inherit = false,
        field = 'card_background',
        type = 'List (,) of Integer',
        func = h.proc.factory.list{
            callback = m_util.cast.number,
        },
        default = {},
    },
    --
    -- Sentinels
    --
    sentinel_duration = {
        field = 'duration',
        type = 'Integer',
        func = h.proc.number,
    },
    sentinel_empowers = {
        field = 'empowers',
        type = 'Integer',
        func = h.proc.number,
    },
    sentinel_empowerment = {
        field = 'empowerment',
        type = 'Integer',
        func = h.proc.number,
    },
    sentinel_charge = {
        field = 'charge',
        type = 'Integer',
        func = h.proc.number,
    },
    sentinel_monster = {
        inherit = false,
        field = 'monster',
        type = 'String',
        func = nil,
    },
    sentinel_monster_level = {
        inherit = false,
        field = 'monster_level',
        type = 'Integer',
        func = h.proc.number,
    },
    --
    -- Corpse items
    --
    corpse_tier = {
        inherit = false,
        field = 'tier',
        type = 'Integer',
        func = function (tpl_args, value)
            if tpl_args.metadata_id ~= nil then
                for k, v in ipairs({'Low', 'Mid', 'High'}) do
                    if string.find(tpl_args.metadata_id, 'Metadata/Items/ItemisedCorpses/%w+' .. v) then
                        return k
                    end
                end
            end
            return nil
        end,
    },
    monster_category = {
        inherit = false,
        field = 'monster_category',
        type = 'String',
        func = h.proc.factory.value{
            validate = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.monster.categories,
                errmsg = i18n.errors.invalid_monster_category,
            },
        },
    },
    monster_category_html = {
        inherit = false,
        field = 'monster_category_html',
        type = 'Text',
        func = function (tpl_args, value)
            if tpl_args.monster_category ~= nil then
                value = mw.html.create()
                    :tag('span')
                        :addClass('mon-cat -' .. m_game.constants.monster.categories[tpl_args.monster_category].long_lower)
                        :done()
                    :wikitext(m_game.constants.monster.categories[tpl_args.monster_category].long_upper)
                value = m_util.html.poe_color('value', tostring(value))
            end
            return value
        end,
    },
    monster_abilities = {
        inherit = false,
        field = 'monster_abilities',
        type = 'Text',
        func = nil,
    },
    --
    -- Embers of the Allflame
    --
    pack_id = {
        inherit = false,
        field = 'pack_id',
        type = 'String',
        func = nil,
    },
    pack_size = {
        inherit = false,
        field = 'pack_size',
        type = nil,
        func = nil,
    },
    pack_min_size = {
        inherit = false,
        field = 'pack_min_size',
        type = 'Integer',
        func = h.proc.number,
        default = 1,
    },
    pack_max_size = {
        inherit = false,
        field = 'pack_max_size',
        type = 'Integer',
        func = h.proc.number,
        default = 1,
    },
    pack_leader_chance = {
        inherit = false,
        field = 'pack_leader_chance',
        type = 'Float',
        func = h.proc.number,
    },

    -- ------------------------------------------------------------------------
    -- derived stats
    -- ------------------------------------------------------------------------
    
    -- Populated by processing base item
    base_item_id = {
        inherit = false,
        field = 'base_item_id',
        type = 'String',
        func = nil,
    },
    base_item_page = {
        inherit = false,
        field = 'base_item_page',
        type = 'Page',
        func = nil,
    },
    base_item = {
        inherit = false,
        field = 'base_item',
        type = 'String',
        func = nil,
    },
    name_list = {
        inherit = false,
        field = 'name_list',
        type = 'List (�) of String',
        func = function (tpl_args, value)
            value = m_util.cast.table(value)
            value[#value+1] = tpl_args.name
            return value
        end,
        default = {},
    },
    frame_type = {
        inherit = false,
        field = 'frame_type',
        type = 'String',
        func = function (tpl_args, value)
            if value then
                return value
            end
            if tpl_args._flags.is_prophecy then
                return 'prophecy'
            end
            local var = cfg.class_specifics[tpl_args.class_id]
            if var ~= nil and var.frame_type ~= nil then
                return var.frame_type
            end
            return tpl_args.rarity_id
        end,
    },
    --
    -- args populated by mod validation
    --
    physical_damage_html = {
        inherit = false,
        field = 'physical_damage_html',
        type = 'Text',
        func = h.proc.factory.damage_html{type = 'physical'},
    },
    fire_damage_html = {
        inherit = false,
        field = 'fire_damage_html',
        type = 'Text',
        func = h.proc.factory.damage_html{type = 'fire'},
    },
    cold_damage_html = {
        inherit = false,
        field = 'cold_damage_html',
        type = 'Text',
        func = h.proc.factory.damage_html{type = 'cold'},
    },
    lightning_damage_html = {
        inherit = false,
        field = 'lightning_damage_html',
        type = 'Text',
        func = h.proc.factory.damage_html{type = 'lightning'},
    },
    chaos_damage_html = {
        inherit = false,
        field = 'chaos_damage_html',
        type = 'Text',
        func = h.proc.factory.damage_html{type = 'chaos'},
    },
    damage_avg = {
        inherit = false,
        field = 'damage_avg',
        type = 'Text',
        func = function (tpl_args, value)
            local min = 0
            local max = 0
            for _, damage_type in ipairs(m_game.constants.damage_type_order) do
                min = min + tpl_args[damage_type .. '_damage_min_range_average']
                max = max + tpl_args[damage_type .. '_damage_max_range_average']
            end
            value = (min + max) / 2
            return value
        end,
    },
    damage_html = {
        inherit = false,
        field = 'damage_html',
        type = 'Text',
        func = function (tpl_args, value)
            local text = {}
            for _, dkey in ipairs(m_game.constants.damage_type_order) do
                local range = tpl_args[dkey .. '_damage_html']
                if range ~= nil then
                    text[#text+1] = range
                end
            end
            if #text > 0 then
                value = table.concat(text, '<br>')
            end
            return value
        end,
    },
    jewel_radius_html = {
        inherit = false,
        field = 'radius_html',
        type = 'Text',
        func = function (tpl_args, value)
            -- Get radius from stats
            local radius = tpl_args._stats.local_jewel_effect_base_radius
            if radius then
                radius = radius.min
                local size = m_game.constants.item.jewel_radius_to_size[radius] or radius
                local color =  radius == 0 and 'mod' or 'value'
                value = m_util.html.poe_color(color, size)
            end
            return value
        end,
    },
    drop_areas_html = {
        inherit = false,
        field = 'drop_areas_html',
        type = 'Text',
        func = function (tpl_args, value)
            if tpl_args._drop_areas_data == nil then
                return value
            end
            if value ~= nil then
                return value
            end
            local areas = {}
            for _, data in pairs(tpl_args._drop_areas_data) do
                -- skip legacy maps in the drop html listing
                if not string.match(data.id, '^Map.+') or string.match(data.id, '^MapWorlds.+') or string.match(data.id, '^MapAtziri.+') then
                    areas[#areas+1] = string.format('[[%s|%s]]', data.main_page or data._pageName, data.main_page or data.name)
                end
            end
            return table.concat(areas, ' • ')
        end,
    },
    release_version = {
        inherit = false,
        field = 'release_version',
        type = 'String',
        func = nil,
    },
    removal_version = {
        inherit = false,
        field = 'removal_version',
        type = 'String',
        func = nil,
    },
    --
    -- args governing use of the template itself
    -- 
    suppress_improper_modifiers_category = {
        inherit = false,
        field = nil,
        func = h.proc.boolean,
        default = false,
    },
    disable_automatic_recipes = {
        inherit = false,
        field = nil,
        func = h.proc.boolean,
        default = false,
    },
}

core.stat_map = {
    required_level_final = {
        field = 'required_level',
        stats_add = {
            'local_level_requirement_+',
        },
        minimum = 1,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    weapon_range = {
        field = 'weapon_range',
        stats_add_distance = {
            'local_weapon_range_+',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%.1f',
        },
    },
    physical_damage_min = {
        field = 'physical_damage_min',
        stats_add = {
            'local_minimum_added_physical_damage',
        },
        stats_increased = {
            'local_physical_damage_+%',
            'quality',
        },
        stats_override = {
            ['local_weapon_no_physical_damage'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    physical_damage_max = {
        field = 'physical_damage_max',
        stats_add = {
            'local_maximum_added_physical_damage',
        },
        stats_increased = {
            'local_physical_damage_+%',
            'quality',
        },
        stats_override = {
            ['local_weapon_no_physical_damage'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    fire_damage_min = {
        field = 'fire_damage_min',
        stats_add = {
            'local_minimum_added_fire_damage',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            color = 'fire',
        },
    },
    fire_damage_max = {
        field = 'fire_damage_max',
        stats_add = {
            'local_maximum_added_fire_damage',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            color = 'fire',
        },
    },
    cold_damage_min = {
        field = 'cold_damage_min',
        stats_add = {
            'local_minimum_added_cold_damage',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            color = 'cold',
        },
    },
    cold_damage_max = {
        field = 'cold_damage_max',
        stats_add = {
            'local_maximum_added_cold_damage',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            color = 'cold',
        },
    },
    lightning_damage_min = {
        field = 'lightning_damage_min',
        stats_add = {
            'local_minimum_added_lightning_damage',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            color = 'lightning',
        },
    },
    lightning_damage_max = {
        field = 'lightning_damage_max',
        stats_add = {
            'local_maximum_added_lightning_damage',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            color = 'lightning',
        },
    },
    chaos_damage_min = {
        field = 'chaos_damage_min',
        stats_add = {
            'local_minimum_added_chaos_damage',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            color = 'chaos',
        },
    },
    chaos_damage_max = {
        field = 'chaos_damage_max',
        stats_add = {
            'local_maximum_added_chaos_damage',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            color = 'chaos',
        },
    },
    critical_strike_chance = {
        field = 'critical_strike_chance',
        stats_add = {
            'local_critical_strike_chance',
        },
        stats_increased = {
            'local_critical_strike_chance_+%',
        },
        stats_override = {
            ['local_weapon_crit_chance_is_100'] = {min=100, max=100},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%.2f',
            inline = '%s%%',
        },
    },
    attack_speed = {
        field = 'attack_speed',
        stats_increased = {
            'local_attack_speed_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%.2f',
        },
    },
    flask_life = {
        field = 'life',
        stats_add = {
            'local_flask_life_to_recover',
        },
        stats_increased = {
            'local_flask_life_to_recover_+%',
            'local_flask_amount_to_recover_+%',
            'quality',
        },
        html_fmt_options = {
            fmt = '%i',
        },
    },
    flask_mana = {
        field = 'mana',
        stats_add = {
            'local_flask_mana_to_recover',
        },
        stats_increased = {
            'local_flask_mana_to_recover_+%',
            'local_flask_amount_to_recover_+%',
            'quality',
        },
        html_fmt_options = {
            fmt = '%i',
        },
    },
    flask_duration = {
        field = 'duration',
        stats_increased = {
            'local_flask_duration_+%',
            -- regular quality isn't used here because it doesn't increase duration of life/mana/hybrid flasks
            'quality_flask_duration',
        },
        stats_increased_inverse = {
            'local_flask_recovery_speed_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%.2f',
        },
    },
    charges_per_use = {
        field = 'charges_per_use',
        stats_increased = {
            'local_charges_used_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    charges_max = {
        field = 'charges_max',
        stats_add = {
            'local_extra_max_charges',
        },
        stats_increased = {
            'local_max_charges_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    armour = {
        arg = {
            min = 'armour_min',
            max = 'armour_max',
        },
        field = '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_+%',
            'quality',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    evasion = {
        arg = {
            min = 'evasion_min',
            max = 'evasion_max',
        },
        field = 'evasion',
        stats_add = {
            'local_base_evasion_rating',
            'local_evasion_rating_and_energy_shield',
        },
        stats_increased = {
            'local_evasion_rating_+%',
            'local_evasion_and_energy_shield_+%',
            'local_armour_and_evasion_+%',
            'local_armour_and_evasion_and_energy_shield_+%',
            'quality',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    energy_shield = {
        arg = {
            min = 'energy_shield_min',
            max = 'energy_shield_max',
        },
        field = 'energy_shield',
        stats_add = {
            'local_energy_shield',
            'local_evasion_rating_and_energy_shield',
        },
        stats_increased = {
            'local_energy_shield_+%',
            'local_armour_and_energy_shield_+%',
            'local_evasion_and_energy_shield_+%',
            'local_armour_and_evasion_and_energy_shield_+%',
            'quality',
        },
        stats_override = {
            ['local_no_energy_shield'] = {min=0, max=0},
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    ward = {
        arg = {
            min = 'ward_min',
            max = 'ward_max',
        },
        field = 'ward',
        stats_add = {
            'local_ward',
        },
        stats_increased = {
            'local_ward_+%',
            'quality',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    block = {
        field = 'block',
        stats_add = {
            'local_additional_block_chance_%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
            inline = '%s%%',
        },
    },
    required_dexterity = {
        field = 'required_dexterity',
        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},
            ['local_no_attribute_requirements'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    required_intelligence = {
        field = 'required_intelligence',
        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},
            ['local_no_attribute_requirements'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    required_strength = {
        field = 'required_strength',
        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},
            ['local_no_attribute_requirements'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    map_area_level = {
        field = 'map_area_level',
        stats_override = {
            ['map_item_level_override'] = true,
        },
    },
    sentinel_duration = {
        field = 'duration',
        stats_add = {
            'local_sentinel_duration_+',
        },
        stats_increased = {
            'local_sentinel_drone_duration_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    sentinel_empowers = {
        field = 'empowers',
        stats_add = {
            'local_sentinel_tag_limit_+',
        },
        stats_increased = {
            'local_sentinel_tag_limit_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    sentinel_empowerment = {
        field = 'empowerment',
        stats_add = {
            'local_sentinel_drone_difficulty_+',
        },
        stats_increased = {
            'local_sentinel_drone_difficulty_+%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    sentinel_charge = {
        field = 'charge',
        stats_add = {
            'local_sentinel_drone_charge_+',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    pack_size = {
        arg = {
            min = 'pack_min_size',
            max = 'pack_max_size',
        },
        field = 'pack_size',
        minimum = 1,
        html_fmt_options = {
            fmt = '%i',
            fmt_range = '%s-%s',
        },
    },
}

core.dps_map = {
    physical_dps = {
        field = 'physical_dps',
        damage_args = {'physical_damage'},
        label_infobox = i18n.tooltips.physical_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'value',
        },
    },
    fire_dps = {
        field = 'fire_dps',
        damage_args = {'fire_damage'},
        label_infobox = i18n.tooltips.fire_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'fire',
        },
    },
    cold_dps = {
        field = 'cold_dps',
        damage_args = {'cold_damage'},
        label_infobox = i18n.tooltips.cold_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'cold',
        },
    },
    lightning_dps = {
        field = 'lightning_dps',
        damage_args = {'lightning_damage'},
        label_infobox = i18n.tooltips.lightning_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'lightning',
        },
    },
    chaos_dps = {
        field = 'chaos_dps',
        damage_args = {'chaos_damage'},
        label_infobox = i18n.tooltips.chaos_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'chaos',
        },
    },
    elemental_dps = {
        field = 'elemental_dps',
        damage_args = {'fire_damage', 'cold_damage', 'lightning_damage'},
        label_infobox = i18n.tooltips.elemental_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'value',
        },
    },
    poison_dps = {
        field = 'poison_dps',
        damage_args = {'physical_damage', 'chaos_damage'},
        label_infobox = i18n.tooltips.poison_dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'value',
        },
    },
    dps = {
        field = 'dps',
        damage_args = {'physical_damage', 'fire_damage', 'cold_damage', 'lightning_damage', 'chaos_damage'},
        label_infobox = i18n.tooltips.dps,
        html_fmt_options = {
            fmt = '%.1f',
            color = 'value',
        },
    },
}

return core