Module:Item2/core: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
(Undo revision 708941 by Vinifera7 (talk))
(fixed)
Line 11: Line 11:


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


-- The cfg table contains all localisable strings and configuration, to make it
-- The cfg table contains all localisable strings and configuration, to make it
Line 25: 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)
      
     --[[
    for _, modinfo in ipairs(tpl_args._mods) do
     args
         if modinfo.is_implicit == args.is_implicit then
    type  How to read data from tpl_args using the given keys. nil = Regular, gem = Skill progression, stat = Stats
             if modinfo.is_random == true then
    parts
                if random_mods[modinfo.stat_text] then
      [n]
                    table.insert(random_mods[modinfo.stat_text], modinfo)
      key  key to use. If type = gem and table is given, parse for subfield along path
                 else
      hide  Hide part if this function returns true
                    random_mods[modinfo.stat_text] = {modinfo}
      hide_key  Alternate key to use to retrieve the value
                end
      hide_default  hide the value if this is set
             else
      hide_default_key  key to use if it isn't equal to the key parameter
                 if modinfo.id == nil then
      -- from m_util.html.format_value --
                     table.insert(lines, modinfo.result)
      func  Function to transform the value retrieved from the database
                -- Allows the override of the SMW fetched mod texts for this modifier via <modtype><id>_text parameter
      fmt  Format string (or function that returns format string) to use for the value. Default: '%s'
                elseif modinfo.text ~= nil then
      fmt_range  Format string to use for the value range. Default: '(%s-%s)'
                    table.insert(lines, modinfo.text)
      color  poe_color code to use for the value range. False for no color. Default: 'mod'
                else
      class  Additional css class added to color tag
                     for _, line in ipairs(m_util.string.split(modinfo.result['mods.stat_text'] or '', '<br>')) do
      inline  Format string to use for the output
                         if line ~= '' then
      inline_color  poe_color code to use for the output. False for no color. Default: 'default'
                             if skip == nil then
      inline_class  Additional css class added to inline color tag
                                table.insert(lines, line)
    sep  If specified, parts are joined with this separator before being formatted for output
                             else
    fmt  Format string to use for output. If not specified, parts are simply concatenated
                                local skipped = false
    color  poe_color code to use for output. Default: no color
                                for _, pattern in ipairs(skip) do
    class  Additional css class added to output
                                    if string.match(line, pattern) then
     --]]
                                        skipped = true
 
                                        break
     args.parts = args.parts or {}
                                    end
     return function (tpl_args, frame)
                                 end
        local base_values = {}
                                if not skipped then
        local temp_values = {}
                                     table.insert(lines, line)
         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
        elseif args.type == 'stat' then
            -- Stats. Look for key in tpl_args._stats
            for i, data in ipairs(args.parts) do
                local value = tpl_args._stats[data.key]
                if value ~= nil then
                    base_values[i] = value.min
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
        else
            -- Regular. Look for key exactly as written in tpl_args
            for i, data in ipairs(args.parts) do
                base_values[i] = tpl_args[data.key]
                local value = {}
                if tpl_args[data.key .. '_range_minimum'] ~= nil then
                    value.min = tpl_args[data.key .. '_range_minimum']
                    value.max = tpl_args[data.key .. '_range_maximum']
                elseif tpl_args[data.key] ~= nil then
                    value.min = tpl_args[data.key]
                    value.max = tpl_args[data.key]
                end
                if value.min == nil then
                else
                    temp_values[#temp_values+1] = {value=value, index=i}
                 end
                 end
             end
             end
         end
         end
    end
       
   
         local final_values = {}
    for stat_text, modinfo_list in pairs(random_mods) do
         for i, data in ipairs(temp_values) do
         local text = {}
             local opt = args.parts[data.index]
         for _, modinfo in ipairs(modinfo_list) do
            local hide = false
             table.insert(text, modinfo.result['mods.stat_text'])
             if type(opt.hide) == 'function' then
        end
                local v = data.value
   
                 if opt.hide_key then
        local tbl = mw.html.create('table')
                     v = {
        tbl
                        min = tpl_args[opt.hide_key .. '_range_minimum'],
             :attr('class', 'random-modifier-stats mw-collapsed')
                        max = tpl_args[opt.hide_key .. '_range_maximum'],
            :attr('style', 'text-align: left')
                    }
            :tag('tr')
                     if v.min == nil or v.max == nil then
                 :tag('th')
                        v = tpl_args[opt.hide_key]
                     :attr('class', 'mw-customtoggle-31')
                     end
                     :wikitext(stat_text)
                end
                     :done()
                 hide = opt.hide(tpl_args, frame, v)
                 :done()
             elseif opt.hide_default ~= nil then
             :tag('tr')
                 if opt.hide_default_key then
                 :attr('class', 'mw-collapsible mw-collapsed')
                    local v = {
                :attr('id', 'mw-customcollapsible-31')
                        min = tpl_args[opt.hide_default_key .. '_range_minimum'],
                :tag('td')
                        max = tpl_args[opt.hide_default_key .. '_range_maximum'],
                     :wikitext(table.concat(text, '<hr style="width: 20%">'))
                    }
                    :done()
                     if v.min == nil or v.max == nil then
                :done()
                        if opt.hide_default == tpl_args[opt.hide_default_key] then
        table.insert(lines, tostring(tbl))
                            hide = true
    end
                        end
   
                    elseif opt.hide_default == v.min or opt.hide_default == v.max then
    if #lines == 0 then
                        hide = true
        return
                    end
    else
                else
        return table.concat(lines, '<br>')
                    local v = data.value
    end
                    if opt.hide_default == v.min or opt.hide_default == v.max then
end
                        hide = true
 
                    end
--  
                end
-- Factory
            end           
--
            if not hide then
 
                table.insert(final_values, data)
h.factory = {}
            end
 
        end
function h.factory.cast_text(k, args)
       
    args = args or {}
        -- all zeros = dont display and return early
    return function (tpl_args, frame)
        if #final_values == 0 then
        tpl_args[args.key_out or k] = m_util.cast.text(tpl_args[k])
            return nil
    end
        end
end
       
 
        local parts = {}
-- ----------------------------------------------------------------------------
        for i, data in ipairs(final_values) do
-- Core
            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, frame, value, options)
        end
        if args.sep then
            -- Join parts with separator before formatting
            parts = {table.concat(parts, args.sep)}
        end


local core = {}
        -- Build output string
 
        local out
core.factory = {}
        if args.fmt then
 
            out = string.format(args.fmt, unpack(parts))
function core.factory.infobox_line(args)
        else
    --[[
            out = table.concat(parts)
    args:
        end
    type: How to read data from tpl_args using the given keys. nil = Regular, gem = Skill progression, stat = Stats
        if args.color then
    parts:
            out = m_util.html.poe_color(args.color, out, args.class)
      [n]:
        elseif args.class then
      key: key to use. If type = gem and table is given, parse for subfield along path
            out = tostring(mw.html.create('em')
      allow_zero: allow zero values
                :attr('class', class)
      hide: Hide part if this function returns true
                :wikitext(out)
      hide_key: Alternate key to use to retrieve the value
            )
      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
        return out
      -- from m_util.html.format_value --
     end
      func: Function to transform the value retrieved from the database
end
      fmt: Format string (or function that returns format string) to use for the value. Default: '%s'
      fmt_range: Format string to use for the value range. Default: '(%s-%s)'
      color: poe_color code to use for the value range. False for no color. Default: 'mod'
      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: 'default'
      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 {}
function core.stats_update(tpl_args, id, value, modid, key)
    return function (tpl_args, frame)
    if tpl_args[key][id] == nil then
         local base_values = {}
         tpl_args[key][id] = {
        local temp_values = {}
            references = {modid},
        if args.type == 'gem' then
            min = value.min,
             -- Skill progression. Look for keys in tpl_args.skill_levels
             max = value.max,
             if not cfg.class_groups.gems.keys[tpl_args.class_id] then
             avg = value.avg,
                -- Skip if this item is not actually a gem
        }
                return
    else
            end
        if modid ~= nil then
             for i, data in ipairs(args.parts) do
             table.insert(tpl_args[key][id].references, modid)
                if data.key then
        end
                    local path = type(data.key) == 'table' and data.key or {data.key}
        tpl_args[key][id].min = tpl_args[key][id].min + value.min
                    -- Check for static value
        tpl_args[key][id].max = tpl_args[key][id].max + value.max
                    local value = tpl_args.skill_levels[0]
        tpl_args[key][id].avg = tpl_args[key][id].avg + value.avg
                    for _, p in ipairs(path) do -- Parse for subfield along path
    end
                        if value[p] == nil then
end
                            value = nil
 
                            break
--
                        else
-- Functions for processing tpl_args
                            value = value[p]
--
                        end
h.proc = {}
                    end
h.proc.factory = {}
                    if value ~= nil then
 
                        base_values[i] = value
function h.proc.factory.value(args)
                        temp_values[#temp_values+1] = {value={min=value,max=value}, index=i}
    args = args or {}
                    else -- Check for leveled values
    return function (tpl_args, frame, value)
                        value = {
        if value == nil then
                            min = tpl_args.skill_levels[1],
            return nil
                            max = tpl_args.skill_levels[tpl_args.max_level],
        end
                        }
        if args.cast then
                        for k, _ in pairs(value) do
            value = args.cast(value)
                            for _, p in ipairs(path) do -- Parse for subfield along path
        end
                                if value[k][p] == nil then
        if args.validate then
                                    value[k] = nil
            value = args.validate(value)
                                    break
        end
                                else
        return value
                                    value[k] = value[k][p]
    end
                                end
end
                            end
 
                        end
function h.proc.factory.list(args)
                        if value.min ~= nil and value.max ~= nil then
    args = args or {}
                            base_values[i] = value.min
    return function (tpl_args, frame, value)
                            temp_values[#temp_values+1] = {value=value, index=i}
        return m_util.cast.table(value, {
                        end
            pattern = args.pattern,
                    end
            callback = args.callback,
                end
        })
            end
    end
         elseif args.type == 'stat' then
end
             -- Stats. Look for key in tpl_args._stats
 
            for i, data in ipairs(args.parts) do
function h.proc.factory.damage_html(args)
                local value = tpl_args._stats[data.key]
    return function (tpl_args, frame, value)
                 if value ~= nil then
         local keys = {
                     base_values[i] = value.min
            min = args.type .. '_damage_min',
                    temp_values[#temp_values+1] = {value=value, index=i}
             max = args.type .. '_damage_max',
                 end
        }
             end
        range = {}
         else
        for ktype, key in pairs(keys) do
            -- Regular. Look for key exactly as written in tpl_args
            range[ktype] = core.factory.infobox_line{
             for i, data in ipairs(args.parts) do
                 parts = {
                base_values[i] = tpl_args[data.key]
                     {
                local value = {}
                        key = key,
                if tpl_args[data.key .. '_range_minimum'] ~= nil then
                        color = false,
                    value.min = tpl_args[data.key .. '_range_minimum']
                        hide_default = 0,
                    value.max = tpl_args[data.key .. '_range_maximum']
                    }
                 elseif tpl_args[data.key] ~= nil then
                 }
                     value.min = tpl_args[data.key]
             }(tpl_args, frame)
                    value.max = tpl_args[data.key]
        end
                 end
         if range.min and range.max then
                 if value.min == nil then
             local color = args.type or false
                else
            local range_fmt
                     temp_values[#temp_values+1] = {value=value, index=i}
            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
                 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
         end
         end
          
         return value
        local final_values = {}
    end
        for i, data in ipairs(temp_values) do
end
            local opt = args.parts[data.index]
 
            local hide = false
h.proc.text = h.proc.factory.value{cast = m_util.cast.text}
            if type(opt.hide) == 'function' then
h.proc.boolean = h.proc.factory.value{cast = m_util.cast.boolean}
                local v = data.value
h.proc.number = h.proc.factory.value{cast = m_util.cast.number}
                if opt.hide_key then
 
                    v = {
h.proc.percentage = h.proc.factory.value{
                        min = tpl_args[opt.hide_key .. '_range_minimum'],
    cast = function (value)
                        max = tpl_args[opt.hide_key .. '_range_maximum'],
        args = {
                    }
            min = 0,
                    if v.min == nil or v.max == nil then
            max = 100,
                        v = tpl_args[opt.hide_key]
        }
                    end
        return math.floor(m_util.cast.number(value, args))
                end
    end,
                hide = opt.hide(tpl_args, frame, v)
}
            elseif opt.hide_default ~= nil then
 
                if opt.hide_default_key then
h.proc.list = h.proc.factory.list()
                    local v = {
 
                        min = tpl_args[opt.hide_default_key .. '_range_minimum'],
-- Process mod stats
                        max = tpl_args[opt.hide_default_key .. '_range_maximum'],
function h.process_mod_stats(tpl_args, args)
                    }
    local lines = {}
                    if v.min == nil or v.max == nil then
   
                        if opt.hide_default == tpl_args[opt.hide_default_key] then
    local skip = cfg.class_specifics[tpl_args.class_id]
                            hide = true
    if skip then
                        end
        skip = skip.skip_stat_lines
                     elseif opt.hide_default == v.min or opt.hide_default == v.max then
    end
                        hide = true
   
                    end
    local random_mods = {}
   
    for _, modinfo in ipairs(tpl_args._mods) do
        if modinfo.is_implicit == args.is_implicit then
            if modinfo.is_random == true then
                if random_mods[modinfo.stat_text] then
                     table.insert(random_mods[modinfo.stat_text], modinfo)
                 else
                 else
                     local v = data.value
                     random_mods[modinfo.stat_text] = {modinfo}
                    if opt.hide_default == v.min or opt.hide_default == v.max then
                        hide = true
                    end
                 end
                 end
             end          
             else
            if not hide then
                if modinfo.id == nil then
                table.insert(final_values, data)
                    table.insert(lines, modinfo.result)
             end
                -- Allows the override of the SMW fetched mod texts for this modifier via <modtype><id>_text parameter
                elseif modinfo.text ~= nil then
                    table.insert(lines, modinfo.text)
                else
                    for _, line in ipairs(m_util.string.split(modinfo.result['mods.stat_text'] or '', '<br>')) do
                        if line ~= '' then
                            if skip == nil then
                                table.insert(lines, line)
                            else
                                local skipped = false
                                for _, pattern in ipairs(skip) do
                                    if string.match(line, pattern) then
                                        skipped = true
                                        break
                                    end
                                end
                                if not skipped then
                                    table.insert(lines, line)
                                end
                            end
                        end
                    end
                end
             end
         end
         end
       
    end
         -- all zeros = dont display and return early
   
         if #final_values == 0 then
    for stat_text, modinfo_list in pairs(random_mods) do
             return nil
         local text = {}
         for _, modinfo in ipairs(modinfo_list) do
             table.insert(text, modinfo.result['mods.stat_text'])
         end
         end
       
   
         local parts = {}
         local tbl = mw.html.create('table')
        for i, data in ipairs(final_values) do
        tbl
            local value = data.value
             :attr('class', 'random-modifier-stats mw-collapsed')
             value.base = base_values[data.index]
             :attr('style', 'text-align: left')
             local options = args.parts[data.index]
             :tag('tr')
             if args.type == 'gem' and options.color == nil then
                 :tag('th')
                 -- Display skill progression range values as unmodified (white)
                    :attr('class', 'mw-customtoggle-31')
                options.color = 'value'
                    :wikitext(stat_text)
            end
                    :done()
            parts[#parts+1] = m_util.html.format_value(tpl_args, frame, value, options)
                :done()
        end
             :tag('tr')
        if args.sep then
                :attr('class', 'mw-collapsible mw-collapsed')
            -- Join parts with separator before formatting
                 :attr('id', 'mw-customcollapsible-31')
            parts = {table.concat(parts, args.sep)}
                 :tag('td')
        end
                    :wikitext(table.concat(text, '<hr style="width: 20%">'))
 
                    :done()
        -- Build output string
                :done()
        local out
         table.insert(lines, tostring(tbl))
        if args.fmt then
    end
            out = string.format(args.fmt, unpack(parts))
   
        else
    if #lines == 0 then
            out = table.concat(parts)
        return
        end
    else
        if args.color then
         return table.concat(lines, '<br>')
             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
end
end


function core.factory.damage_html(args)
--
     return function(tpl_args, frame)
-- Argument mapping
         local keys = {
--
            min = args.key .. '_damage_min',
-- [<tpl_args key>] = {
            max = args.key .. '_damage_max',
--  inherit  boolean  Whether the item will inherit this key from its base item. Default: true
         }
--  field  string  Cargo field name
         local value = {}
--  type  string  Cargo field type
         for ktype, key in pairs(keys) do
--  func  function Function to unpack the argument into a native lua value and validate it
            value[ktype] = core.factory.infobox_line{
--  default  varies  Default value if parameter is not set
                parts = {
-- }
                    {
core.map = {
                        key = key,
    -- special params
                        color = false,
     html = {
                        hide_default = 0,
         inherit = false,
                    }
        field = 'html',
                }
        type = 'Text',
             }(tpl_args, frame)
         func = nil,
         end
    },
         if value.min and value.max then
    html_extra = {
             local color = args.key or false
         inherit = false,
            local range_fmt
         field = 'html_extra',
             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
        type = 'Text',
                 -- Variable damage range, based on modifier rolls
        func = nil,
                if args.key == 'physical' then
    },
                    color = 'mod'
    implicit_stat_text = {
                end
        field = 'implicit_stat_text',
                range_fmt = i18n.fmt.variable_damage_range
        type = 'Text',
            else
        func = function (tpl_args, frame, value)
                -- Standard damage range
             return h.process_mod_stats(tpl_args, {is_implicit=true})
                 if args.key == 'physical' then
         end,
                     color = 'value'
    },
    explicit_stat_text = {
         field = 'explicit_stat_text',
        type = 'Text',
        func = function (tpl_args, frame, value)
             local explicit = h.process_mod_stats(tpl_args, {is_implicit=false})
             if tpl_args.is_talisman or tpl_args.is_corrupted then
                 explicit = explicit or ''  
                 if explicit ~= '' then
                     explicit = explicit .. '<br> '
                 end
                 end
                 range_fmt = i18n.fmt.standard_damage_range
                 explicit = explicit .. i18n.tooltips.corrupted
             end
             end
             value = string.format(range_fmt, value.min, value.max)
             return explicit
             if color then
        end,
                 value = m_util.html.poe_color(color, value)
    },
    stat_text = {
        field = 'stat_text',
        type = 'Text',
        func = function (tpl_args, frame, value)
            local sep = ''
             if tpl_args.implicit_stat_text and tpl_args.explicit_stat_text then
                 sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
             end
             end
             tpl_args[args.key .. '_damage_html'] = value
             local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
         end
            if string.len(text) > 0 then
     end
                value = text
end
            end
 
            return value
function core.stats_update(tpl_args, id, value, modid, key)
         end,
     if tpl_args[key][id] == nil then
     },
         tpl_args[key][id] = {
     class = {
            references = {modid},
         inherit = false,
            min = value.min,
        field = 'class',
            max = value.max,
        type = 'String',
            avg = value.avg,
         func = function (tpl_args, frame, value)
         }
            local class = m_game.constants.item.classes[tpl_args.class_id]['long_upper']
    else
            -- Avoids errors with empty item class names later on
        if modid ~= nil then
            if class == '' then
            table.insert(tpl_args[key][id].references, modid)
                class = nil
        end
            end
        tpl_args[key][id].min = tpl_args[key][id].min + value.min
            return class
        tpl_args[key][id].max = tpl_args[key][id].max + value.max
        end,
        tpl_args[key][id].avg = tpl_args[key][id].avg + value.avg
    },
    end
     -- processed in build_item_classes
end
     class_id = {
 
         inherit = false,
--
         field = 'class_id',
-- argument mapping
         type = 'String',
--
         func = h.proc.factory.value{
-- format:
            validate = m_util.validate.factory.in_table_keys{
-- tpl_args key = {
                tbl = m_game.constants.item.classes,
--  no_copy = true or nil          -- When loading an base item, dont copy this key
                errmsg = i18n.errors.invalid_class_id,
--  property = 'prop',              -- Property associated with this key
                errlvl = 3,
--  property_func = function or nil -- Function to unpack the property into a native lua value.
            },
--                                      If not specified, func is used.
        },
--                                      If neither is specified, value is copied as string
--  func = function or nil          -- Function to unpack the argument into a native lua value and validate it.
--                                      If not specified, value will not be set.
--  default = object                -- Default value if the parameter is nil
-- }
core.map = {
     -- special params
     html = {
         no_copy = true,
         field = 'html',
         type = 'Text',
         func = nil,
     },
     },
     html_extra = {
     -- generic
         no_copy = true,
    rarity_id = {
         field = 'html_extra',
         inherit = false,
         type = 'Text',
         field = 'rarity_id',
         func = nil,
         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,
                errlvl = 3,
            },
        },
     },
     },
     implicit_stat_text = {
     rarity = {
         field = 'implicit_stat_text',
        inherit = false,
         type = 'Text',
         field = 'rarity',
         func = function(tpl_args, frame)
         type = 'String',
             tpl_args.implicit_stat_text = h.process_mod_stats(tpl_args, {is_implicit=true})
         func = function (tpl_args, frame, value)
             return m_game.constants.rarities[tpl_args.rarity_id]['long_upper']
         end,
         end,
     },
     },
     explicit_stat_text = {
     name = {
         field = 'explicit_stat_text',
        inherit = false,
         type = 'Text',
         field = 'name',
         func = function(tpl_args, frame)
         type = 'String',
            tpl_args.explicit_stat_text = h.process_mod_stats(tpl_args, {is_implicit=false})
         func = nil,
           
            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 = {
     size_x = {
         field = 'stat_text',
         field = 'size_x',
         type = 'Text',
         type = 'Integer',
         func = function(tpl_args, frame)
         func = h.proc.number,
            local sep = ''
            if tpl_args.implicit_stat_text and tpl_args.explicit_stat_text then
                sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
            end
            local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
           
            if string.len(text) > 0 then
                tpl_args.stat_text = text
            end
        end,
     },
     },
     class = {
     size_y = {
         no_copy = true,
         field = 'size_y',
         field = 'class',
        type = 'Integer',
         type = 'String',
        func = h.proc.number,
         func = function (tpl_args, frame)
    },
             tpl_args.class = m_game.constants.item.classes[tpl_args.class_id]['long_upper']
    drop_rarities_ids = {
             -- Avoids errors with empty item class names later on
        inherit = false,
             if tpl_args.class == '' then
         field = 'drop_rarity_ids',
                 tpl_args.class = nil
         type = 'List (,) of Text',
         func = function (tpl_args, frame, value)
             tpl_args.drop_rarities_ids = nil
            if true then return end
            -- Drop rarities only matter for base items.
            if tpl_args.rarity_id ~= 'normal' then
                return
             end
             if tpl_args.drop_rarities_ids == nil then
                tpl_args.drop_rarities_ids = {}
                return
            end
            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
                 if m_game.constants.rarities[rarity_id] == nil then
                    error(string.format(i18n.errors.invalid_rarity_id, tostring(rarity_id)))
                end
             end
             end
         end,
         end,
     },
     },
     -- processed in build_item_classes
     drop_rarities = {
    class_id = {
         inherit = false,
         no_copy = true,
         field = nil,
         field = 'class_id',
         type = 'List (,) of Text',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function (tpl_args, frame)
             tpl_args.drop_rarities = nil
             if m_game.constants.item.classes[tpl_args.class_id] == nil then
            if true then return end
                error(string.format(i18n.errors.invalid_class_id, tostring(tpl_args.class_id)))
            -- Drop rarities only matter for base items.
             end
            if tpl_args.rarity_id ~= 'normal' then
        end
                return
    },
             end
    -- generic
           
    rarity_id = {
            local rarities = {}
        no_copy = true,
            for _, rarity_id in ipairs(tpl_args.drop_rarities_ids) do
        field = 'rarity_id',
                rarities[#rarities+1] = m_game.constants.rarities[rarity_id].full
        type = 'String',
        func = function (tpl_args, frame)
            if m_game.constants.rarities[tpl_args.rarity_id] == nil then
                error(string.format(i18n.errors.invalid_rarity_id, tostring(tpl_args.rarity_id)))
             end
             end
         end
            tpl_args.drop_rarities = rarities
         end,
     },
     },
     rarity = {
     drop_enabled = {
         no_copy = true,
         inherit = false,
         field = 'rarity',
         field = 'drop_enabled',
         type = 'String',
         type = 'Boolean',
         func = function(tpl_args, frame)
         func = h.proc.boolean,
            tpl_args.rarity = m_game.constants.rarities[tpl_args.rarity_id]['long_upper']
         default = true,
         end
     },
     },
     name = {
     drop_level = {
         no_copy = true,
         inherit = false,
         field = 'name',
         field = 'drop_level',
         type = 'String',
         type = 'Integer',
         func = nil,
         func = h.proc.number,
     },
     },
     size_x = {
     drop_level_maximum = {
         field = 'size_x',
        inherit = false,
         field = 'drop_level_maximum',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('size_x'),
         func = h.proc.number,
     },
     },
     size_y = {
     drop_leagues = {
         field = 'size_y',
        inherit = false,
         type = 'Integer',
         field = 'drop_leagues',
         func = m_util.cast.factory.number('size_y'),
         type = 'List (,) of String',
         func = h.proc.factory.list{
            callback = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.leagues,
                errmsg = i18n.errors.invalid_league,
                errlvl = 4,
            },
        },
        default = {},
     },
     },
     drop_rarities_ids = {
     drop_areas = {
         no_copy = true,
         inherit = false,
         field = 'drop_rarity_ids',
         field = 'drop_areas',
         type = 'List (,) of Text',
         type = 'List (,) of String',
         func = function(tpl_args, frame)
         func = function (tpl_args, frame, value)
             tpl_args.drop_rarities_ids = nil
             if value ~= nil then
            if true then return end
                value = m_util.string.split(value, ',%s*')
            -- Drop rarities only matter for base items.
                tpl_args.drop_areas_data = m_cargo.array_query{
            if tpl_args.rarity_id ~= 'normal' then
                    tables={'areas'},
                return
                    fields={'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
            end
                    id_field='areas.id',
           
                    id_array=value,
            if tpl_args.drop_rarities_ids == nil then
                    query={limit=5000},
                tpl_args.drop_rarities_ids = {}
                 }
                 return
             end
             end
               
            -- find areas based on item tags for atlas bases
             tpl_args.drop_rarities_ids = m_util.string.split(tpl_args.drop_rarities_ids, ',%s*')
             local query_data
             for _, rarity_id in ipairs(tpl_args.drop_rarities_ids) do
             for _, tag in ipairs(tpl_args.tags or {}) do
                 if m_game.constants.rarities[rarity_id] == nil then
                query_data = nil
                     error(string.format(i18n.errors.invalid_rarity_id, tostring(rarity_id)))
                 if string.match(tag, '[%w_]*atlas[%w_]*') ~= nil and tag ~= 'atlas_base_type' then
                end
                     query_data = m_cargo.query(
            end
                        {'atlas_maps', 'maps', 'areas', 'atlas_base_item_types'},
        end,
                        {'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
    },
                        {
    drop_rarities = {
                            join='atlas_maps._pageID=maps._pageID, maps.area_id=areas.id, atlas_maps.region_id=atlas_base_item_types.region_id',
        no_copy = true,
                            where=string.format([[
        field = nil,
                                    atlas_base_item_types.tag = "%s"
        type = 'List (,) of Text',
                                    AND atlas_base_item_types.weight > 0
        func = function(tpl_args, frame)
                                    AND (
            tpl_args.drop_rarities = nil
                                        atlas_maps.map_tier0 >= tier_min AND atlas_maps.map_tier0 <= atlas_base_item_types.tier_max
            if true then return end
                                        OR atlas_maps.map_tier1 >= tier_min AND atlas_maps.map_tier1 <= atlas_base_item_types.tier_max
            -- Drop rarities only matter for base items.
                                        OR atlas_maps.map_tier2 >= tier_min AND atlas_maps.map_tier2 <= atlas_base_item_types.tier_max
            if tpl_args.rarity_id ~= 'normal' then
                                        OR atlas_maps.map_tier3 >= tier_min AND atlas_maps.map_tier3 <= atlas_base_item_types.tier_max
                return
                                        OR atlas_maps.map_tier4 >= tier_min AND atlas_maps.map_tier4 <= atlas_base_item_types.tier_max
            end
                                    )]],
           
                                tag),
            local rarities = {}
                            groupBy='areas.id',
            for _, rarity_id in ipairs(tpl_args.drop_rarities_ids) do
                        }
                rarities[#rarities+1] = m_game.constants.rarities[rarity_id].full
                    )
            end
                end
            tpl_args.drop_rarities = rarities
               
        end,
                if query_data ~= nil then
    },
                    -- in case no manual drop areas have been set
    drop_enabled = {
                    if value == nil then
        no_copy = true,
                        value = {}
        field = 'drop_enabled',
                        tpl_args.drop_areas_data = {}
        type = 'Boolean',
                    end
        func = m_util.cast.factory.boolean('drop_enabled'),
                    local drop_areas_assoc = {}
        default = true,
                    for _, id in ipairs(value) 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
                            value[#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
            return value
        end,
     },
     },
     drop_level = {
     drop_monsters = {
         no_copy = true,
         inherit = false,
         field = 'drop_level',
         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',
         type = 'Integer',
         func = m_util.cast.factory.number('drop_level'),
         func = h.proc.number,
        default = 1,
     },
     },
     drop_level_maximum = {
     required_level_final = {
        no_copy = true,
         field = 'required_level',
         field = 'drop_level_maximum',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('drop_level_maximum'),
         func = function (tpl_args, frame, value)
            value = tpl_args.required_level
            if value < cfg.base_item_required_level_threshold then
                value = 1
            end
            return value
        end,
        default = 1,
     },
     },
     drop_leagues = {
     required_dexterity = {
        no_copy = true,
         field = 'required_dexterity',
         field = 'drop_leagues',
         type = 'Integer',
         type = 'List (,) of String',
         func = h.proc.number,
         func = m_util.cast.factory.assoc_table('drop_leagues', {tbl=m_game.constants.leagues, errmsg=i18n.errors.invalid_league}),
        default = 0,
     },
     },
     drop_areas = {
     required_strength = {
         no_copy = true,
         field = 'required_strength',
         field = 'drop_areas',
        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, frame, 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]
                elseif tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
                    value = i18n.default_inventory_icons['Prophecy']
                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',
         type = 'List (,) of String',
         func = function(tpl_args, frame)
         func = function (tpl_args, frame, value)
             if tpl_args.drop_areas ~= nil then
             return m_util.cast.table(value, {
                 tpl_args.drop_areas = m_util.string.split(tpl_args.drop_areas, ',%s*')
                 callback = function (value)
                 tpl_args.drop_areas_data = m_cargo.array_query{
                    return string.format(i18n.files.inventory_icon, string.format('%s %s', tpl_args.inventory_icon_id, tostring(value)))
                    tables={'areas'},
                 end,
                    fields={'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
            })
                    id_field='areas.id',
        end,
                    id_array=tpl_args.drop_areas,
        default = {},
                    query={limit=5000},
    },
                 }
    cannot_be_traded_or_modified = {
            end
        inherit = false,
              
        field = 'cannot_be_traded_or_modified',
            -- find areas based on item tags for atlas bases
        type = 'Boolean',
            local query_data
        func = h.proc.boolean,
            for _, tag in ipairs(tpl_args.tags or {}) do
        default = false,
                query_data = nil
    },
                if string.match(tag, '[%w_]*atlas[%w_]*') ~= nil and tag ~= 'atlas_base_type' then
    help_text = {
                    query_data = m_cargo.query(
        debug_ignore_nil = true,
                        {'atlas_maps', 'maps', 'areas', 'atlas_base_item_types'},
        field = 'help_text',
                        {'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
        type = 'Text',
                        {
        func = h.proc.text,
                            join='atlas_maps._pageID=maps._pageID, maps.area_id=areas.id, atlas_maps.region_id=atlas_base_item_types.region_id',
    },
                            where=string.format([[
    flavour_text = {
                                    atlas_base_item_types.tag = "%s"  
        inherit = false,
                                    AND atlas_base_item_types.weight > 0
        field = 'flavour_text',
                                    AND (
        type = 'Text',
                                        atlas_maps.map_tier0 >= tier_min AND atlas_maps.map_tier0 <= atlas_base_item_types.tier_max
        func = h.proc.text,
                                        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
    flavour_text_id = {
                                        OR atlas_maps.map_tier3 >= tier_min AND atlas_maps.map_tier3 <= atlas_base_item_types.tier_max
        inherit = false,
                                        OR atlas_maps.map_tier4 >= tier_min AND atlas_maps.map_tier4 <= atlas_base_item_types.tier_max
        field = 'flavour_text_id',
                                    )]],
        type = 'String',
                                tag),
        func = nil,
                            groupBy='areas.id',
    },
                        }
    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',
        --type = 'String(unique; size=200)',
        func = function (tpl_args, frame, value)
            if value ~= nil 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().fullText)
                        )
                    }
                )
                if #results > 0 and tpl_args.debug_id == nil and not tpl_args.debug then
                    error(string.format(i18n.errors.duplicate_metadata, value, results[1]['items._pageName']))
                 end
                 end
               
            end
                if query_data ~= nil then
            return value
                    -- in case no manual drop areas have been set
        end,
                    if tpl_args.drop_areas == nil then
    },
                        tpl_args.drop_areas = {}
    influences = {
                        tpl_args.drop_areas_data = {}
        inherit = false,
                    end
        field = 'influences',
                    local drop_areas_assoc = {}
        type = 'List (,) of String',
                    for _, id in ipairs(tpl_args.drop_areas) do
        func = h.proc.factory.list{
                        drop_areas_assoc[id] = true
            callback = m_util.validate.factory.in_table_keys{
                    end
                tbl = m_game.constants.influences,
                   
                errmsg = i18n.errors.invalid_influence,
                    local duplicates = {}
                errlvl = 4,
                   
            },
                    for _, row in ipairs(query_data) do
        },
                        if drop_areas_assoc[row['areas.id']] == nil then
         default = {},
                            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 = {
     is_fractured = {
         no_copy = true,
         inherit = false,
         field = 'drop_monsters',
         field = 'is_fractured',
         type = 'List (,) of Text',
         type = 'Boolean',
         func = function (tpl_args, frame)
         func = h.proc.boolean,
            if tpl_args.drop_monsters ~= nil then
         default = false,
                tpl_args.drop_monsters = m_util.string.split(tpl_args.drop_monsters, ',%s*')
            end
         end,
     },
     },
     drop_text = {
     is_synthesised = {
         no_copy = true,
         inherit = false,
         field = 'drop_text',
         field = 'is_synthesised',
         type = 'Text',
         type = 'Boolean',
         func = h.factory.cast_text('drop_text'),
         func = h.proc.boolean,
        default = false,
     },
     },
     required_level = {
     is_veiled = {
         field = 'required_level_base',
        inherit = false,
         type = 'Integer',
         field = 'is_veiled',
         func = m_util.cast.factory.number('required_level'),
         type = 'Boolean',
         default = 1,
         func = h.proc.boolean,
         default = false,
     },
     },
     required_level_final = {
     is_replica = {
         field = 'required_level',
        inherit = false,
         type = 'Integer',
         field = 'is_replica',
         func = function(tpl_args, frame)
         type = 'Boolean',
             tpl_args.required_level_final = tpl_args.required_level
         func = function (tpl_args, frame, value)
             if tpl_args.required_level_final < cfg.base_item_required_level_threshold then
             value = m_util.cast.boolean(value)
                 tpl_args.required_level_final = 1
             if value == true and tpl_args.rarity_id ~= 'unique' then
                 error(string.format(i18n.errors.non_unique_flag, 'is_replica'))
             end
             end
            return value
         end,
         end,
         default = 1,
         default = false,
     },
     },
     required_dexterity = {
     is_corrupted = {
         field = 'required_dexterity',
        inherit = false,
         type = 'Integer',
         field = 'is_corrupted',
         func = m_util.cast.factory.number('required_dexterity'),
         type = 'Boolean',
         default = 0,
         func = h.proc.boolean,
         default = false,
     },
     },
     required_strength = {
     is_relic = {
         field = 'required_strength',
         inherit = false,
        type = 'Integer',
         field = 'is_relic',
        func = m_util.cast.factory.number('required_strength'),
         type = 'Boolean',
        default = 0,
         func = function (tpl_args, frame, value)
    },
             value = m_util.cast.boolean(value)
    required_intelligence = {
            if value == true and tpl_args.rarity_id ~= 'unique' then
         field = 'required_intelligence',
                error(string.format(i18n.errors.non_unique_flag, 'is_relic'))
         type = 'Integer',
        func = m_util.cast.factory.number('required_intelligence'),
        default = 0,
    },
    inventory_icon = {
        no_copy = true,
        field = 'inventory_icon',
        type = 'String',
         func = function(tpl_args, frame)
             if not tpl_args.inventory_icon then
                -- Certain types of items have default inventory icons
                if i18n.default_inventory_icons[tpl_args.class_id] then
                    tpl_args.inventory_icon = i18n.default_inventory_icons[tpl_args.class_id]
                elseif tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
                    tpl_args.inventory_icon = i18n.default_inventory_icons['Prophecy']
                end
             end
             end
             tpl_args.inventory_icon_id = tpl_args.inventory_icon or tpl_args.name
             return value
            tpl_args.inventory_icon = string.format(i18n.files.inventory_icon, tpl_args.inventory_icon_id)
         end,
         end,
        default = false,
     },
     },
     -- note: this must be called after inventory_icon to work correctly as it depends on tpl_args.inventory_icon_id being set
     is_fated = {
    alternate_art_inventory_icons = {
         inherit = false,
         no_copy = true,
         field = 'is_fated',
         field = 'alternate_art_inventory_icons',
         type = 'Boolean',
         type = 'List (,) of String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             value = m_util.cast.boolean(value)
             local icons = {}
             if value == true and tpl_args.rarity_id ~= 'unique' then
             if tpl_args.alternate_art_inventory_icons ~= nil then
                 error(string.format(i18n.errors.non_unique_flag, 'is_fated'))
                local names = m_util.string.split(tpl_args.alternate_art_inventory_icons, ',%s*')
               
                 for _, name in ipairs(names) do
                    icons[#icons+1] = string.format(i18n.files.inventory_icon, string.format('%s %s', tpl_args.inventory_icon_id, name))
                end
             end
             end
             tpl_args.alternate_art_inventory_icons = icons
             return value
         end,
         end,
        default = function (tpl_args, frame) return {} end,
    },
    cannot_be_traded_or_modified = {
        no_copy = true,
        field = 'cannot_be_traded_or_modified',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('cannot_be_traded_or_modified'),
         default = false,
         default = false,
     },
     },
     help_text = {
     is_prophecy = {
         debug_ignore_nil = true,
         inherit = false,
         field = 'help_text',
         field = nil,
         type = 'Text',
         type = nil,
         func = h.factory.cast_text('help_text'),
         func = function (tpl_args, frame, value)
            tpl_args._flags.is_prophecy = (tpl_args.metadata_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' or tpl_args.base_item == cfg.prophecy_base_item or tpl_args.base_item_page == cfg.prophecy_base_item_page or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy')
            return value
        end
     },
     },
     flavour_text = {
     is_blight_item = {
         no_copy = true,
         inherit = false,
         field = 'flavour_text',
         field = nil,
         type = 'Text',
         type = nil,
         func = h.factory.cast_text('flavour_text'),
         func = function (tpl_args, frame, value)
            tpl_args._flags.is_blight_item = (tpl_args.blight_item_tier ~= nil)
            return value
        end
     },
     },
     flavour_text_id = {
     is_drop_restricted = {
         no_copy = true,
         inherit = false,
         field = 'flavour_text_id',
         field = 'is_drop_restricted',
         type = 'String',
         type = 'Boolean',
         func = nil,
         func = h.proc.boolean,
    },
         default = function (tpl_args, frame)
    tags = {
            -- Generally items that are obtained only from specific monsters can't be obtained via cards or other means unless specifically specified
         field = 'tags',
            for _, class in ipairs({'Incubator'}) do
        type = 'List (,) of String',
                if tpl_args.class_id == class then
        func = m_util.cast.factory.assoc_table('tags', {
                    return true
            tbl = m_game.constants.tags,
                end
             errmsg = i18n.errors.invalid_tag,
             end
        }),
            for _, var in ipairs({'is_talisman', 'is_essence', 'is_fated', 'is_replica', 'is_relic', 'drop_monsters'}) do
    },
                if tpl_args[var] then
    metadata_id = {
                    return true
        no_copy = true,
                 end
        field = 'metadata_id',
        type = 'String',
        --type = 'String(unique; size=200)',
        func = function(tpl_args, frame)
            if tpl_args.metadata_id == nil then
                 return
             end
             end
             local results = m_cargo.query(
             for _, flag in ipairs({'is_prophecy', 'is_blight_item'}) do
                {'items'},
                 if tpl_args._flags[flag] then
                {'items._pageName'},
                    return true
                {
                 end
                    where=string.format('items.metadata_id="%s" AND items._pageName != "%s"', tpl_args.metadata_id, m_cargo.addslashes(mw.title.getCurrentTitle().fullText))
                 }
            )
            if #results > 0 and tpl_args.debug_id == nil and not tpl_args.debug then
                 error(string.format(i18n.errors.duplicate_metadata, tpl_args.metadata_id, results[1]['items._pageName']))
             end
             end
            return false
         end,
         end,
     },
     },
     influences = {
     purchase_costs = {
         no_copy = true,
         field = nil,
         field = 'influences',
         type = nil,
         type = 'List (,) of String',
         func = function (tpl_args, frame, value)
        func = m_util.cast.factory.assoc_table('influences', {
            local purchase_costs = {}
             tbl = m_game.constants.influences,
             for _, rarity_id in ipairs(m_game.constants.rarity_order) do
            errmsg = i18n.errors.invalid_influence,
                local rtbl = {}
        }),
                local prefix = string.format('purchase_cost_%s', rarity_id)
    },
                local i = 1
    is_fractured = {
                while i ~= -1 do
        no_copy = true,
                    local iprefix = prefix .. i
        field = 'is_fractured',
                    local values = {
        type = 'Boolean',
                        name = tpl_args[iprefix .. '_name'],
        func = m_util.cast.factory.boolean('is_fractured'),
                        amount = tonumber(tpl_args[iprefix .. '_amount']),
        default = false,
                        rarity = rarity_id,
    },
                    }
    is_synthesised = {
                    if values.name ~= nil and values.amount ~= nil then
        no_copy = true,
                        rtbl[#rtbl+1] = values
        field = 'is_synthesised',
                        i = i + 1
        type = 'Boolean',
                       
        func = m_util.cast.factory.boolean('is_synthesised'),
                        tpl_args._subobjects[#tpl_args._subobjects+1] = {
        default = false,
                            _table = 'item_purchase_costs',
    },
                            amount = values.amount,
    is_veiled = {
                            name = values.name,
        no_copy = true,
                            rarity = values.rarity,
        field = 'is_veiled',
                        }
        type = 'Boolean',
                    else
        func = m_util.cast.factory.boolean('is_veiled'),
                        i = -1
        default = false,
                    end
    },
                end
    is_replica = {
                purchase_costs[rarity_id] = rtbl
        no_copy = true,
        field = 'is_replica',
        type = 'Boolean',
        func = function(tpl_args, frame)
            m_util.cast.factory.boolean('is_replica')(tpl_args, frame)
            if tpl_args.is_replica == true and tpl_args.rarity_id ~= 'unique' then
                error(string.format(i18n.errors.non_unique_flag, 'is_replica'))
            end
        end,
        default = false,
    },
    is_corrupted = {
        no_copy = true,
        field = 'is_corrupted',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('is_corrupted'),
        default = false,
    },
    is_relic = {
        no_copy = true,
        field = 'is_relic',
        type = 'Boolean',
        func = function(tpl_args, frame)
            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
            return purchase_costs
         end,
         end,
         default = false,
         func_fetch = function (tpl_args, frame)
    },
             if tpl_args.rarity_id ~= 'unique' then
    is_fated = {
                 return
        no_copy = true,
        field = 'is_fated',
        type = 'Boolean',
        func = function(tpl_args, frame)
            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
        end,
           
        default = false,
            local results = m_cargo.query(
    },
                {'items' ,'item_purchase_costs'},
    is_prophecy = {
                {'item_purchase_costs.amount', 'item_purchase_costs.name', 'item_purchase_costs.rarity'},
        no_copy = true,
                {
        field = nil,
                    join = 'items._pageID=item_purchase_costs._pageID',
        type = nil,
                    where = string.format('items._pageName="%s" AND item_purchase_costs.rarity="unique"', tpl_args.base_item_page),
        func = function(tpl_args, frame)
                }
             tpl_args._flags.is_prophecy = (tpl_args.metadata_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' or tpl_args.base_item == cfg.prophecy_base_item or tpl_args.base_item_page == cfg.prophecy_base_item_page or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy')
            )
         end
              
            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
               
                tpl_args._subobjects[#tpl_args._subobjects+1] = {
                    _table = 'item_purchase_costs',
                    amount = values.amount,
                    name = values.name,
                    rarity = values.rarity,
                }
            end
         end,
     },
     },
     is_blight_item = {
     sell_prices_override = {
         no_copy = true,
         inherit = false,
         field = nil,
         field = nil,
         type = nil,
         type = nil,
         func = function(tpl_args, frame)
         func = function (tpl_args, frame, value)
             tpl_args._flags.is_blight_item = (tpl_args.blight_item_tier ~= nil)
            -- these variables are also used by mods when setting automatic sell prices
         end
             tpl_args.sell_prices = {}
            tpl_args.sell_price_order = {}
            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
                    tpl_args._subobjects[#tpl_args._subobjects+1] = {
                        _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,
     },
     },
     is_drop_restricted = {
     --
         no_copy  = true,
    -- specific section
         field = 'is_drop_restricted',
    --
         type = 'Boolean',
 
         func = m_util.cast.factory.boolean('is_drop_restricted'),
    -- Most item classes
         default = function(tpl_args, frame)
    quality = {
             -- Generally items that are obtained only from specific monsters can't be obtained via cards or other means unless specifically specified
         inherit = false,
             for _, class in ipairs({'Incubator'}) do
         field = 'quality',
                 if tpl_args.class_id == class then
         type = 'Integer',
                     return true
         -- Can be set manually, but default to Q20 for unique weapons/body armours
         -- Also must copy to stat for the stat adjustments to work properly
        func = function (tpl_args, frame, value)
             local quality = tonumber(value)
             if quality == nil then
                if tpl_args.rarity_id ~= 'unique' then
                    quality = 0
                 elseif cfg.class_groups.weapons.keys[tpl_args.class_id] or cfg.class_groups.armor.keys[tpl_args.class_id] then
                    quality = 20
                else
                     quality = 0
                 end
                 end
             end
             end
             for _, var in ipairs({'is_talisman', 'is_essence', 'is_fated', 'is_replica', 'is_relic', 'drop_monsters'}) do
             local stat = {
                if tpl_args[var] then
                min = quality,
                    return true
                max = quality,
                 end
                avg = quality,
            }
            core.stats_update(tpl_args, 'quality', stat, nil, '_stats')
            if tpl_args.class_id == 'UtilityFlask' or tpl_args.class_id == 'UtilityFlaskCritical' then
                core.stats_update(tpl_args, 'quality_flask_duration', stat, nil, '_stats')
            -- quality is added to quantity for maps
            elseif tpl_args.class_id == 'Map' then
                 core.stats_update(tpl_args, 'map_item_drop_quantity_+%', stat, nil, '_stats')
             end
             end
             for _, flag in ipairs({'is_prophecy', 'is_blight_item'}) do
             return quality
                if tpl_args._flags[flag] then
                    return true
                end
            end
            return false
         end,
         end,
     },
     },
     purchase_costs = {
     -- amulets
         func = function(tpl_args, frame)
    is_talisman = {
            local purchase_costs = {}
        field = 'is_talisman',
            for _, rarity_id in ipairs(m_game.constants.rarity_order) do
        type = 'Boolean',
                local rtbl = {}
         func = h.proc.boolean,
                local prefix = string.format('purchase_cost_%s', rarity_id)
        default = false,
                local i = 1
    },
                while i ~= -1 do
    talisman_tier = {
                    local iprefix = prefix .. i
        field = 'talisman_tier',
                    local values = {
        type = 'Integer',
                        name = tpl_args[iprefix .. '_name'],
        func = h.proc.number,
                        amount = tonumber(tpl_args[iprefix .. '_amount']),
    },
                        rarity = rarity_id,
    -- flasks
                    }
    charges_max = {
                    if values.name ~= nil and values.amount ~= nil then
        field = 'charges_max',
                        rtbl[#rtbl+1] = values
        type = 'Integer',
                        i = i + 1
        func = h.proc.number,
                       
    },
                        tpl_args._subobjects[#tpl_args._subobjects+1] = {
    charges_per_use = {
                            _table = 'item_purchase_costs',
        field = 'charges_per_use',
                            amount = values.amount,
        type = 'Integer',
                            name = values.name,
        func = h.proc.number,
                            rarity = values.rarity,
    },
                        }
    flask_mana = {
                    else
        field = 'mana',
                        i = -1
        type = 'Integer',
                    end
        func = h.proc.number,
                end
    },
               
    flask_life = {
                purchase_costs[rarity_id] = rtbl
        field = 'life',
            end
        type = 'Integer',
           
        func = h.proc.number,
            tpl_args.purchase_costs = purchase_costs
    },
         end,
    flask_duration = {
         func_fetch = function(tpl_args, frame)
         field = 'duration',
            if tpl_args.rarity_id ~= 'unique' then
         type = 'Float',
                return
        func = h.proc.number,
            end
    },
           
    buff_id = {
            local results = m_cargo.query(
        field = 'id',
                {'items' ,'item_purchase_costs'},
        type = 'String',
                {'item_purchase_costs.amount', 'item_purchase_costs.name', 'item_purchase_costs.rarity'},
        func = nil,
                {
    },
                    join = 'items._pageID=item_purchase_costs._pageID',
    buff_values = {
                    where = string.format('items._pageName="%s" AND item_purchase_costs.rarity="unique"', tpl_args.base_item_page),
        field = 'buff_values',
                 }
        type = 'List (,) of Integer',
             )
        func = function (tpl_args, frame, 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
              
              
             for _, row in ipairs(results) do
             -- needed so the values copyied from unique item base isn't overriden
                local values = {
            if #values >= 1 then
                    rarity = row['item_purchase_costs.rarity'],
                value = values
                    name = row['item_purchase_costs.name'],
            end
                    amount = tonumber(row['item_purchase_costs.amount']),
            return value
                }
        end,
                local datavar = tpl_args.purchase_costs[string.lower(values.rarity)]
        func_copy = function (tpl_args, frame)
                datavar[#datavar+1] = values
            tpl_args.buff_values = m_util.string.split(tpl_args.buff_values, ',%s*')
               
        end,
                tpl_args._subobjects[#tpl_args._subobjects+1] = {
        default = {},
                    _table = 'item_purchase_costs',
    },
                    amount = values.amount,
    buff_stat_text = {
                    name = values.name,
        field = 'stat_text',
                    rarity = values.rarity,
        type = 'String',
                }
        func = nil,
             end
    },
    buff_icon = {
        field = 'icon',
        type = 'String',
        func = function (tpl_args, frame, value)
             return string.format(i18n.files.status_icon, tpl_args.name)
         end,
         end,
     },
     },
     sell_prices_override = {
      
        no_copy = true,
    -- weapons
        func = function(tpl_args, frame)
    critical_strike_chance = {
            -- these variables are also used by mods when setting automatic sell prices
        field = 'critical_strike_chance',
            tpl_args.sell_prices = {}
        type = 'Float',
            tpl_args.sell_price_order = {}
        func = h.proc.number,
           
    },
           
    attack_speed = {
            local name
        field = 'attack_speed',
            local amount
        type = 'Float',
            local i = 0
        func = h.proc.number,
            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
                    tpl_args._subobjects[#tpl_args._subobjects+1] = {
                        _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
        end,
     },
     },
     --
     weapon_range = {
     -- specific section
        field = 'weapon_range',
     --
        type = 'Integer',
   
        func = h.proc.number,
     -- Most item classes
     },
     quality = {
     physical_damage_min = {
         no_copy = true,
        field = 'physical_damage_min',
         field = 'quality',
        type = 'Integer',
        func = h.proc.number,
     },
     physical_damage_max = {
         field = 'physical_damage_max',
        type = 'Integer',
        func = h.proc.number,
    },
    fire_damage_min = {
         field = 'fire_damage_min',
         type = 'Integer',
         type = 'Integer',
        -- Can be set manually, but default to Q20 for unique weapons/body armours
         func = h.proc.number,
        -- Also must copy to stat for the stat adjustments to work properly
        default = 0,
         func = function(tpl_args, frame)
            local quality = tonumber(tpl_args.quality)
            --
            if quality == nil then
                if tpl_args.rarity_id ~= 'unique' then
                    quality = 0
                elseif cfg.class_groups.weapons.keys[tpl_args.class_id] or cfg.class_groups.armor.keys[tpl_args.class_id] then
                    quality = 20
                else
                    quality = 0
                end
            end
           
            tpl_args.quality = quality
           
            local stat = {
                min = quality,
                max = quality,
                avg = quality,
            }
           
            core.stats_update(tpl_args, 'quality', stat, nil, '_stats')
           
            if tpl_args.class_id == 'UtilityFlask' or tpl_args.class_id == 'UtilityFlaskCritical' then
                core.stats_update(tpl_args, 'quality_flask_duration', stat, nil, '_stats')
            -- quality is added to quantity for maps
            elseif tpl_args.class_id == 'Map' then
                core.stats_update(tpl_args, 'map_item_drop_quantity_+%', stat, nil, '_stats')
            end
        end,
     },
     },
     -- amulets
     fire_damage_max = {
    is_talisman = {
         field = 'fire_damage_max',
        field = 'is_talisman',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('is_talisman'),
        default = false,
    },
   
    talisman_tier = {
         field = 'talisman_tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('talisman_tier'),
         func = h.proc.number,
        default = 0,
     },
     },
      
     cold_damage_min = {
    -- flasks
         field = 'cold_damage_min',
    charges_max = {
         field = 'charges_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('charges_max'),
         func = h.proc.number,
        default = 0,
     },
     },
     charges_per_use = {
     cold_damage_max = {
         field = 'charges_per_use',
         field = 'cold_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('charges_per_use'),
         func = h.proc.number,
        default = 0,
     },
     },
     flask_mana = {
     lightning_damage_min = {
         field = 'mana',
         field = 'lightning_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('flask_mana'),
         func = h.proc.number,
        default = 0,
     },
     },
     flask_life = {
     lightning_damage_max = {
         field = 'life',
         field = 'lightning_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('flask_life'),
         func = h.proc.number,
        default = 0,
     },
     },
     flask_duration = {
     chaos_damage_min = {
         field = 'duration',
         field = 'chaos_damage_min',
         type = 'Float',
         type = 'Integer',
         func = m_util.cast.factory.number('flask_duration'),
         func = h.proc.number,
        default = 0,
     },
     },
     buff_id = {
     chaos_damage_max = {
         field = 'id',
         field = 'chaos_damage_max',
         type = 'String',
         type = 'Integer',
         func = nil,
         func = h.proc.number,
        default = 0,
     },
     },
     buff_values = {
     -- armor-type stuff
         field = 'buff_values',
    armour = {
         type = 'List (,) of Integer',
         field = 'armour',
         func = function(tpl_args, frame)
         type = 'Integer',
            local values = {}
         func = h.proc.number,
            local i = 0
         default = 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
                tpl_args.buff_values = values
            end
        end,
        func_copy = function(tpl_args, frame)
            tpl_args.buff_values = m_util.string.split(tpl_args.buff_values, ',%s*')
        end,
         default = function (tpl_args, frame) return {} end,
     },
     },
     buff_stat_text = {
     energy_shield = {
         field = 'stat_text',
         field = 'energy_shield',
        type = 'String',
        func = nil,
    },
    buff_icon = {
        field = 'icon',
        type = 'String',
        func = function(tpl_args, frame)
            tpl_args.buff_icon = string.format(i18n.files.status_icon, tpl_args.name)
        end,
    },
   
    -- weapons
    critical_strike_chance = {
        field = 'critical_strike_chance',
        type = 'Float',
        func = m_util.cast.factory.number('critical_strike_chance'),
    },
    attack_speed = {
        field = 'attack_speed',
        type = 'Float',
        func = m_util.cast.factory.number('attack_speed'),
    },
    weapon_range = {
        field = 'weapon_range',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('weapon_range'),
         func = h.proc.number,
        default = 0,
     },
     },
     physical_damage_min = {
     evasion = {
         field = 'physical_damage_min',
         field = 'evasion',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('physical_damage_min'),
         func = h.proc.number,
        default = 0,
     },
     },
     physical_damage_max = {
     -- This is the inherent penality from the armour piece if any
         field = 'physical_damage_max',
    movement_speed = {
         field = 'movement_speed',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('physical_damage_max'),
         func = h.proc.number,
        default = 0,
     },
     },
     fire_damage_min = {
     -- shields
         field = 'fire_damage_min',
    block = {
         field = 'block',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('fire_damage_min'),
         func = h.proc.number,
         default = 0,
    },
    -- skill gem stuff
    gem_description = {
        field = 'gem_description',
         type = 'Text',
        func = h.proc.text,
     },
     },
     fire_damage_max = {
     dexterity_percent = {
         field = 'fire_damage_max',
         field = 'dexterity_percent',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('fire_damage_max'),
         func = h.proc.percentage,
        default = 0,
     },
     },
     cold_damage_min = {
     strength_percent = {
         field = 'cold_damage_min',
         field = 'strength_percent',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('cold_damage_min'),
         func = h.proc.percentage,
        default = 0,
     },
     },
     cold_damage_max = {
     intelligence_percent = {
         field = 'cold_damage_max',
         field = 'intelligence_percent',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('cold_damage_max'),
         func = h.proc.percentage,
        default = 0,
     },
     },
     lightning_damage_min = {
     primary_attribute = {
         field = 'lightning_damage_min',
         field = 'primary_attribute',
         type = 'Integer',
         type = 'String',
         func = m_util.cast.factory.number('lightning_damage_min'),
         func = function (tpl_args, frame, value)
         default = 0,
            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,
     },
     },
     lightning_damage_max = {
     gem_tags = {
         field = 'lightning_damage_max',
         field = 'gem_tags',
         type = 'Integer',
         type = 'List (,) of String',
         func = m_util.cast.factory.number('lightning_damage_max'),
         func = h.proc.factory.list{
         default = 0,
            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 = {},
     },
     },
     chaos_damage_min = {
     -- Support gems only
         field = 'chaos_damage_min',
    support_gem_letter = {
         type = 'Integer',
         field = 'support_gem_letter',
         func = m_util.cast.factory.number('chaos_damage_min'),
         type = 'String(size=1)',
        default = 0,
         func = nil,
     },
     },
     chaos_damage_max = {
     support_gem_letter_html = {
         field = 'chaos_damage_max',
         field = 'support_gem_letter_html',
         type = 'Integer',
         type = 'Text',
         func = m_util.cast.factory.number('chaos_damage_max'),
         func = function (tpl_args, frame, value)
         default = 0,
            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', k)
                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,
     },
     },
     -- armor-type stuff
     --
     armour = {
    -- Maps
         field = 'armour',
    --
     map_tier = {
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('armour'),
         func = h.proc.number,
        default = 0,
     },
     },
     energy_shield = {
     map_guild_character = {
         field = 'energy_shield',
         field = 'guild_character',
         type = 'Integer',
         type = 'String(size=1)',
         func = m_util.cast.factory.number('energy_shield'),
         func = nil,
        default = 0,
     },
     },
     evasion = {
     map_area_id = {
         field = 'evasion',
         field = 'area_id',
         type = 'Integer',
         type = 'String',
         func = m_util.cast.factory.number('evasion'),
         func = nil, -- TODO: Validate against a query?
        default = 0,
     },
     },
     -- This is the inherent penality from the armour piece if any
     map_area_level = {
    movement_speed = {
         field = 'area_level',
         field = 'movement_speed',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('movement_speed'),
         func = h.proc.number,
        default = 0,
     },
     },
     -- shields
     unique_map_guild_character = {
    block = {
         field = 'unique_guild_character',
         field = 'block',
         type = 'String(size=1)',
         type = 'Integer',
         func = nil,
         func = m_util.cast.factory.number('block'),
        func_copy = function (tpl_args, frame)
            tpl_args.map_guild_character = tpl_args.unique_map_guild_character
        end,
     },
     },
     -- skill gem stuff
     unique_map_area_id = {
    gem_description = {
         field = 'unique_area_id',
         field = 'gem_description',
         type = 'String',
         type = 'Text',
         func = nil, -- TODO: Validate against a query?
         func = h.factory.cast_text('gem_description'),
        func_copy = function (tpl_args, frame)
            tpl_args.map_area_id = tpl_args.unique_map_area_id
        end,
     },
     },
     dexterity_percent = {
     unique_map_area_level = {
         field = 'dexterity_percent',
         field = 'unique_area_level',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.percentage('dexterity_percent'),
         func = h.proc.number,
        func_copy = function (tpl_args, frame)
            tpl_args.map_area_level = tpl_args.unique_map_area_level
        end,
     },
     },
     strength_percent = {
     map_series = {
         field = 'strength_percent',
         field = 'series',
        type = 'Integer',
        func = m_util.cast.factory.percentage('strength_percent'),
    },
    intelligence_percent = {
        field = 'intelligence_percent',
        type = 'Integer',
        func = m_util.cast.factory.percentage('intelligence_percent'),
    },
    primary_attribute = {
        field = 'primary_attribute',
         type = 'String',
         type = 'String',
         func = function(tpl_args, frame)
         func = function (tpl_args, frame, value)
             for _, attr in ipairs(m_game.constants.attribute_order) do
             if tpl_args.rarity == 'normal' and value == nil then
                local val = tpl_args[attr .. '_percent']
                error(string.format(i18n.errors.generic_required_parameter, 'map_series'))
                if val and val >= 60 then
                    tpl_args['primary_attribute'] = attr
                    return
                end
             end
             end
             tpl_args['primary_attribute'] = 'none'
             return value
         end,
         end,
     },
     },
     gem_tags = {
     -- atlas info is only for the current map series
         field = 'gem_tags',
    atlas_x = {
         type = 'List (,) of String',
         field = 'x',
        -- TODO: default rework
         type = 'Float',
         func = function(tpl_args, frame)
         func = h.proc.number,
            if tpl_args.gem_tags then
    },
                tpl_args.gem_tags = m_util.string.split(tpl_args.gem_tags, ',%s*')
    atlas_y = {
            end
        field = 'y',
         end,
         type = 'Float',
         default = function (tpl_args, frame) return {} end,
         func = h.proc.number,
     },
     },
     -- Support gems only
     atlas_region_id = {
    support_gem_letter = {
         field = 'region_id',
         field = 'support_gem_letter',
         type = 'String',
         type = 'String(size=1)',
         func = nil,
         func = nil,
     },
     },
     support_gem_letter_html = {
     atlas_region_minimum = {
         field = 'support_gem_letter_html',
         field = 'region_minimum',
         type = 'Text',
         type = 'Integer',
         func = function(tpl_args, frame)
         func = h.proc.number,
            if tpl_args.support_gem_letter == nil then
    },
                return
    atlas_x0 = {
            end
        field = 'x0',
       
        type = 'Float',
            -- TODO replace this with a loop possibly
        func = h.proc.number,
            local css_map = {
                strength = 'red',
                intelligence = 'blue',
                dexterity = 'green',
            }
            local id
            for k, v in pairs(css_map) do
                k = string.format('%s_percent', k)
                if tpl_args[k] and tpl_args[k] > 50 then
                    id = v
                    break
                end
            end
           
            if id ~= nil then
                local container = mw.html.create('span')
                container
                    :attr('class', string.format('support-gem-id-%s', id))
                    :wikitext(tpl_args.support_gem_letter)
                    :done()
                tpl_args.support_gem_letter_html = tostring(container)
            end
        end,
     },
     },
     --
     atlas_x1 = {
    -- Maps
         field = 'x1',
    --
         type = 'Float',
    map_tier = {
         func = h.proc.number,
         field = 'tier',
         type = 'Integer',
         func = m_util.cast.factory.number('map_tier'),
     },
     },
     map_guild_character = {
     atlas_x2 = {
         field = 'guild_character',
         field = 'x2',
         type = 'String(size=1)',
         type = 'Float',
         func = nil,
         func = h.proc.number,
     },
     },
     map_area_id = {
     atlas_x3 = {
         field = 'area_id',
         field = 'x3',
         type = 'String',
         type = 'Float',
         func = nil, -- TODO: Validate against a query?
         func = h.proc.number,
     },
     },
     map_area_level = {
     atlas_x4 = {
         field = 'area_level',
         field = 'x4',
         type = 'Integer',
         type = 'Float',
         func = m_util.cast.factory.number('map_area_level'),
         func = h.proc.number,
     },
     },
     unique_map_guild_character = {
     atlas_y0 = {
         field = 'unique_guild_character',
         field = 'y0',
         type = 'String(size=1)',
         type = 'Float',
         func_copy = function(tpl_args, frame)
         func = h.proc.number,
            tpl_args.map_guild_character = tpl_args.unique_map_guild_character
        end,
        func = nil,
     },
     },
     unique_map_area_id = {
     atlas_y1 = {
         field = 'unique_area_id',
         field = 'y1',
         type = 'String',
         type = 'Float',
         func = nil, -- TODO: Validate against a query?
         func = h.proc.number,
        func_copy = function(tpl_args, frame)
            tpl_args.map_area_id = tpl_args.unique_map_area_id
        end,
     },
     },
     unique_map_area_level = {
     atlas_y2 = {
         field = 'unique_area_level',
         field = 'y2',
         type = 'Integer',
         type = 'Float',
         func = m_util.cast.factory.number('unique_map_area_level'),
         func = h.proc.number,
        func_copy = function(tpl_args, frame)
            tpl_args.map_area_level = tpl_args.unique_map_area_level
        end,
     },
     },
     map_series = {
     atlas_y3 = {
         field = 'series',
         field = 'y3',
        type = 'String',
        func = function(tpl_args, frame)
            if tpl_args.rarity == 'normal' and tpl_args.map_series == nil then
                error(string.format(i18n.errors.generic_required_parameter, 'map_series'))
            end
        end,
    },
    -- atlas info is only for the current map series
    atlas_x = {
        field = 'x',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_x'),
         func = h.proc.number,
     },
     },
     atlas_y = {
     atlas_y4 = {
         field = 'y',
         field = 'y4',
         type = 'Float',
         type = 'Float',
         func = m_util.cast.factory.number('atlas_y'),
         func = h.proc.number,
     },
     },
     atlas_region_id = {
     atlas_map_tier0 = {
         field = 'region_id',
         field = 'map_tier0',
        type = 'String',
        func = nil,
    },
    atlas_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_map_tier1 = {
         field = 'x0',
         field = 'map_tier1',
         type = 'Float',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_x0'),
         func = h.proc.number,
     },
     },
     atlas_x1 = {
     atlas_map_tier2 = {
         field = 'x1',
         field = 'map_tier2',
         type = 'Float',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_x1'),
         func = h.proc.number,
     },
     },
     atlas_x2 = {
     atlas_map_tier3 = {
         field = 'x2',
         field = 'map_tier3',
         type = 'Float',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_x2'),
         func = h.proc.number,
     },
     },
     atlas_x3 = {
     atlas_map_tier4 = {
         field = 'x3',
         field = 'map_tier4',
         type = 'Float',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_x3'),
         func = h.proc.number,
     },
     },
     atlas_x4 = {
     atlas_connections = {
         field = 'x4',
         field = nil,
        type = 'Float',
        type = nil,
        func = m_util.cast.factory.number('atlas_x4'),
        func = function (tpl_args, frame, 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._subobjects, data)
                else
                    cont = false
                    if i == 1 then
                        value = nil
                    end
                end
                i = i + 1
            end
            return value
        end,
     },
     },
     atlas_y0 = {
     --
         field = 'y0',
    -- Currency-like items
         type = 'Float',
    --
         func = m_util.cast.factory.number('atlas_0'),
    stack_size = {
         field = 'stack_size',
         type = 'Integer',
         func = h.proc.number,
     },
     },
     atlas_y1 = {
     stack_size_currency_tab = {
         field = 'y1',
         field = 'stack_size_currency_tab',
         type = 'Float',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_y1'),
         func = h.proc.number,
     },
     },
     atlas_y2 = {
     description = {
         field = 'y2',
         field = 'description',
         type = 'Float',
         type = 'Text',
         func = m_util.cast.factory.number('atlas_y2'),
         func = h.proc.text,
     },
     },
     atlas_y3 = {
     cosmetic_type = {
         field = 'y3',
         field = 'cosmetic_type',
         type = 'Float',
         type = 'String',
         func = m_util.cast.factory.number('atlas_y3'),
         func = h.proc.text,
     },
     },
     atlas_y4 = {
     -- for essences
         field = 'y4',
    is_essence = {
         type = 'Float',
         field = nil,
         func = m_util.cast.factory.number('atlas_y4'),
         type = nil,
         func = h.proc.boolean,
        default = false,
     },
     },
     atlas_map_tier0 = {
     essence_level_restriction = {
         field = 'map_tier0',
         field = 'level_restriction',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier0'),
         func = h.proc.number,
     },
     },
     atlas_map_tier1 = {
     essence_level = {
         field = 'map_tier1',
         field = 'level',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier1'),
         func = h.proc.number,
     },
     },
     atlas_map_tier2 = {
     essence_type = {
         field = 'map_tier2',
         field = 'type',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier2'),
         func = h.proc.number,
     },
     },
     atlas_map_tier3 = {
     essence_category = {
         field = 'map_tier3',
         field = 'category',
         type = 'Integer',
         type = 'String',
         func = m_util.cast.factory.number('atlas_map_tier3'),
         func = nil,
     },
     },
     atlas_map_tier4 = {
     -- blight crafting items (i.e. oils)
         field = 'map_tier4',
    blight_item_tier = {
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('atlas_map_tier4'),
         func = h.proc.number,
     },
     },
     atlas_connections = {
     -- harvest seeds
    seed_type_id = {
        field = 'type_id',
        type = 'String',
        func = nil,
    },
    seed_type = {
        field = 'type',
        type = 'String',
        func = function (tpl_args, frame, 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 = {
         field = nil,
         field = nil,
         type = nil,
         type = nil,
         func = function(tpl_args, frame)
         func = function (tpl_args, frame, value)
             tpl_args.atlas_connections = {}
             if tpl_args.seed_type ~= nil then
           
                value = m_util.html.poe_color(tpl_args.seed_type_id, tpl_args.seed_type)
             local cont = true
             end
             local i = 1
             return value
            while cont do
        end
                local prefix = string.format('atlas_connection%s_', i)
    },
                local regions = tpl_args[prefix .. 'tier']
    seed_effect = {
                local data = {
        field = 'effect',
                    _table = 'atlas_connections',
        type = 'Text',
                    map1 = string.format('%s (%s)', tpl_args.name, tpl_args.map_series or ''),
        func = nil,
                    map2 = tpl_args[prefix .. 'target'],
    },
                }
    seed_tier = {
               
        field = 'tier',
                if regions and data.map2 then
        type = 'Integer',
                    regions = m_util.string.split(regions, ',%s*')
        func = h.proc.number,
                    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
                   
                    tpl_args.atlas_connections[data.map2] = data
                    table.insert(tpl_args._subobjects, data)
                else
                    cont = false
                    if i == 1 then
                        tpl_args.atlas_connections = nil
                    end
                end
               
                i = i + 1
            end
        end,
        default = nil,
     },
     },
     --
     seed_growth_cycles = {
    -- Currency-like items
         field = 'growth_cycles',
    --
    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 = {
     seed_required_nearby_seed_tier = {
         field = 'stack_size_currency_tab',
         field = 'required_nearby_seed_tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('stack_size_currency_tab'),
         func = h.proc.number,
     },
     },
     description = {
     seed_required_nearby_seed_amount = {
         field = 'description',
         field = 'required_nearby_seed_amount',
        type = 'Text',
        func = h.factory.cast_text('description'),
    },
    cosmetic_type = {
        field = 'cosmetic_type',
        type = 'String',
        func = h.factory.cast_text('cosmetic_type'),
    },
    -- for essences
    is_essence = {
        field = nil,
        func = m_util.cast.factory.boolean('is_essence'),
        default = false,
    },
    essence_level_restriction = {
        field = 'level_restriction',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('essence_level_restriction'),
         func = h.proc.number,
     },
     },
     essence_level = {
     seed_consumed_wild_lifeforce_percentage = {
         field = 'level',
         field = 'consumed_wild_lifeforce_percentage',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('essence_level'),
         func = h.proc.number,
        default = 0,
     },
     },
     essence_type = {
     seed_consumed_vivid_lifeforce_percentage = {
         field = 'type',
         field = 'consumed_vivid_lifeforce_percentage',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('essence_type'),
         func = h.proc.number,
        default = 0,
     },
     },
     essence_category = {
     seed_consumed_primal_lifeforce_percentage = {
         field = 'category',
         field = 'consumed_primal_lifeforce_percentage',
         type = 'String',
         type = 'Integer',
         func = nil,
        func = h.proc.number,
        default = 0,
    },
    seed_granted_craft_option_ids = {
        field = 'granted_craft_option_ids',
        type = 'List (,) of String',
         func = h.proc.list,
        default = {},
     },
     },
     -- blight crafting items (i.e. oils)
     --
     blight_item_tier = {
     -- harvest planet boosters
         field = 'tier',
    --
    plant_booster_radius = {
         field = 'radius',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('blight_item_tier'),
         func = h.proc.number,
     },
     },
     -- harvest seeds
     plant_booster_lifeforce = {
    seed_type_id = {
         field = 'lifeforce',
        field = 'type_id',
        type = 'String',
    },
    seed_type = {
        field = 'type',
        type = 'String',
        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,
        type = nil,
        func = function (tpl_args, frame)
            if tpl_args.seed_type ~= nil then
                tpl_args.seed_type_html = m_util.html.poe_color(tpl_args.seed_type_id, tpl_args.seed_type)
            end
        end
    },
    seed_effect = {
        field = 'effect',
        type = 'Text',
    },
    seed_tier = {
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('seed_tier'),
         func = h.proc.number,
     },
     },
     seed_growth_cycles = {
     plant_booster_additional_crafting_options = {
         field = 'growth_cycles',
         field = 'additional_crafting_options',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('seed_growth_cycles'),
         func = h.proc.number,
     },
     },
     seed_required_nearby_seed_tier = {
     plant_booster_extra_chances = {
         field = 'required_nearby_seed_tier',
         field = 'extra_chances',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('seed_required_nearby_seed_tier'),
         func = h.proc.number,
     },
     },
     seed_required_nearby_seed_amount = {
     --
         field = 'required_nearby_seed_amount',
    -- Heist properties
         type = 'Integer',
    --
         func = m_util.cast.factory.number('seed_required_nearby_seed_amount'),
    heist_required_job_id = {
         field = 'required_job_id',
         type = 'String',
         func = h.proc.text,
     },
     },
     seed_consumed_wild_lifeforce_percentage = {
     heist_required_job_level = {
         field = 'consumed_wild_lifeforce_percentage',
         field = 'required_job_level',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('seed_consumed_wild_lifeforce_percentage'),
         func = h.proc.number,
        default = 0,
     },
     },
     seed_consumed_vivid_lifeforce_percentage = {
     heist_data = {
         field = 'consumed_vivid_lifeforce_percentage',
         field = nil,
         type = 'Integer',
         type = nil,
         func = m_util.cast.factory.number('seed_consumed_vivid_lifeforce_percentage'),
         func = function (tpl_args, frame, value)
        default = 0,
            if tpl_args.heist_required_job_level then
    },
                if tpl_args.heist_required_job_id then
    seed_consumed_primal_lifeforce_percentage = {
                    local results = m_cargo.query(
        field = 'consumed_primal_lifeforce_percentage',
                        {'heist_jobs', 'heist_npcs', 'heist_npc_skills'},
        type = 'Integer',
                        {'heist_npcs.name', 'heist_jobs.name'},
        func = m_util.cast.factory.number('seed_consumed_primal_lifeforce_percentage'),
                        {
        default = 0,
                            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),
    seed_granted_craft_option_ids = {
                        }
        field = 'granted_craft_option_ids',
                    )
        type = 'List (,) of String',
                    local npcs = {}
        func = m_util.cast.factory.number('seed_grandted_craft_option_ids'),
                    for _, row in ipairs(results) do
         default = 0,
                        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,
     },
     },
     --
     --
     -- harvest planet boosters
     -- hideout doodads (HideoutDoodads.dat)
     --
     --
     plant_booster_radius = {
     is_master_doodad = {
         field = 'radius',
         field = 'is_master_doodad',
         type = 'Integer',
         type = 'Boolean',
         func = m_util.cast.factory.number('plant_booster_radius'),
         func = h.proc.boolean,
     },
     },
     plant_booster_lifeforce = {
     master = {
         field = 'lifeforce',
         field = 'master',
        type = 'String',
        func = function (tpl_args, frame, value)
            if value == nil then
                return nil
            end
            return m_util.validate.factory.in_table_keys{
                tbl = m_util.table.column(m_game.constants.masters, 'long_upper', 'full'),
                errmsg = i18n.errors.invalid_master,
                errlvl = 3,
            }(value)
        end,
    },
    master_level_requirement = {
        field = 'level_requirement',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('plant_booster_lifeforce'),
         func = h.proc.number,
     },
     },
     plant_booster_additional_crafting_options = {
     master_favour_cost = {
         field = 'additional_crafting_options',
         field = 'favour_cost',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('plant_booster_additional_crafting_options'),
         func = h.proc.number,
     },
     },
     plant_booster_extra_chances = {
     variation_count = {
         field = 'extra_chances',
         field = 'variation_count',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('plant_booster_extra_chances'),
         func = h.proc.number,
     },
     },
     --
     -- Propehcy
     -- Heist properties
     prophecy_id = {
    --
         field = 'prophecy_id',
    heist_required_job_id = {
         field = 'required_job_id',
         type = 'String',
         type = 'String',
         func = h.factory.cast_text('heist_required_job_id'),
         func = nil,
     },
     },
     heist_required_job_level = {
     prediction_text = {
         field = 'required_job_level',
         field = 'prediction_text',
        type = 'Text',
        func = h.proc.text,
    },
    seal_cost = {
        field = 'seal_cost',
         type = 'Integer',
         type = 'Integer',
         func = m_util.cast.factory.number('heist_required_job_level'),
         func = h.proc.number,
    },
    prophecy_reward = {
        field = 'reward',
        type = 'Text',
        func = h.proc.text,
     },
     },
     heist_data = {
     prophecy_objective = {
         func = function (tpl_args, frame)
         field = 'objective',
            if tpl_args.heist_required_job_level then
        type = 'Text',
                if tpl_args.heist_required_job_id then
        func = h.proc.text,
                    local results = m_cargo.query(
    },
                        {'heist_jobs', 'heist_npcs', 'heist_npc_skills'},
    -- Divination cards
                        {'heist_npcs.name', 'heist_jobs.name'},
    card_art = {
                        {
        field = 'card_art',
                            join = 'heist_npc_skills.job_id=heist_jobs.id, heist_npc_skills.npc_id=heist_npcs.id',
        type = 'Page',
                            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),
        func = function (tpl_args, frame, value)
                        }
            return string.format(i18n.files.divination_card_art, value or tpl_args.name)
                    )
         end,
                   
                    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,
     },
     },
     --
     -- ------------------------------------------------------------------------
     -- hideout doodads (HideoutDoodads.dat)
     -- derived stats
     --
     -- ------------------------------------------------------------------------
     is_master_doodad = {
      
        field = 'is_master_doodad',
    -- For rarity != normal, rarity already verified
        type = 'Boolean',
    base_item = {
         func = m_util.cast.factory.boolean('is_master_doodad'),
         inherit = false,
    },
         field = 'base_item',
    master = {
         field = 'master',
         type = 'String',
         type = 'String',
        -- todo validate against list of master names
         func = function (tpl_args, frame, value)
         func = m_util.cast.factory.table('master', {key='full', tbl=m_game.constants.masters}),
            return tpl_args.base_item_data['items.name']
        end,
     },
     },
     master_level_requirement = {
     base_item_id = {
         field = 'level_requirement',
        inherit = false,
         type = 'Integer',
         field = 'base_item_id',
         func = m_util.cast.factory.number('master_level_requirement'),
         type = 'String',
         func = function (tpl_args, frame, value)
            return tpl_args.base_item_data['items.metadata_id']
        end,
     },
     },
     master_favour_cost = {
     base_item_page = {
         field = 'favour_cost',
        inherit = false,
         type = 'Integer',
         field = 'base_item_page',
         func = m_util.cast.factory.number('master_favour_cost'),
         type = 'Page',
         func = function (tpl_args, frame, value)
            return tpl_args.base_item_data['items._pageName']
        end,
     },
     },
     variation_count = {
     name_list = {
         field = 'variation_count',
        inherit = false,
         type = 'Integer',
         field = 'name_list',
         func = m_util.cast.factory.number('variation_count'),
         type = 'List (�) of String',
         func = function (tpl_args, frame, value)
            value = m_util.cast.table(value)
            value[#value+1] = tpl_args.name
            return value
        end,
        default = {},
     },
     },
     -- Propehcy
     frame_type = {
    prophecy_id = {
        inherit = false,
         field = 'prophecy_id',
         field = 'frame_type',
         type = 'String',
         type = 'String',
         func = nil,
         func = function (tpl_args, frame, value)
    },
            if value then
    prediction_text = {
                return value
        field = 'prediction_text',
            end
        type = 'Text',
            if tpl_args._flags.is_prophecy then
        func = h.factory.cast_text('prediction_text'),
                return 'prophecy'
    },
            end
    seal_cost = {
            local var = cfg.class_specifics[tpl_args.class_id]
        field = 'seal_cost',
            if var ~= nil and var.frame_type ~= nil then
        type = 'Integer',
                return var.frame_type
        func = m_util.cast.factory.number('seal_cost'),
            end
    },
            if tpl_args.is_relic then
    prophecy_reward = {
                return 'relic'
        field = 'reward',
            end
        type = 'Text',
             return tpl_args.rarity_id
        func = h.factory.cast_text('prophecy_reward'),
    },
    prophecy_objective = {
        field = 'objective',
        type = 'Text',
      func = h.factory.cast_text('prophecy_objective'),
    },
    -- Divination cards
    card_art = {
        field = 'card_art',
        type = 'Page',
        func = function(tpl_args, frame)
             tpl_args.card_art = string.format(i18n.files.divination_card_art, tpl_args.card_art or tpl_args.name)
         end,
         end,
     },
     },
     -- ------------------------------------------------------------------------
     --
     -- derived stats
     -- args populated by mod validation
     -- ------------------------------------------------------------------------
     --  
      
     mods = {
    -- For rarity != normal, rarity already verified
        field = nil,
    base_item = {
         type = nil,
         no_copy = true,
         func = nil,
         field = 'base_item',
         default = {},
         type = 'String',
         func_fetch = function (tpl_args, frame)
         func = function(tpl_args, frame)
             -- Fetch implicit mods from base item
             tpl_args.base_item = tpl_args.base_item_data['items.name']
            local results = m_cargo.query(
        end,
                {'items' ,'item_mods'},
    },
                {'item_mods.id', 'item_mods.is_implicit', 'item_mods.is_random', 'item_mods.text'},
    base_item_id = {
                {
        no_copy = true,
                    join = 'items._pageID=item_mods._pageID',
        field = 'base_item_id',
                    where = string.format('items._pageName="%s" AND item_mods.is_implicit=1', tpl_args.base_item_page),
        type = 'String',
                }
        func = function(tpl_args, frame)
            )
             tpl_args.base_item_id = tpl_args.base_item_data['items.metadata_id']
             for _, row in ipairs(results) do
        end,
                -- Handle text-only mods
    },
                local result
    base_item_page = {
                if row['item_mods.id'] == nil then
        no_copy = true,
                    result = row['item_mods.text']
        field = 'base_item_page',
                end
        type = 'Page',
                tpl_args._base_implicit_mods[#tpl_args._base_implicit_mods+1] = {
        func = function(tpl_args, frame)
                    result=result,
            tpl_args.base_item_page = tpl_args.base_item_data['items._pageName']
                    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,
         end,
     },
     },
     name_list = {
     physical_damage_html = {
         no_copy = true,
         inherit = false,
         field = 'name_list',
         field = 'physical_damage_html',
         type = 'List (�) of String',
         type = 'Text',
         func = function(tpl_args, frame)
         func = h.proc.factory.damage_html{type = 'physical'},
            if tpl_args.name_list ~= nil then
                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
            else
                tpl_args.name_list = {tpl_args.name}
            end
        end,
     },
     },
     frame_type = {
     fire_damage_html = {
         no_copy = true,
         inherit = false,
         field = 'frame_type',
         field = 'fire_damage_html',
         type = 'String',
         type = 'Text',
         property = nil,
         func = h.proc.factory.damage_html{type = 'fire'},
         func = function(tpl_args, frame)
    },
            if tpl_args.frame_type then
    cold_damage_html = {
                return
         inherit = false,
            end
        field = 'cold_damage_html',
            if tpl_args._flags.is_prophecy then
        type = 'Text',
                tpl_args.frame_type = 'prophecy'
        func = h.proc.factory.damage_html{type = 'cold'},
                return
    },
            end
    lightning_damage_html = {
            local var = cfg.class_specifics[tpl_args.class_id]
        inherit = false,
            if var ~= nil and var.frame_type ~= nil then
        field = 'lightning_damage_html',
                tpl_args.frame_type = var.frame_type
        type = 'Text',
                return
        func = h.proc.factory.damage_html{type = 'lightning'},
            end
    },
            if tpl_args.is_relic then
    chaos_damage_html = {
                tpl_args.frame_type = 'relic'
        inherit = false,
                return
        field = 'chaos_damage_html',
            end
        type = 'Text',
            tpl_args.frame_type = tpl_args.rarity_id
        func = h.proc.factory.damage_html{type = 'chaos'},
        end,
     },
     },
     --
     damage_avg = {
    -- args populated by mod validation
        inherit = false,
    --
        field = 'damage_avg',
    mods = {
         type = 'Text',
         default = function (tpl_args, frame) return {} end,
         func = function (tpl_args, frame, value)
         func_fetch = function (tpl_args, frame)
             local dmg = {min=0, max=0}
            -- Fetch implicit mods from base item
            for key, _ in pairs(dmg) do
             local results = m_cargo.query(
                 for _, dkey in ipairs(m_game.constants.damage_type_order) do
                {'items' ,'item_mods'},
                    dmg[key] = dmg[key] + tpl_args[string.format('%s_damage_%s_range_average', dkey, key)]
                {'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
                 end
                tpl_args._base_implicit_mods[#tpl_args._base_implicit_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
            dmg = (dmg.min + dmg.max) / 2
            return dmg
         end,
         end,
     },
     },
     physical_damage_html = {
     damage_html = {
         no_copy = true,
         inherit = false,
         field = 'physical_damage_html',
         field = 'damage_html',
         type = 'Text',
         type = 'Text',
         func = core.factory.damage_html{key='physical'},
         func = function (tpl_args, frame, 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,
     },
     },
     fire_damage_html = {
     item_limit = {
         no_copy = true,
         inherit = false,
         field = 'fire_damage_html',
         field = 'item_limit',
        type = 'Integer',
        func = h.proc.number,
    },
    jewel_radius_html = {
        inherit = false,
        field = 'radius_html',
         type = 'Text',
         type = 'Text',
         func = core.factory.damage_html{key='fire'},
         func = function (tpl_args, frame, 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,
     },
     },
     cold_damage_html = {
     incubator_effect = {
         no_copy = true,
         inherit = false,
         field = 'cold_damage_html',
         field = 'effect',
         type = 'Text',
         type = 'Text',
         func = core.factory.damage_html{key='cold'},
         func = nil,
     },
     },
     lightning_damage_html = {
     drop_areas_html = {
         no_copy = true,
         inherit = false,
         field = 'lightning_damage_html',
         field = 'drop_areas_html',
         type = 'Text',
         type = 'Text',
        func = core.factory.damage_html{key='lightning'},
         func = function (tpl_args, frame, value)
    },
             if tpl_args.drop_areas_data == nil then
    chaos_damage_html = {
                return value
        no_copy = true,
             end
        field = 'chaos_damage_html',
            if value ~= nil then
        type = 'Text',
                 return value
        func = core.factory.damage_html{key='chaos'},
    },
    damage_avg = {
        no_copy = true,
        field = 'damage_avg',
        type = 'Text',
         func = function(tpl_args, frame)
             local dmg = {min=0, max=0}
             for key, _ in pairs(dmg) do
                for _, dkey in ipairs(m_game.constants.damage_type_order) do
                    dmg[key] = dmg[key] + tpl_args[string.format('%s_damage_%s_range_average', dkey, key)]
                 end
             end
             end
              
             local areas = {}
             dmg = (dmg.min + dmg.max) / 2
             for _, data in pairs(tpl_args.drop_areas_data) do
           
                -- skip legacy maps in the drop html listing
            tpl_args.damage_avg = dmg
                if not string.match(data['areas.id'], '^Map.*') or string.match(data['areas.id'], '^MapWorlds.*') or string.match(data['areas.id'], '^MapAtziri.*') then
        end,
                     areas[#areas+1] = string.format('[[%s|%s]]', data['areas.main_page'] or data['areas._pageName'], data['areas.main_page'] or data['areas.name'])
    },
    damage_html = {
        no_copy = true,
        field = 'damage_html',
        type = 'Text',
        func = function(tpl_args, frame)
            local text = {}
            for _, dkey in ipairs(m_game.constants.damage_type_order) do
                local value = tpl_args[dkey .. '_damage_html']
                if value ~= nil then
                     text[#text+1] = value
                 end
                 end
             end
             end
             if #text > 0 then
             return table.concat(areas, ' ')
                tpl_args.damage_html = table.concat(text, '<br>')
            end
         end,
         end,
     },
     },
     item_limit = {
     release_version = {
         no_copy = true,
         inherit = false,
         field = 'item_limit',
         field = 'release_version',
         type = 'Integer',
         type = 'String',
         func = m_util.cast.factory.number('item_limit'),
         func = nil,
     },
     },
     jewel_radius_html = {
     removal_version = {
         no_copy = true,
         inherit = false,
         field = 'radius_html',
         field = 'removal_version',
         type = 'Text',
         type = 'String',
        func = function(tpl_args, frame)
            -- 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'
                tpl_args.jewel_radius_html = m_util.html.poe_color(color, size)
            end
        end,
    },
    incubator_effect = {
        no_copy = true,
        field = 'effect',
        type = 'Text',
         func = nil,
         func = nil,
    },
    drop_areas_html = {
        no_copy = true,
        field = 'drop_areas_html',
        type = 'Text',
        func = function(tpl_args, frame)
            if tpl_args.drop_areas_data == nil then
                return
            end
           
            if tpl_args.drop_areas_html ~= nil then
                return
            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['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,
    },
    release_version = {
        no_copy = true,
        field = 'release_version',
        type = 'String'
    },
    removal_version = {
        no_copy = true,
        field = 'removal_version',
        type = 'String',
     },
     },
     --
     --
Line 1,975: Line 2,017:
     --  
     --  
     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 = {
     upgraded_from_disabled = {
         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 2,354: Line 2,396:
     physical_dps = {
     physical_dps = {
         field = 'physical_dps',
         field = 'physical_dps',
         damage_args = {'physical_damage', },
         damage_args = {'physical_damage'},
         label_infobox = i18n.tooltips.physical_dps,
         label_infobox = i18n.tooltips.physical_dps,
         html_fmt_options = {
         html_fmt_options = {

Revision as of 04:26, 13 July 2021

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

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

-- 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
     type  How to read data from tpl_args using the given keys. nil = Regular, gem = Skill progression, stat = Stats
     parts
      [n]
       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
       -- 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 the value range. Default: '(%s-%s)'
       color  poe_color code to use for the value range. False for no color. Default: 'mod'
       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: 'default'
       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, frame)
        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 value = tpl_args._stats[data.key]
                if value ~= nil then
                    base_values[i] = value.min
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
        else
            -- Regular. Look for key exactly as written in tpl_args
            for i, data in ipairs(args.parts) do
                base_values[i] = tpl_args[data.key]
                local value = {}
                if tpl_args[data.key .. '_range_minimum'] ~= nil then
                    value.min = tpl_args[data.key .. '_range_minimum']
                    value.max = tpl_args[data.key .. '_range_maximum']
                elseif tpl_args[data.key] ~= nil then
                    value.min = tpl_args[data.key]
                    value.max = tpl_args[data.key]
                end
                if value.min == nil then
                else
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
        end
        
        local final_values = {}
        for i, data in ipairs(temp_values) do
            local opt = args.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, frame, 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 or opt.hide_default == v.max then
                        hide = true
                    end
                else
                    local v = data.value
                    if opt.hide_default == v.min or 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, frame, 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.stats_update(tpl_args, id, value, modid, key)
    if tpl_args[key][id] == nil then
        tpl_args[key][id] = {
            references = {modid},
            min = value.min,
            max = value.max,
            avg = value.avg,
        }
    else
        if modid ~= nil then
            table.insert(tpl_args[key][id].references, modid)
        end
        tpl_args[key][id].min = tpl_args[key][id].min + value.min
        tpl_args[key][id].max = tpl_args[key][id].max + value.max
        tpl_args[key][id].avg = tpl_args[key][id].avg + value.avg
    end
end

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

function h.proc.factory.value(args)
    args = args or {}
    return function (tpl_args, frame, 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, frame, 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, frame, value)
        local keys = {
            min = args.type .. '_damage_min',
            max = args.type .. '_damage_max',
        }
        range = {}
        for ktype, key in pairs(keys) do
            range[ktype] = core.factory.infobox_line{
                parts = {
                    {
                        key = key,
                        color = false,
                        hide_default = 0,
                    }
                }
            }(tpl_args, frame)
        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

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 = function (value)
        args = {
            min = 0,
            max = 100,
        }
        return math.floor(m_util.cast.number(value, args))
    end,
}

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

-- Process mod stats
function h.process_mod_stats(tpl_args, args)
    local lines = {}
    
    local skip = cfg.class_specifics[tpl_args.class_id]
    if skip then
        skip = skip.skip_stat_lines
    end 
    
    local random_mods = {}
    
    for _, modinfo in ipairs(tpl_args._mods) do
        if modinfo.is_implicit == args.is_implicit then
            if modinfo.is_random == true then
                if random_mods[modinfo.stat_text] then
                    table.insert(random_mods[modinfo.stat_text], modinfo)
                else
                    random_mods[modinfo.stat_text] = {modinfo}
                end
            else
                if modinfo.id == nil then
                    table.insert(lines, modinfo.result)
                -- Allows the override of the SMW fetched mod texts for this modifier via <modtype><id>_text parameter
                elseif modinfo.text ~= nil then
                     table.insert(lines, modinfo.text)
                else
                    for _, line in ipairs(m_util.string.split(modinfo.result['mods.stat_text'] or '', '<br>')) do
                        if line ~= '' then
                            if skip == nil then
                                table.insert(lines, line)
                            else
                                local skipped = false
                                for _, pattern in ipairs(skip) do
                                    if string.match(line, pattern) then
                                        skipped = true
                                        break
                                    end
                                end
                                if not skipped then
                                    table.insert(lines, line)
                                end
                            end
                        end
                    end
                end
            end
        end
    end
    
    for stat_text, modinfo_list in pairs(random_mods) do
        local text = {}
        for _, modinfo in ipairs(modinfo_list) do
            table.insert(text, modinfo.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
    
    if #lines == 0 then
        return
    else
        return table.concat(lines, '<br>')
    end
end

--
-- 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
-- }
core.map = {
    -- special params
    html = {
        inherit = false,
        field = 'html',
        type = 'Text',
        func = nil,
    },
    html_extra = {
        inherit = false,
        field = 'html_extra',
        type = 'Text',
        func = nil,
    },
    implicit_stat_text = {
        field = 'implicit_stat_text',
        type = 'Text',
        func = function (tpl_args, frame, value)
            return h.process_mod_stats(tpl_args, {is_implicit=true})
        end,
    },
    explicit_stat_text = {
        field = 'explicit_stat_text',
        type = 'Text',
        func = function (tpl_args, frame, value)
            local explicit = h.process_mod_stats(tpl_args, {is_implicit=false})
            if tpl_args.is_talisman or tpl_args.is_corrupted then
                explicit = explicit or '' 
                if explicit ~= '' then
                    explicit = explicit .. '<br> '
                end
                explicit = explicit .. i18n.tooltips.corrupted
            end
            return explicit
        end,
    },
    stat_text = {
        field = 'stat_text',
        type = 'Text',
        func = function (tpl_args, frame, value)
            local sep = ''
            if tpl_args.implicit_stat_text and tpl_args.explicit_stat_text then
                sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
            end
            local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
            if string.len(text) > 0 then
                value = text
            end
            return value
        end,
    },
    class = {
        inherit = false,
        field = 'class',
        type = 'String',
        func = function (tpl_args, frame, 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,
    },
    -- processed in build_item_classes
    class_id = {
        inherit = false,
        field = 'class_id',
        type = 'String',
        func = h.proc.factory.value{
            validate = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.item.classes,
                errmsg = i18n.errors.invalid_class_id,
                errlvl = 3,
            },
        },
    },
    -- generic
    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,
                errlvl = 3,
            },
        },
    },
    rarity = {
        inherit = false,
        field = 'rarity',
        type = 'String',
        func = function (tpl_args, frame, value)
            return m_game.constants.rarities[tpl_args.rarity_id]['long_upper']
        end,
    },
    name = {
        inherit = false,
        field = 'name',
        type = 'String',
        func = nil,
    },
    size_x = {
        field = 'size_x',
        type = 'Integer',
        func = h.proc.number,
    },
    size_y = {
        field = 'size_y',
        type = 'Integer',
        func = h.proc.number,
    },
    drop_rarities_ids = {
        inherit = false,
        field = 'drop_rarity_ids',
        type = 'List (,) of Text',
        func = function (tpl_args, frame, value)
            tpl_args.drop_rarities_ids = nil
            if true then return end
            -- Drop rarities only matter for base items.
            if tpl_args.rarity_id ~= 'normal' then
                return
            end
            if tpl_args.drop_rarities_ids == nil then
                tpl_args.drop_rarities_ids = {}
                return
            end
            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
                if m_game.constants.rarities[rarity_id] == nil then
                    error(string.format(i18n.errors.invalid_rarity_id, tostring(rarity_id)))
                end
            end
        end,
    },
    drop_rarities = {
        inherit = false,
        field = nil,
        type = 'List (,) of Text',
        func = function (tpl_args, frame, value)
            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 = {
        inherit = false,
        field = 'drop_enabled',
        type = 'Boolean',
        func = h.proc.boolean,
        default = true,
    },
    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,
    },
    drop_leagues = {
        inherit = false,
        field = 'drop_leagues',
        type = 'List (,) of String',
        func = h.proc.factory.list{
            callback = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.leagues,
                errmsg = i18n.errors.invalid_league,
                errlvl = 4,
            },
        },
        default = {},
    },
    drop_areas = {
        inherit = false,
        field = 'drop_areas',
        type = 'List (,) of String',
        func = function (tpl_args, frame, value)
            if value ~= nil then
                value = m_util.string.split(value, ',%s*')
                tpl_args.drop_areas_data = m_cargo.array_query{
                    tables={'areas'},
                    fields={'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
                    id_field='areas.id',
                    id_array=value,
                    query={limit=5000},
                }
            end
            -- find areas based on item tags for atlas bases
            local query_data
            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 value == nil then
                        value = {}
                        tpl_args.drop_areas_data = {}
                    end
                    local drop_areas_assoc = {}
                    for _, id in ipairs(value) 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
                            value[#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
            return value
        end,
    },
    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, frame, value)
            value = tpl_args.required_level
            if value < cfg.base_item_required_level_threshold 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, frame, 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]
                elseif tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
                    value = i18n.default_inventory_icons['Prophecy']
                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, frame, 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 = {
        debug_ignore_nil = true,
        field = 'help_text',
        type = 'Text',
        func = h.proc.text,
    },
    flavour_text = {
        inherit = false,
        field = 'flavour_text',
        type = 'Text',
        func = h.proc.text,
    },
    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',
        --type = 'String(unique; size=200)',
        func = function (tpl_args, frame, value)
            if value ~= nil 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().fullText)
                        )
                    }
                )
                if #results > 0 and tpl_args.debug_id == nil and not tpl_args.debug 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_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, frame, 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_relic = {
        inherit = false,
        field = 'is_relic',
        type = 'Boolean',
        func = function (tpl_args, frame, 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_relic'))
            end
            return value
        end,
        default = false,
    },
    is_fated = {
        inherit = false,
        field = 'is_fated',
        type = 'Boolean',
        func = function (tpl_args, frame, 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_fated'))
            end
            return value
        end,
        default = false,
    },
    is_prophecy = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, frame, value)
            tpl_args._flags.is_prophecy = (tpl_args.metadata_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' or tpl_args.base_item == cfg.prophecy_base_item or tpl_args.base_item_page == cfg.prophecy_base_item_page or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy')
            return value
        end
    },
    is_blight_item = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, frame, value)
            tpl_args._flags.is_blight_item = (tpl_args.blight_item_tier ~= nil)
            return value
        end
    },
    is_drop_restricted = {
        inherit = false,
        field = 'is_drop_restricted',
        type = 'Boolean',
        func = h.proc.boolean,
        default = function (tpl_args, frame)
            -- Generally items that are obtained only from specific monsters can't be obtained via cards or other means unless specifically specified
            for _, class in ipairs({'Incubator'}) do
                if tpl_args.class_id == class then
                    return true
                end
            end
            for _, var in ipairs({'is_talisman', 'is_essence', 'is_fated', 'is_replica', 'is_relic', 'drop_monsters'}) do
                if tpl_args[var] then
                    return true
                end
            end
            for _, flag in ipairs({'is_prophecy', 'is_blight_item'}) do
                if tpl_args._flags[flag] then
                    return true
                end
            end
            return false
        end,
    },
    purchase_costs = {
        field = nil,
        type = nil,
        func = function (tpl_args, frame, 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
                        
                        tpl_args._subobjects[#tpl_args._subobjects+1] = {
                            _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, frame)
            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
                
                tpl_args._subobjects[#tpl_args._subobjects+1] = {
                    _table = 'item_purchase_costs',
                    amount = values.amount,
                    name = values.name,
                    rarity = values.rarity,
                }
            end
        end,
    },
    sell_prices_override = {
        inherit = false,
        field = nil,
        type = nil,
        func = function (tpl_args, frame, value)
            -- these variables are also used by mods when setting automatic sell prices
            tpl_args.sell_prices = {}
            tpl_args.sell_price_order = {}
            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
                    tpl_args._subobjects[#tpl_args._subobjects+1] = {
                        _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',
        -- Can be set manually, but default to Q20 for unique weapons/body armours
        -- Also must copy to stat for the stat adjustments to work properly
        func = function (tpl_args, frame, value)
            local quality = tonumber(value)
            if quality == nil then
                if tpl_args.rarity_id ~= 'unique' then
                    quality = 0
                elseif cfg.class_groups.weapons.keys[tpl_args.class_id] or cfg.class_groups.armor.keys[tpl_args.class_id] then
                    quality = 20
                else
                    quality = 0
                end
            end
            local stat = {
                min = quality,
                max = quality,
                avg = quality,
            }
            core.stats_update(tpl_args, 'quality', stat, nil, '_stats')
            if tpl_args.class_id == 'UtilityFlask' or tpl_args.class_id == 'UtilityFlaskCritical' then
                core.stats_update(tpl_args, 'quality_flask_duration', stat, nil, '_stats')
            -- quality is added to quantity for maps
            elseif tpl_args.class_id == 'Map' then
                core.stats_update(tpl_args, 'map_item_drop_quantity_+%', stat, nil, '_stats')
            end
            return quality
        end,
    },
    -- amulets
    is_talisman = {
        field = 'is_talisman',
        type = 'Boolean',
        func = h.proc.boolean,
        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, frame, 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, frame)
            tpl_args.buff_values = m_util.string.split(tpl_args.buff_values, ',%s*')
        end,
        default = {},
    },
    buff_stat_text = {
        field = 'stat_text',
        type = 'String',
        func = nil,
    },
    buff_icon = {
        field = 'icon',
        type = 'String',
        func = function (tpl_args, frame, 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 = 'Integer',
        func = h.proc.number,
    },
    physical_damage_min = {
        field = 'physical_damage_min',
        type = 'Integer',
        func = h.proc.number,
    },
    physical_damage_max = {
        field = 'physical_damage_max',
        type = 'Integer',
        func = h.proc.number,
    },
    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 = {
        field = 'armour',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    energy_shield = {
        field = 'energy_shield',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    evasion = {
        field = 'evasion',
        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, frame, 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 = {},
    },
    -- Support gems only
    support_gem_letter = {
        field = 'support_gem_letter',
        type = 'String(size=1)',
        func = nil,
    },
    support_gem_letter_html = {
        field = 'support_gem_letter_html',
        type = 'Text',
        func = function (tpl_args, frame, 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', k)
                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,
    },
    --
    -- Maps
    --
    map_tier = {
        field = 'tier',
        type = 'Integer',
        func = h.proc.number,
    },
    map_guild_character = {
        field = 'guild_character',
        type = 'String(size=1)',
        func = nil,
    },
    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(size=1)',
        func = nil,
        func_copy = function (tpl_args, frame)
            tpl_args.map_guild_character = tpl_args.unique_map_guild_character
        end,
    },
    unique_map_area_id = {
        field = 'unique_area_id',
        type = 'String',
        func = nil, -- TODO: Validate against a query?
        func_copy = function (tpl_args, frame)
            tpl_args.map_area_id = tpl_args.unique_map_area_id
        end,
    },
    unique_map_area_level = {
        field = 'unique_area_level',
        type = 'Integer',
        func = h.proc.number,
        func_copy = function (tpl_args, frame)
            tpl_args.map_area_level = tpl_args.unique_map_area_level
        end,
    },
    map_series = {
        field = 'series',
        type = 'String',
        func = function (tpl_args, frame, 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 = {
        field = 'x',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y = {
        field = 'y',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_region_id = {
        field = 'region_id',
        type = 'String',
        func = nil,
    },
    atlas_region_minimum = {
        field = 'region_minimum',
        type = 'Integer',
        func = h.proc.number,
    },
    atlas_x0 = {
        field = 'x0',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_x1 = {
        field = 'x1',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_x2 = {
        field = 'x2',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_x3 = {
        field = 'x3',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_x4 = {
        field = 'x4',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y0 = {
        field = 'y0',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y1 = {
        field = 'y1',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y2 = {
        field = 'y2',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y3 = {
        field = 'y3',
        type = 'Float',
        func = h.proc.number,
    },
    atlas_y4 = {
        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 = {
        field = nil,
        type = nil,
        func = function (tpl_args, frame, 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._subobjects, data)
                else
                    cont = false
                    if i == 1 then
                        value = nil
                    end
                end
                i = i + 1
            end
            return value
        end,
    },
    --
    -- Currency-like items
    --
    stack_size = {
        field = 'stack_size',
        type = 'Integer',
        func = h.proc.number,
    },
    stack_size_currency_tab = {
        field = 'stack_size_currency_tab',
        type = 'Integer',
        func = h.proc.number,
    },
    description = {
        field = 'description',
        type = 'Text',
        func = h.proc.text,
    },
    cosmetic_type = {
        field = 'cosmetic_type',
        type = 'String',
        func = h.proc.text,
    },
    -- for essences
    is_essence = {
        field = nil,
        type = nil,
        func = h.proc.boolean,
        default = false,
    },
    essence_level_restriction = {
        field = 'level_restriction',
        type = 'Integer',
        func = h.proc.number,
    },
    essence_level = {
        field = 'level',
        type = 'Integer',
        func = h.proc.number,
    },
    essence_type = {
        field = 'type',
        type = 'Integer',
        func = h.proc.number,
    },
    essence_category = {
        field = 'category',
        type = 'String',
        func = nil,
    },
    -- blight crafting items (i.e. oils)
    blight_item_tier = {
        field = 'tier',
        type = 'Integer',
        func = h.proc.number,
    },
    -- harvest seeds
    seed_type_id = {
        field = 'type_id',
        type = 'String',
        func = nil,
    },
    seed_type = {
        field = 'type',
        type = 'String',
        func = function (tpl_args, frame, 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 = {
        field = nil,
        type = nil,
        func = function (tpl_args, frame, 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 = {
        field = 'effect',
        type = 'Text',
        func = nil,
    },
    seed_tier = {
        field = 'tier',
        type = 'Integer',
        func = h.proc.number,
    },
    seed_growth_cycles = {
        field = 'growth_cycles',
        type = 'Integer',
        func = h.proc.number,
    },
    seed_required_nearby_seed_tier = {
        field = 'required_nearby_seed_tier',
        type = 'Integer',
        func = h.proc.number,
    },
    seed_required_nearby_seed_amount = {
        field = 'required_nearby_seed_amount',
        type = 'Integer',
        func = h.proc.number,
    },
    seed_consumed_wild_lifeforce_percentage = {
        field = 'consumed_wild_lifeforce_percentage',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    seed_consumed_vivid_lifeforce_percentage = {
        field = 'consumed_vivid_lifeforce_percentage',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    seed_consumed_primal_lifeforce_percentage = {
        field = 'consumed_primal_lifeforce_percentage',
        type = 'Integer',
        func = h.proc.number,
        default = 0,
    },
    seed_granted_craft_option_ids = {
        field = 'granted_craft_option_ids',
        type = 'List (,) of String',
        func = h.proc.list,
        default = {},
    },
    --
    -- harvest planet boosters
    --
    plant_booster_radius = {
        field = 'radius',
        type = 'Integer',
        func = h.proc.number,
    },
    plant_booster_lifeforce = {
        field = 'lifeforce',
        type = 'Integer',
        func = h.proc.number,
    },
    plant_booster_additional_crafting_options = {
        field = 'additional_crafting_options',
        type = 'Integer',
        func = h.proc.number,
    },
    plant_booster_extra_chances = {
        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 = {
        field = nil,
        type = nil,
        func = function (tpl_args, frame, value)
            if tpl_args.heist_required_job_level then
                if tpl_args.heist_required_job_id then
                    local results = m_cargo.query(
                        {'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
            return value
        end,
    },
    --
    -- hideout doodads (HideoutDoodads.dat)
    --
    is_master_doodad = {
        field = 'is_master_doodad',
        type = 'Boolean',
        func = h.proc.boolean,
    },
    master = {
        field = 'master',
        type = 'String',
        func = function (tpl_args, frame, value)
            if value == nil then
                return nil
            end
            return m_util.validate.factory.in_table_keys{
                tbl = m_util.table.column(m_game.constants.masters, 'long_upper', 'full'),
                errmsg = i18n.errors.invalid_master,
                errlvl = 3,
            }(value)
        end,
    },
    master_level_requirement = {
        field = 'level_requirement',
        type = 'Integer',
        func = h.proc.number,
    },
    master_favour_cost = {
        field = 'favour_cost',
        type = 'Integer',
        func = h.proc.number,
    },
    variation_count = {
        field = 'variation_count',
        type = 'Integer',
        func = h.proc.number,
    },
    -- Propehcy
    prophecy_id = {
        field = 'prophecy_id',
        type = 'String',
        func = nil,
    },
    prediction_text = {
        field = 'prediction_text',
        type = 'Text',
        func = h.proc.text,
    },
    seal_cost = {
        field = 'seal_cost',
        type = 'Integer',
        func = h.proc.number,
    },
    prophecy_reward = {
        field = 'reward',
        type = 'Text',
        func = h.proc.text,
    },
    prophecy_objective = {
        field = 'objective',
        type = 'Text',
        func = h.proc.text,
    },
    -- Divination cards
    card_art = {
        field = 'card_art',
        type = 'Page',
        func = function (tpl_args, frame, value)
            return string.format(i18n.files.divination_card_art, value or tpl_args.name)
        end,
    },
    -- ------------------------------------------------------------------------
    -- derived stats
    -- ------------------------------------------------------------------------
    
    -- For rarity != normal, rarity already verified
    base_item = {
        inherit = false,
        field = 'base_item',
        type = 'String',
        func = function (tpl_args, frame, value)
            return tpl_args.base_item_data['items.name']
        end,
    },
    base_item_id = {
        inherit = false,
        field = 'base_item_id',
        type = 'String',
        func = function (tpl_args, frame, value)
            return tpl_args.base_item_data['items.metadata_id']
        end,
    },
    base_item_page = {
        inherit = false,
        field = 'base_item_page',
        type = 'Page',
        func = function (tpl_args, frame, value)
            return tpl_args.base_item_data['items._pageName']
        end,
    },
    name_list = {
        inherit = false,
        field = 'name_list',
        type = 'List (�) of String',
        func = function (tpl_args, frame, 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, frame, 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
            if tpl_args.is_relic then
                return 'relic'
            end
            return tpl_args.rarity_id
        end,
    },
    --
    -- args populated by mod validation
    -- 
    mods = {
        field = nil,
        type = nil,
        func = nil,
        default = {},
        func_fetch = function (tpl_args, frame)
            -- Fetch implicit mods from base item
            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._base_implicit_mods[#tpl_args._base_implicit_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,
    },
    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, frame, value)
            local dmg = {min=0, max=0}
            for key, _ in pairs(dmg) do
                for _, dkey in ipairs(m_game.constants.damage_type_order) do
                    dmg[key] = dmg[key] + tpl_args[string.format('%s_damage_%s_range_average', dkey, key)]
                end
            end
            dmg = (dmg.min + dmg.max) / 2
            return dmg
        end,
    },
    damage_html = {
        inherit = false,
        field = 'damage_html',
        type = 'Text',
        func = function (tpl_args, frame, 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,
    },
    item_limit = {
        inherit = false,
        field = 'item_limit',
        type = 'Integer',
        func = h.proc.number,
    },
    jewel_radius_html = {
        inherit = false,
        field = 'radius_html',
        type = 'Text',
        func = function (tpl_args, frame, 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,
    },
    incubator_effect = {
        inherit = false,
        field = 'effect',
        type = 'Text',
        func = nil,
    },
    drop_areas_html = {
        inherit = false,
        field = 'drop_areas_html',
        type = 'Text',
        func = function (tpl_args, frame, 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['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
            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,
    },
    upgraded_from_disabled = {
        inherit = false,
        field = nil,
        func = h.proc.boolean,
        default = false,
    },
}

core.stat_map = {
    required_level_final = {
        field = 'required_level',
        stats_add = {
            'local_level_requirement_+',
        },
        stats_override = {
            ['local_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=1, max=1},
        },
        minimum = 1,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    weapon_range = {
        field = 'weapon_range',
        stats_add = {
            'local_weapon_range_+',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    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 = {
            color = 'fire',
            fmt = '%i',
        },
    },
    fire_damage_max = {
        field = 'fire_damage_max',
        stats_add = {
            'local_maximum_added_fire_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'fire',
            fmt = '%i',
        },
    },
    cold_damage_min = {
        field = 'cold_damage_min',
        stats_add = {
            'local_minimum_added_cold_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'cold',
            fmt = '%i',
        },
    },
    cold_damage_max = {
        field = 'cold_damage_max',
        stats_add = {
            'local_maximum_added_cold_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'cold',
            fmt = '%i',
        },
    },
    lightning_damage_min = {
        field = 'lightning_damage_min',
        stats_add = {
            'local_minimum_added_lightning_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'lightning',
            fmt = '%i',
        },
    },
    lightning_damage_max = {
        field = 'lightning_damage_max',
        stats_add = {
            'local_maximum_added_lightning_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'lightning',
            fmt = '%i',
        },
    },
    chaos_damage_min = {
        field = 'chaos_damage_min',
        stats_add = {
            'local_minimum_added_chaos_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'chaos',
            fmt = '%i',
        },
    },
    chaos_damage_max = {
        field = 'chaos_damage_max',
        stats_add = {
            'local_maximum_added_chaos_damage',
        },
        minimum = 0,
        html_fmt_options = {
            color = 'chaos',
            fmt = '%i',
        },
    },
    critical_strike_chance = {
        field = 'critical_strike_chance',
        stats_add = {
            'local_critical_strike_chance',
        },
        stats_increased = {
            'local_critical_strike_chance_+%',
        },
        stats_override = {
            ['local_weapon_always_crit'] = {min=100, max=100},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%.2f%%',
        },
    },
    attack_speed = {
        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',
        },
    },
    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',
        },
    },
    block = {
        field = 'block',
        stats_add = {
            'local_additional_block_chance_%',
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i%%',
        },
    },
    armour = {
        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 = {
        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 = {
        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_unique_tabula_rasa_no_requirement_or_energy_shield'] = {min=0, max=0},
        },
        minimum = 0,
        html_fmt_options = {
            fmt = '%i',
        },
    },
    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,
        },
    },
}

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

return core