Module:Item/core: Difference between revisions

From Path of Exile 2 Wiki
Jump to navigation Jump to search
(core.map rewritten to be more expressive for argument processing)
(Undo revision 708941 by Vinifera7 (talk))
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('Item2')
local use_sandbox = m_util.misc.maybe_sandbox()


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


function core.stats_update(tpl_args, id, value, modid, key)
    args.parts = args.parts or {}
    if tpl_args[key][id] == nil then
    return function (tpl_args, frame)
         tpl_args[key][id] = {
         local base_values = {}
            references = {modid},
        local temp_values = {}
            min = value.min,
        if args.type == 'gem' then
            max = value.max,
             -- Skill progression. Look for keys in tpl_args.skill_levels
            avg = value.avg,
            if not cfg.class_groups.gems.keys[tpl_args.class_id] then
        }
                -- Skip if this item is not actually a gem
    else
                return
        if modid ~= nil then
            end
             table.insert(tpl_args[key][id].references, modid)
            for i, data in ipairs(args.parts) do
        end
                if data.key then
        tpl_args[key][id].min = tpl_args[key][id].min + value.min
                    local path = type(data.key) == 'table' and data.key or {data.key}
        tpl_args[key][id].max = tpl_args[key][id].max + value.max
                    -- Check for static value
        tpl_args[key][id].avg = tpl_args[key][id].avg + value.avg
                    local value = tpl_args.skill_levels[0]
    end
                    for _, p in ipairs(path) do -- Parse for subfield along path
end
                        if value[p] == nil then
 
                            value = nil
--
                            break
-- Functions for processing tpl_args
                        else
--
                            value = value[p]
h.proc = {}
                        end
h.proc.factory = {}
                    end
 
                    if value ~= nil then
function h.proc.factory.value(args)
                        base_values[i] = value
    args = args or {}
                        temp_values[#temp_values+1] = {value={min=value,max=value}, index=i}
    return function (tpl_args, frame, value)
                    else -- Check for leveled values
        if value == nil then
                        value = {
            return nil
                            min = tpl_args.skill_levels[1],
        end
                            max = tpl_args.skill_levels[tpl_args.max_level],
        if args.cast then
                        }
            value = args.cast(value)
                        for k, _ in pairs(value) do
        end
                            for _, p in ipairs(path) do -- Parse for subfield along path
        if args.validate then
                                if value[k][p] == nil then
            value = args.validate(value)
                                    value[k] = nil
        end
                                    break
        return value
                                else
    end
                                    value[k] = value[k][p]
end
                                end
 
                            end
function h.proc.factory.list(args)
                        end
    args = args or {}
                        if value.min ~= nil and value.max ~= nil then
    return function (tpl_args, frame, value)
                            base_values[i] = value.min
        return m_util.cast.table(value, {
                            temp_values[#temp_values+1] = {value=value, index=i}
            pattern = args.pattern,
                        end
            callback = args.callback,
                     end
        })
    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
                 end
                range_fmt = i18n.fmt.standard_damage_range
             end
             end
             value = string.format(range_fmt, range.min, range.max)
        elseif args.type == 'stat' then
            if color then
            -- Stats. Look for key in tpl_args._stats
                 value = m_util.html.poe_color(color, value)
             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
         return value
          
    end
        local final_values = {}
end
        for i, data in ipairs(temp_values) do
 
            local opt = args.parts[data.index]
h.proc.text = h.proc.factory.value{cast = m_util.cast.text}
            local hide = false
h.proc.boolean = h.proc.factory.value{cast = m_util.cast.boolean}
            if type(opt.hide) == 'function' then
h.proc.number = h.proc.factory.value{cast = m_util.cast.number}
                local v = data.value
 
                if opt.hide_key then
h.proc.percentage = h.proc.factory.value{
                    v = {
    cast = function (value)
                        min = tpl_args[opt.hide_key .. '_range_minimum'],
        args = {
                        max = tpl_args[opt.hide_key .. '_range_maximum'],
            min = 0,
                    }
            max = 100,
                    if v.min == nil or v.max == nil then
        }
                        v = tpl_args[opt.hide_key]
        return math.floor(m_util.cast.number(value, args))
                    end
    end,
                end
}
                hide = opt.hide(tpl_args, frame, v)
 
            elseif opt.hide_default ~= nil then
h.proc.list = h.proc.factory.list()
                if opt.hide_default_key then
 
                    local v = {
-- Process mod stats
                        min = tpl_args[opt.hide_default_key .. '_range_minimum'],
function h.process_mod_stats(tpl_args, args)
                        max = tpl_args[opt.hide_default_key .. '_range_maximum'],
    local lines = {}
                    }
   
                    if v.min == nil or v.max == nil then
    local skip = cfg.class_specifics[tpl_args.class_id]
                        if opt.hide_default == tpl_args[opt.hide_default_key] then
    if skip then
                            hide = true
        skip = skip.skip_stat_lines
                        end
    end
                     elseif opt.hide_default == v.min or opt.hide_default == v.max then
   
                        hide = true
    local random_mods = {}
                    end
   
    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
                     random_mods[modinfo.stat_text] = {modinfo}
                     local v = data.value
                    if opt.hide_default == v.min or opt.hide_default == v.max then
                        hide = true
                    end
                 end
                 end
             else
             end           
                if modinfo.id == nil then
            if not hide then
                    table.insert(lines, modinfo.result)
                table.insert(final_values, data)
                -- Allows the override of the SMW fetched mod texts for this modifier via <modtype><id>_text parameter
            end
                elseif modinfo.text ~= nil then
        end
                    table.insert(lines, modinfo.text)
       
                else
        -- all zeros = dont display and return early
                    for _, line in ipairs(m_util.string.split(modinfo.result['mods.stat_text'] or '', '<br>')) do
        if #final_values == 0 then
                        if line ~= '' then
             return nil
                            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
       
   
         local parts = {}
    for stat_text, modinfo_list in pairs(random_mods) do
         for i, data in ipairs(final_values) do
         local text = {}
             local value = data.value
         for _, modinfo in ipairs(modinfo_list) do
            value.base = base_values[data.index]
             table.insert(text, modinfo.result['mods.stat_text'])
            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
         end
   
 
         local tbl = mw.html.create('table')
        -- Build output string
         tbl
         local out
             :attr('class', 'random-modifier-stats mw-collapsed')
        if args.fmt then
             :attr('style', 'text-align: left')
            out = string.format(args.fmt, unpack(parts))
             :tag('tr')
         else
                :tag('th')
             out = table.concat(parts)
                    :attr('class', 'mw-customtoggle-31')
        end
                    :wikitext(stat_text)
        if args.color then
                    :done()
             out = m_util.html.poe_color(args.color, out, args.class)
                :done()
        elseif args.class then
            :tag('tr')
             out = tostring(mw.html.create('em')
                 :attr('class', 'mw-collapsible mw-collapsed')
                 :attr('class', class)
                 :attr('id', 'mw-customcollapsible-31')
                 :wikitext(out)
                :tag('td')
            )
                    :wikitext(table.concat(text, '<hr style="width: 20%">'))
         end
                    :done()
         return out
                :done()
         table.insert(lines, tostring(tbl))
    end
   
    if #lines == 0 then
         return
    else
        return table.concat(lines, '<br>')
     end
     end
end
end


--
function core.factory.damage_html(args)
-- Argument mapping
     return function(tpl_args, frame)
--
        local keys = {
-- [<tpl_args key>] = {
            min = args.key .. '_damage_min',
--  inherit  boolean  Whether the item will inherit this key from its base item. Default: true
            max = args.key .. '_damage_max',
--  field  string  Cargo field name
         }
--  type  string  Cargo field type
        local value = {}
--  func  function Function to unpack the argument into a native lua value and validate it
         for ktype, key in pairs(keys) do
--  default  varies  Default value if parameter is not set
            value[ktype] = core.factory.infobox_line{
-- }
                parts = {
core.map = {
                    {
     -- special params
                        key = key,
    html = {
                        color = false,
        inherit = false,
                        hide_default = 0,
        field = 'html',
                    }
        type = 'Text',
                }
         func = nil,
             }(tpl_args, frame)
    },
         end
    html_extra = {
         if value.min and value.max then
         inherit = false,
             local color = args.key or false
        field = 'html_extra',
            local range_fmt
        type = 'Text',
             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
        func = nil,
                 -- Variable damage range, based on modifier rolls
    },
                if args.key == 'physical' then
    implicit_stat_text = {
                    color = 'mod'
        field = 'implicit_stat_text',
                end
        type = 'Text',
                range_fmt = i18n.fmt.variable_damage_range
        func = function (tpl_args, frame, value)
            else
             return h.process_mod_stats(tpl_args, {is_implicit=true})
                -- Standard damage range
         end,
                 if args.key == 'physical' then
    },
                     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
                 explicit = explicit .. i18n.tooltips.corrupted
                 range_fmt = i18n.fmt.standard_damage_range
             end
             end
             return explicit
             value = string.format(range_fmt, value.min, value.max)
        end,
             if color then
    },
                 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
             local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
             tpl_args[args.key .. '_damage_html'] = value
            if text == '' then
        end
                text = nil
    end
            end
end
            return text
 
         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
 
--
-- argument mapping
--
-- format:
-- tpl_args key = {
--  no_copy = true or nil          -- When loading an base item, dont copy this key
--  property = 'prop',              -- Property associated with this key
--  property_func = function or nil -- Function to unpack the property into a native lua value.
--                                      If not specified, func is used.
--                                      If neither is specified, value is copied as string
--  func = function or nil          -- Function to unpack the argument into a native lua value and validate it.
--                                      If not specified, value will not be set.
--  default = object                -- Default value if the parameter is nil
-- }
core.map = {
    -- special params
    html = {
        no_copy = true,
        field = 'html',
        type = 'Text',
         func = nil,
     },
     },
     class = {
     html_extra = {
         inherit = false,
         no_copy = true,
         field = 'class',
        field = 'html_extra',
         type = 'String',
        type = 'Text',
         func = function (tpl_args, frame, value)
        func = nil,
             local class = m_game.constants.item.classes[tpl_args.class_id]['long_upper']
    },
             -- Avoids errors with empty item class names later on
    implicit_stat_text = {
             if class == '' then
         field = 'implicit_stat_text',
                 class = nil
         type = 'Text',
         func = function(tpl_args, frame)
             tpl_args.implicit_stat_text = h.process_mod_stats(tpl_args, {is_implicit=true})
        end,
    },
    explicit_stat_text = {
        field = 'explicit_stat_text',
        type = 'Text',
        func = function(tpl_args, frame)
            tpl_args.explicit_stat_text = h.process_mod_stats(tpl_args, {is_implicit=false})
              
             if tpl_args.is_talisman or tpl_args.is_corrupted then
                if tpl_args.explicit_stat_text == nil or tpl_args.explicit_stat_text == '' then
                    tpl_args.explicit_stat_text = i18n.tooltips.corrupted
                 else
                    tpl_args.explicit_stat_text = (tpl_args.explicit_stat_text or '') .. '<br>' .. i18n.tooltips.corrupted
                end
             end
             end
            return class
         end,
         end,
     },
     },
     -- processed in build_item_classes
     stat_text = {
     class_id = {
        field = 'stat_text',
         inherit = false,
        type = 'Text',
         field = 'class_id',
        func = function(tpl_args, frame)
         type = 'String',
            local sep = ''
         func = h.proc.factory.value{
            if tpl_args.implicit_stat_text and tpl_args.explicit_stat_text then
             validate = m_util.validate.factory.in_table_keys{
                sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
                tbl = m_game.constants.item.classes,
            end
                 errmsg = i18n.errors.invalid_class_id,
            local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
                errlvl = 3,
           
             },
            if string.len(text) > 0 then
         },
                tpl_args.stat_text = text
     },
            end
     -- generic
        end,
     rarity_id = {
    },
         inherit = false,
     class = {
         no_copy = true,
         field = 'class',
         type = 'String',
         func = function (tpl_args, frame)
            tpl_args.class = m_game.constants.item.classes[tpl_args.class_id]['long_upper']
            -- Avoids errors with empty item class names later on
             if tpl_args.class == '' then
                tpl_args.class = nil
            end
        end,
    },
    -- processed in build_item_classes
    class_id = {
        no_copy = true,
        field = 'class_id',
        type = 'String',
        func = function (tpl_args, frame)
            if m_game.constants.item.classes[tpl_args.class_id] == nil then
                 error(string.format(i18n.errors.invalid_class_id, tostring(tpl_args.class_id)))
             end
         end
     },
     -- generic
     rarity_id = {
         no_copy = true,
         field = 'rarity_id',
         field = 'rarity_id',
         type = 'String',
         type = 'String',
         func = h.proc.factory.value{
         func = function (tpl_args, frame)
             validate = m_util.validate.factory.in_table_keys{
             if m_game.constants.rarities[tpl_args.rarity_id] == nil then
                tbl = m_game.constants.rarities,
                 error(string.format(i18n.errors.invalid_rarity_id, tostring(tpl_args.rarity_id)))
                 errmsg = i18n.errors.invalid_rarity_id,
             end
                errlvl = 3,
         end
             },
         },
     },
     },
     rarity = {
     rarity = {
         inherit = false,
         no_copy = true,
         field = 'rarity',
         field = 'rarity',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             return m_game.constants.rarities[tpl_args.rarity_id]['long_upper']
             tpl_args.rarity = m_game.constants.rarities[tpl_args.rarity_id]['long_upper']
         end,
         end
     },
     },
     name = {
     name = {
         inherit = false,
         no_copy = true,
         field = 'name',
         field = 'name',
         type = 'String',
         type = 'String',
Line 524: Line 488:
         field = 'size_x',
         field = 'size_x',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('size_x'),
     },
     },
     size_y = {
     size_y = {
         field = 'size_y',
         field = 'size_y',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('size_y'),
     },
     },
     drop_rarities_ids = {
     drop_rarities_ids = {
         inherit = false,
         no_copy = true,
         field = 'drop_rarity_ids',
         field = 'drop_rarity_ids',
         type = 'List (,) of Text',
         type = 'List (,) of Text',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             tpl_args.drop_rarities_ids = nil
             tpl_args.drop_rarities_ids = nil
             if true then return end
             if true then return end
Line 542: Line 506:
                 return
                 return
             end
             end
           
             if tpl_args.drop_rarities_ids == nil then
             if tpl_args.drop_rarities_ids == nil then
                 tpl_args.drop_rarities_ids = {}
                 tpl_args.drop_rarities_ids = {}
                 return
                 return
             end
             end
               
             tpl_args.drop_rarities_ids = m_util.string.split(tpl_args.drop_rarities_ids, ',%s*')
             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
             for _, rarity_id in ipairs(tpl_args.drop_rarities_ids) do
Line 555: Line 521:
     },
     },
     drop_rarities = {
     drop_rarities = {
         inherit = false,
         no_copy = true,
         field = nil,
         field = nil,
         type = 'List (,) of Text',
         type = 'List (,) of Text',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             tpl_args.drop_rarities = nil
             tpl_args.drop_rarities = nil
             if true then return end
             if true then return end
Line 574: Line 540:
     },
     },
     drop_enabled = {
     drop_enabled = {
         inherit = false,
         no_copy = true,
         field = 'drop_enabled',
         field = 'drop_enabled',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('drop_enabled'),
         default = true,
         default = true,
     },
     },
     drop_level = {
     drop_level = {
         inherit = false,
         no_copy = true,
         field = 'drop_level',
         field = 'drop_level',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('drop_level'),
     },
     },
     drop_level_maximum = {
     drop_level_maximum = {
         inherit = false,
         no_copy = true,
         field = 'drop_level_maximum',
         field = 'drop_level_maximum',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('drop_level_maximum'),
     },
     },
     drop_leagues = {
     drop_leagues = {
         inherit = false,
         no_copy = true,
         field = 'drop_leagues',
         field = 'drop_leagues',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = h.proc.factory.list{
         func = m_util.cast.factory.assoc_table('drop_leagues', {tbl=m_game.constants.leagues, errmsg=i18n.errors.invalid_league}),
            callback = m_util.validate.factory.in_table_keys{
                tbl = m_game.constants.leagues,
                errmsg = i18n.errors.invalid_league,
                errlvl = 4,
            },
        },
        default = {},
     },
     },
     drop_areas = {
     drop_areas = {
         inherit = false,
         no_copy = true,
         field = 'drop_areas',
         field = 'drop_areas',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             if value ~= nil then
             if tpl_args.drop_areas ~= nil then
                 value = m_util.string.split(value, ',%s*')
                 tpl_args.drop_areas = m_util.string.split(tpl_args.drop_areas, ',%s*')
                 tpl_args.drop_areas_data = m_cargo.array_query{
                 tpl_args.drop_areas_data = m_cargo.array_query{
                     tables={'areas'},
                     tables={'areas'},
                     fields={'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
                     fields={'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
                     id_field='areas.id',
                     id_field='areas.id',
                     id_array=value,
                     id_array=tpl_args.drop_areas,
                     query={limit=5000},
                     query={limit=5000},
                 }
                 }
             end
             end
           
             -- find areas based on item tags for atlas bases
             -- find areas based on item tags for atlas bases
             local query_data
             local query_data
Line 648: Line 608:
                 if query_data ~= nil then
                 if query_data ~= nil then
                     -- in case no manual drop areas have been set
                     -- in case no manual drop areas have been set
                     if value == nil then
                     if tpl_args.drop_areas == nil then
                         value = {}
                         tpl_args.drop_areas = {}
                         tpl_args.drop_areas_data = {}
                         tpl_args.drop_areas_data = {}
                     end
                     end
                     local drop_areas_assoc = {}
                     local drop_areas_assoc = {}
                     for _, id in ipairs(value) do
                     for _, id in ipairs(tpl_args.drop_areas) do
                         drop_areas_assoc[id] = true
                         drop_areas_assoc[id] = true
                     end
                     end
Line 661: Line 621:
                     for _, row in ipairs(query_data) do
                     for _, row in ipairs(query_data) do
                         if drop_areas_assoc[row['areas.id']] == nil then
                         if drop_areas_assoc[row['areas.id']] == nil then
                             value[#tpl_args.drop_areas+1] = row['areas.id']
                             tpl_args.drop_areas[#tpl_args.drop_areas+1] = row['areas.id']
                             tpl_args.drop_areas_data[#tpl_args.drop_areas_data+1] = row
                             tpl_args.drop_areas_data[#tpl_args.drop_areas_data+1] = row
                         else
                         else
Line 674: Line 634:
                 end
                 end
             end
             end
            return value
         end,
         end,
     },
     },
     drop_monsters = {
     drop_monsters = {
         inherit = false,
         no_copy = true,
         field = 'drop_monsters',
         field = 'drop_monsters',
         type = 'List (,) of Text',
         type = 'List (,) of Text',
         func = h.proc.list,
         func = function (tpl_args, frame)
         default = {},
            if tpl_args.drop_monsters ~= nil then
                tpl_args.drop_monsters = m_util.string.split(tpl_args.drop_monsters, ',%s*')
            end
         end,
     },
     },
     drop_text = {
     drop_text = {
         inherit = false,
         no_copy = true,
         field = 'drop_text',
         field = 'drop_text',
         type = 'Text',
         type = 'Text',
         func = h.proc.text,
         func = h.factory.cast_text('drop_text'),
     },
     },
     required_level = {
     required_level = {
         field = 'required_level_base',
         field = 'required_level_base',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('required_level'),
         default = 1,
         default = 1,
     },
     },
Line 699: Line 661:
         field = 'required_level',
         field = 'required_level',
         type = 'Integer',
         type = 'Integer',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             value = tpl_args.required_level
             tpl_args.required_level_final = tpl_args.required_level
             if value < cfg.base_item_required_level_threshold then
             if tpl_args.required_level_final < cfg.base_item_required_level_threshold then
                 value = 1
                 tpl_args.required_level_final = 1
             end
             end
            return value
         end,
         end,
         default = 1,
         default = 1,
Line 711: Line 672:
         field = 'required_dexterity',
         field = 'required_dexterity',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('required_dexterity'),
         default = 0,
         default = 0,
     },
     },
Line 717: Line 678:
         field = 'required_strength',
         field = 'required_strength',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('required_strength'),
         default = 0,
         default = 0,
     },
     },
Line 723: Line 684:
         field = 'required_intelligence',
         field = 'required_intelligence',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('required_intelligence'),
         default = 0,
         default = 0,
     },
     },
     inventory_icon = {
     inventory_icon = {
         inherit = false,
         no_copy = true,
         field = 'inventory_icon',
         field = 'inventory_icon',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             if not value then
             if not tpl_args.inventory_icon then
                 -- Certain types of items have default inventory icons
                 -- Certain types of items have default inventory icons
                 if i18n.default_inventory_icons[tpl_args.class_id] then
                 if i18n.default_inventory_icons[tpl_args.class_id] then
                     value = i18n.default_inventory_icons[tpl_args.class_id]
                     tpl_args.inventory_icon = i18n.default_inventory_icons[tpl_args.class_id]
                 elseif tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
                 elseif tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
                     value = i18n.default_inventory_icons['Prophecy']
                     tpl_args.inventory_icon = i18n.default_inventory_icons['Prophecy']
                 end
                 end
             end
             end
             tpl_args.inventory_icon_id = value or tpl_args.name
             tpl_args.inventory_icon_id = tpl_args.inventory_icon or tpl_args.name
             return string.format(i18n.files.inventory_icon, tpl_args.inventory_icon_id)  
             tpl_args.inventory_icon = string.format(i18n.files.inventory_icon, tpl_args.inventory_icon_id)  
         end,
         end,
     },
     },
     -- note: this must be called after inventory_icon to work correctly as it depends on tpl_args.inventory_icon_id being set
     -- note: this must be called after inventory_icon to work correctly as it depends on tpl_args.inventory_icon_id being set
     alternate_art_inventory_icons = {
     alternate_art_inventory_icons = {
         inherit = false,
         no_copy = true,
         field = 'alternate_art_inventory_icons',
         field = 'alternate_art_inventory_icons',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             return m_util.cast.table(value, {
             local icons = {}
                 callback = function (value)
            if tpl_args.alternate_art_inventory_icons ~= nil then
                     return string.format(i18n.files.inventory_icon, string.format('%s %s', tpl_args.inventory_icon_id, tostring(value)))
                local names = m_util.string.split(tpl_args.alternate_art_inventory_icons, ',%s*')
                 end,
               
             })
                 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
            tpl_args.alternate_art_inventory_icons = icons
         end,
         end,
         default = {},
         default = function (tpl_args, frame) return {} end,
     },
     },
     cannot_be_traded_or_modified = {
     cannot_be_traded_or_modified = {
         inherit = false,
         no_copy = true,
         field = 'cannot_be_traded_or_modified',
         field = 'cannot_be_traded_or_modified',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('cannot_be_traded_or_modified'),
         default = false,
         default = false,
     },
     },
Line 768: Line 733:
         field = 'help_text',
         field = 'help_text',
         type = 'Text',
         type = 'Text',
         func = h.proc.text,
         func = h.factory.cast_text('help_text'),
     },
     },
     flavour_text = {
     flavour_text = {
         inherit = false,
         no_copy = true,
         field = 'flavour_text',
         field = 'flavour_text',
         type = 'Text',
         type = 'Text',
         func = h.proc.text,
         func = h.factory.cast_text('flavour_text'),
     },
     },
     flavour_text_id = {
     flavour_text_id = {
         inherit = false,
         no_copy = true,
         field = 'flavour_text_id',
         field = 'flavour_text_id',
         type = 'String',
         type = 'String',
Line 785: Line 750:
         field = 'tags',
         field = 'tags',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = h.proc.factory.list{
         func = m_util.cast.factory.assoc_table('tags', {
             callback = m_util.validate.factory.in_table_keys{
             tbl = m_game.constants.tags,
                tbl = m_game.constants.tags,
            errmsg = i18n.errors.invalid_tag,
                errmsg = i18n.errors.invalid_tag,
         }),
                errlvl = 4,
            },
        },
         default = {},
     },
     },
     metadata_id = {
     metadata_id = {
         inherit = false,
         no_copy = true,
         field = 'metadata_id',
         field = 'metadata_id',
         type = 'String',
         type = 'String',
         --type = 'String(unique; size=200)',
         --type = 'String(unique; size=200)',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             if value ~= nil then
             if tpl_args.metadata_id == nil then
                 local results = m_cargo.query(
                 return
                    {'items'},
            end
                    {'items._pageName'},
            local results = m_cargo.query(
                    {
                {'items'},
                        where=string.format(
                {'items._pageName'},
                            'items.metadata_id="%s" AND items._pageName != "%s"',
                {
                            value,
                    where=string.format('items.metadata_id="%s" AND items._pageName != "%s"', tpl_args.metadata_id, m_cargo.addslashes(mw.title.getCurrentTitle().fullText))
                            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']))
                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
            return value
         end,
         end,
     },
     },
     influences = {
     influences = {
         inherit = false,
         no_copy = true,
         field = 'influences',
         field = 'influences',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = h.proc.factory.list{
         func = m_util.cast.factory.assoc_table('influences', {
             callback = m_util.validate.factory.in_table_keys{
             tbl = m_game.constants.influences,
                tbl = m_game.constants.influences,
            errmsg = i18n.errors.invalid_influence,
                errmsg = i18n.errors.invalid_influence,
         }),
                errlvl = 4,
            },
        },
         default = {},
     },
     },
     is_fractured = {
     is_fractured = {
         inherit = false,
         no_copy = true,
         field = 'is_fractured',
         field = 'is_fractured',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('is_fractured'),
         default = false,
         default = false,
     },
     },
     is_synthesised = {
     is_synthesised = {
         inherit = false,
         no_copy = true,
         field = 'is_synthesised',
         field = 'is_synthesised',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('is_synthesised'),
         default = false,
         default = false,
     },
     },
     is_veiled = {
     is_veiled = {
         inherit = false,
         no_copy = true,
         field = 'is_veiled',
         field = 'is_veiled',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('is_veiled'),
         default = false,
         default = false,
     },
     },
     is_replica = {
     is_replica = {
         inherit = false,
         no_copy = true,
         field = 'is_replica',
         field = 'is_replica',
         type = 'Boolean',
         type = 'Boolean',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             value = m_util.cast.boolean(value)
             m_util.cast.factory.boolean('is_replica')(tpl_args, frame)
             if value == true and tpl_args.rarity_id ~= 'unique' then
             if tpl_args.is_replica == true and tpl_args.rarity_id ~= 'unique' then
                 error(string.format(i18n.errors.non_unique_flag, 'is_replica'))
                 error(string.format(i18n.errors.non_unique_flag, 'is_replica'))
             end
             end
            return value
         end,
         end,
         default = false,
         default = false,
     },
     },
     is_corrupted = {
     is_corrupted = {
         inherit = false,
         no_copy = true,
         field = 'is_corrupted',
         field = 'is_corrupted',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('is_corrupted'),
         default = false,
         default = false,
     },
     },
     is_relic = {
     is_relic = {
         inherit = false,
         no_copy = true,
         field = 'is_relic',
         field = 'is_relic',
         type = 'Boolean',
         type = 'Boolean',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             value = m_util.cast.boolean(value)
             m_util.cast.factory.boolean('is_relic')(tpl_args, frame)
             if value == true and tpl_args.rarity_id ~= 'unique' then
             if tpl_args.is_relic == true and tpl_args.rarity_id ~= 'unique' then
                 error(string.format(i18n.errors.non_unique_flag, 'is_relic'))
                 error(string.format(i18n.errors.non_unique_flag, 'is_relic'))
             end
             end
            return value
         end,
         end,
         default = false,
         default = false,
     },
     },
     is_fated = {
     is_fated = {
         inherit = false,
         no_copy = true,
         field = 'is_fated',
         field = 'is_fated',
         type = 'Boolean',
         type = 'Boolean',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             value = m_util.cast.boolean(value)
             m_util.cast.factory.boolean('is_fated')(tpl_args, frame)
             if value == true and tpl_args.rarity_id ~= 'unique' then
             if tpl_args.is_fated == true and tpl_args.rarity_id ~= 'unique' then
                 error(string.format(i18n.errors.non_unique_flag, 'is_fated'))
                 error(string.format(i18n.errors.non_unique_flag, 'is_fated'))
             end
             end
            return value
         end,
         end,
         default = false,
         default = false,
     },
     },
     is_prophecy = {
     is_prophecy = {
         inherit = false,
         no_copy = true,
         field = nil,
         field = nil,
         type = nil,
         type = nil,
         func = function (tpl_args, frame, value)
         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')
             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
         end
     },
     },
     is_blight_item = {
     is_blight_item = {
         inherit = false,
         no_copy = true,
         field = nil,
         field = nil,
         type = nil,
         type = nil,
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             tpl_args._flags.is_blight_item = (tpl_args.blight_item_tier ~= nil)
             tpl_args._flags.is_blight_item = (tpl_args.blight_item_tier ~= nil)
            return value
         end
         end
     },
     },
     is_drop_restricted = {
     is_drop_restricted = {
         inherit = false,
         no_copy  = true,
         field = 'is_drop_restricted',
         field = 'is_drop_restricted',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('is_drop_restricted'),
         default = function (tpl_args, frame)
         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
             -- 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
             for _, class in ipairs({'Incubator'}) do
Line 943: Line 891:
     },
     },
     purchase_costs = {
     purchase_costs = {
        field = nil,
         func = function(tpl_args, frame)
        type = nil,
         func = function (tpl_args, frame, value)
             local purchase_costs = {}
             local purchase_costs = {}
             for _, rarity_id in ipairs(m_game.constants.rarity_order) do
             for _, rarity_id in ipairs(m_game.constants.rarity_order) do
Line 972: Line 918:
                     end
                     end
                 end
                 end
               
                 purchase_costs[rarity_id] = rtbl
                 purchase_costs[rarity_id] = rtbl
             end
             end
             return purchase_costs
              
            tpl_args.purchase_costs = purchase_costs
         end,
         end,
         func_fetch = function (tpl_args, frame)
         func_fetch = function(tpl_args, frame)
             if tpl_args.rarity_id ~= 'unique' then
             if tpl_args.rarity_id ~= 'unique' then
                 return
                 return
Line 1,009: Line 957:
     },
     },
     sell_prices_override = {
     sell_prices_override = {
         inherit = false,
         no_copy = true,
        field = nil,
         func = function(tpl_args, frame)
        type = nil,
         func = function (tpl_args, frame, value)
             -- these variables are also used by mods when setting automatic sell prices
             -- these variables are also used by mods when setting automatic sell prices
             tpl_args.sell_prices = {}
             tpl_args.sell_prices = {}
             tpl_args.sell_price_order = {}
             tpl_args.sell_price_order = {}
           
           
             local name
             local name
             local amount
             local amount
Line 1,034: Line 982:
                 end
                 end
             until name == nil or amount == nil  
             until name == nil or amount == nil  
           
             -- if sell prices are set, the override is active
             -- if sell prices are set, the override is active
             for _, _ in pairs(tpl_args.sell_prices) do
             for _, _ in pairs(tpl_args.sell_prices) do
Line 1,039: Line 988:
                 break
                 break
             end
             end
            return value
         end,
         end,
     },
     },
Line 1,045: Line 993:
     -- specific section
     -- specific section
     --
     --
 
   
     -- Most item classes
     -- Most item classes
     quality = {
     quality = {
         inherit = false,
         no_copy = true,
         field = 'quality',
         field = 'quality',
         type = 'Integer',
         type = 'Integer',
         -- Can be set manually, but default to Q20 for unique weapons/body armours
         -- 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
         -- Also must copy to stat for the stat adjustments to work properly
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             local quality = tonumber(value)
             local quality = tonumber(tpl_args.quality)
            --
             if quality == nil then
             if quality == nil then
                 if tpl_args.rarity_id ~= 'unique' then
                 if tpl_args.rarity_id ~= 'unique' then
Line 1,064: Line 1,013:
                 end
                 end
             end
             end
           
            tpl_args.quality = quality
           
             local stat = {
             local stat = {
                 min = quality,
                 min = quality,
Line 1,069: Line 1,021:
                 avg = quality,
                 avg = quality,
             }
             }
           
             core.stats_update(tpl_args, 'quality', stat, nil, '_stats')
             core.stats_update(tpl_args, 'quality', stat, nil, '_stats')
           
             if tpl_args.class_id == 'UtilityFlask' or tpl_args.class_id == 'UtilityFlaskCritical' then
             if tpl_args.class_id == 'UtilityFlask' or tpl_args.class_id == 'UtilityFlaskCritical' then
                 core.stats_update(tpl_args, 'quality_flask_duration', stat, nil, '_stats')
                 core.stats_update(tpl_args, 'quality_flask_duration', stat, nil, '_stats')
Line 1,076: Line 1,030:
                 core.stats_update(tpl_args, 'map_item_drop_quantity_+%', stat, nil, '_stats')
                 core.stats_update(tpl_args, 'map_item_drop_quantity_+%', stat, nil, '_stats')
             end
             end
            return quality
         end,
         end,
     },
     },
Line 1,083: Line 1,036:
         field = 'is_talisman',
         field = 'is_talisman',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('is_talisman'),
         default = false,
         default = false,
     },
     },
   
     talisman_tier = {
     talisman_tier = {
         field = 'talisman_tier',
         field = 'talisman_tier',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('talisman_tier'),
     },
     },
   
     -- flasks
     -- flasks
     charges_max = {
     charges_max = {
         field = 'charges_max',
         field = 'charges_max',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('charges_max'),
     },
     },
     charges_per_use = {
     charges_per_use = {
         field = 'charges_per_use',
         field = 'charges_per_use',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('charges_per_use'),
     },
     },
     flask_mana = {
     flask_mana = {
         field = 'mana',
         field = 'mana',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('flask_mana'),
     },
     },
     flask_life = {
     flask_life = {
         field = 'life',
         field = 'life',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('flask_life'),
     },
     },
     flask_duration = {
     flask_duration = {
         field = 'duration',
         field = 'duration',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('flask_duration'),
     },
     },
     buff_id = {
     buff_id = {
Line 1,125: Line 1,080:
         field = 'buff_values',
         field = 'buff_values',
         type = 'List (,) of Integer',
         type = 'List (,) of Integer',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             local values = {}
             local values = {}
             local i = 0
             local i = 0
Line 1,137: Line 1,092:
             -- needed so the values copyied from unique item base isn't overriden
             -- needed so the values copyied from unique item base isn't overriden
             if #values >= 1 then
             if #values >= 1 then
                 value = values
                 tpl_args.buff_values = values
             end
             end
            return value
         end,
         end,
         func_copy = function (tpl_args, frame)
         func_copy = function(tpl_args, frame)
             tpl_args.buff_values = m_util.string.split(tpl_args.buff_values, ',%s*')
             tpl_args.buff_values = m_util.string.split(tpl_args.buff_values, ',%s*')
         end,
         end,
         default = {},
         default = function (tpl_args, frame) return {} end,
     },
     },
     buff_stat_text = {
     buff_stat_text = {
Line 1,154: Line 1,108:
         field = 'icon',
         field = 'icon',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             return string.format(i18n.files.status_icon, tpl_args.name)
             tpl_args.buff_icon = string.format(i18n.files.status_icon, tpl_args.name)  
         end,
         end,
     },
     },
Line 1,163: Line 1,117:
         field = 'critical_strike_chance',
         field = 'critical_strike_chance',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('critical_strike_chance'),
     },
     },
     attack_speed = {
     attack_speed = {
         field = 'attack_speed',
         field = 'attack_speed',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('attack_speed'),
     },
     },
     weapon_range = {
     weapon_range = {
         field = 'weapon_range',
         field = 'weapon_range',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('weapon_range'),
     },
     },
     physical_damage_min = {
     physical_damage_min = {
         field = 'physical_damage_min',
         field = 'physical_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('physical_damage_min'),
     },
     },
     physical_damage_max = {
     physical_damage_max = {
         field = 'physical_damage_max',
         field = 'physical_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('physical_damage_max'),
     },
     },
     fire_damage_min = {
     fire_damage_min = {
         field = 'fire_damage_min',
         field = 'fire_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('fire_damage_min'),
         default = 0,
         default = 0,
     },
     },
Line 1,194: Line 1,148:
         field = 'fire_damage_max',
         field = 'fire_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('fire_damage_max'),
         default = 0,
         default = 0,
     },
     },
Line 1,200: Line 1,154:
         field = 'cold_damage_min',
         field = 'cold_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('cold_damage_min'),
         default = 0,
         default = 0,
     },
     },
Line 1,206: Line 1,160:
         field = 'cold_damage_max',
         field = 'cold_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('cold_damage_max'),
         default = 0,
         default = 0,
     },
     },
Line 1,212: Line 1,166:
         field = 'lightning_damage_min',
         field = 'lightning_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('lightning_damage_min'),
         default = 0,
         default = 0,
     },
     },
Line 1,218: Line 1,172:
         field = 'lightning_damage_max',
         field = 'lightning_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('lightning_damage_max'),
         default = 0,
         default = 0,
     },
     },
Line 1,224: Line 1,178:
         field = 'chaos_damage_min',
         field = 'chaos_damage_min',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('chaos_damage_min'),
         default = 0,
         default = 0,
     },
     },
Line 1,230: Line 1,184:
         field = 'chaos_damage_max',
         field = 'chaos_damage_max',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('chaos_damage_max'),
         default = 0,
         default = 0,
     },
     },
Line 1,237: Line 1,191:
         field = 'armour',
         field = 'armour',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('armour'),
         default = 0,
         default = 0,
     },
     },
Line 1,243: Line 1,197:
         field = 'energy_shield',
         field = 'energy_shield',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('energy_shield'),
         default = 0,
         default = 0,
     },
     },
Line 1,249: Line 1,203:
         field = 'evasion',
         field = 'evasion',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('evasion'),
         default = 0,
         default = 0,
     },
     },
Line 1,256: Line 1,210:
         field = 'movement_speed',
         field = 'movement_speed',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('movement_speed'),
         default = 0,
         default = 0,
     },
     },
Line 1,263: Line 1,217:
         field = 'block',
         field = 'block',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('block'),
     },
     },
     -- skill gem stuff
     -- skill gem stuff
Line 1,269: Line 1,223:
         field = 'gem_description',
         field = 'gem_description',
         type = 'Text',
         type = 'Text',
         func = h.proc.text,
         func = h.factory.cast_text('gem_description'),
     },
     },
     dexterity_percent = {
     dexterity_percent = {
         field = 'dexterity_percent',
         field = 'dexterity_percent',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.percentage,
         func = m_util.cast.factory.percentage('dexterity_percent'),
     },
     },
     strength_percent = {
     strength_percent = {
         field = 'strength_percent',
         field = 'strength_percent',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.percentage,
         func = m_util.cast.factory.percentage('strength_percent'),
     },
     },
     intelligence_percent = {
     intelligence_percent = {
         field = 'intelligence_percent',
         field = 'intelligence_percent',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.percentage,
         func = m_util.cast.factory.percentage('intelligence_percent'),
     },
     },
     primary_attribute = {
     primary_attribute = {
         field = 'primary_attribute',
         field = 'primary_attribute',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             for _, attr in ipairs(m_game.constants.attribute_order) do
             for _, attr in ipairs(m_game.constants.attribute_order) do
                 local val = tpl_args[attr .. '_percent']  
                 local val = tpl_args[attr .. '_percent']  
                 if val and val >= 60 then
                 if val and val >= 60 then
                     return attr
                    tpl_args['primary_attribute'] = attr
                     return
                 end
                 end
             end
             end
             return 'none'
             tpl_args['primary_attribute'] = 'none'
         end,
         end,
     },
     },
Line 1,302: Line 1,257:
         field = 'gem_tags',
         field = 'gem_tags',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = h.proc.factory.list{
        -- TODO: default rework
             callback = m_util.validate.factory.in_table_keys{
         func = function(tpl_args, frame)
                 tbl = m_game.constants.item.gem_tags_lookup,
             if tpl_args.gem_tags then
                errmsg = i18n.errors.invalid_gem_tag,
                 tpl_args.gem_tags = m_util.string.split(tpl_args.gem_tags, ',%s*')
                errlvl = 4,
             end
             },
         end,
         },
         default = function (tpl_args, frame) return {} end,
         default = {},
     },
     },
     -- Support gems only
     -- Support gems only
Line 1,320: Line 1,274:
         field = 'support_gem_letter_html',
         field = 'support_gem_letter_html',
         type = 'Text',
         type = 'Text',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             if tpl_args.support_gem_letter == nil then
             if tpl_args.support_gem_letter == nil then
                 return nil
                 return
             end
             end
             for k, v in pairs(m_game.constants.attributes) do
       
                 local key = string.format('%s_percent', k)
            -- TODO replace this with a loop possibly
                 if tpl_args[key] and tpl_args[key] > 50 then
            local css_map = {
                     value = tostring(
                strength = 'red',
                        mw.html.create('span')
                intelligence = 'blue',
                            :attr('class', string.format('support-gem-id-%s', v.color))
                dexterity = 'green',
                            :wikitext(tpl_args.support_gem_letter)
            }
                    )
            local id
             for k, v in pairs(css_map) do
                 k = string.format('%s_percent', k)
                 if tpl_args[k] and tpl_args[k] > 50 then
                     id = v
                     break
                     break
                 end
                 end
             end
             end
             return value
              
            if id ~= nil then
                local container = mw.html.create('span')
                container
                    :attr('class', string.format('support-gem-id-%s', id))
                    :wikitext(tpl_args.support_gem_letter)
                    :done()
                tpl_args.support_gem_letter_html = tostring(container)
            end
         end,
         end,
     },
     },
Line 1,344: Line 1,310:
         field = 'tier',
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('map_tier'),
     },
     },
     map_guild_character = {
     map_guild_character = {
Line 1,359: Line 1,325:
         field = 'area_level',
         field = 'area_level',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('map_area_level'),
     },
     },
     unique_map_guild_character = {
     unique_map_guild_character = {
         field = 'unique_guild_character',
         field = 'unique_guild_character',
         type = 'String(size=1)',
         type = 'String(size=1)',
        func = nil,
         func_copy = function(tpl_args, frame)
         func_copy = function (tpl_args, frame)
             tpl_args.map_guild_character = tpl_args.unique_map_guild_character
             tpl_args.map_guild_character = tpl_args.unique_map_guild_character
         end,
         end,
        func = nil,
     },
     },
     unique_map_area_id = {
     unique_map_area_id = {
Line 1,373: Line 1,339:
         type = 'String',
         type = 'String',
         func = nil, -- TODO: Validate against a query?
         func = nil, -- TODO: Validate against a query?
         func_copy = function (tpl_args, frame)
         func_copy = function(tpl_args, frame)
             tpl_args.map_area_id = tpl_args.unique_map_area_id
             tpl_args.map_area_id = tpl_args.unique_map_area_id
         end,
         end,
Line 1,380: Line 1,346:
         field = 'unique_area_level',
         field = 'unique_area_level',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('unique_map_area_level'),
         func_copy = function (tpl_args, frame)
         func_copy = function(tpl_args, frame)
             tpl_args.map_area_level = tpl_args.unique_map_area_level
             tpl_args.map_area_level = tpl_args.unique_map_area_level
         end,
         end,
Line 1,388: Line 1,354:
         field = 'series',
         field = 'series',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             if tpl_args.rarity == 'normal' and value == nil then
             if tpl_args.rarity == 'normal' and tpl_args.map_series == nil then
                 error(string.format(i18n.errors.generic_required_parameter, 'map_series'))
                 error(string.format(i18n.errors.generic_required_parameter, 'map_series'))
             end
             end
            return value
         end,
         end,
     },
     },
Line 1,399: Line 1,364:
         field = 'x',
         field = 'x',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_x'),
     },
     },
     atlas_y = {
     atlas_y = {
         field = 'y',
         field = 'y',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_y'),
     },
     },
     atlas_region_id = {
     atlas_region_id = {
Line 1,414: Line 1,379:
         field = 'region_minimum',
         field = 'region_minimum',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_region_minimum'),
     },
     },
     atlas_x0 = {
     atlas_x0 = {
         field = 'x0',
         field = 'x0',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_x0'),
     },
     },
     atlas_x1 = {
     atlas_x1 = {
         field = 'x1',
         field = 'x1',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_x1'),
     },
     },
     atlas_x2 = {
     atlas_x2 = {
         field = 'x2',
         field = 'x2',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_x2'),
     },
     },
     atlas_x3 = {
     atlas_x3 = {
         field = 'x3',
         field = 'x3',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_x3'),
     },
     },
     atlas_x4 = {
     atlas_x4 = {
         field = 'x4',
         field = 'x4',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_x4'),
     },
     },
     atlas_y0 = {
     atlas_y0 = {
         field = 'y0',
         field = 'y0',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_0'),
     },
     },
     atlas_y1 = {
     atlas_y1 = {
         field = 'y1',
         field = 'y1',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_y1'),
     },
     },
     atlas_y2 = {
     atlas_y2 = {
         field = 'y2',
         field = 'y2',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_y2'),
     },
     },
     atlas_y3 = {
     atlas_y3 = {
         field = 'y3',
         field = 'y3',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_y3'),
     },
     },
     atlas_y4 = {
     atlas_y4 = {
         field = 'y4',
         field = 'y4',
         type = 'Float',
         type = 'Float',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_y4'),
     },
     },
     atlas_map_tier0 = {
     atlas_map_tier0 = {
         field = 'map_tier0',
         field = 'map_tier0',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_map_tier0'),
     },
     },
     atlas_map_tier1 = {
     atlas_map_tier1 = {
         field = 'map_tier1',
         field = 'map_tier1',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_map_tier1'),
     },
     },
     atlas_map_tier2 = {
     atlas_map_tier2 = {
         field = 'map_tier2',
         field = 'map_tier2',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_map_tier2'),
     },
     },
     atlas_map_tier3 = {
     atlas_map_tier3 = {
         field = 'map_tier3',
         field = 'map_tier3',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_map_tier3'),
     },
     },
     atlas_map_tier4 = {
     atlas_map_tier4 = {
         field = 'map_tier4',
         field = 'map_tier4',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('atlas_map_tier4'),
     },
     },
     atlas_connections = {
     atlas_connections = {
         field = nil,
         field = nil,
         type = nil,
         type = nil,
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             value = {}
             tpl_args.atlas_connections = {}
           
             local cont = true
             local cont = true
             local i = 1
             local i = 1
Line 1,516: Line 1,482:
                     end
                     end
                      
                      
                     value[data.map2] = data
                     tpl_args.atlas_connections[data.map2] = data
                     table.insert(tpl_args._subobjects, data)
                     table.insert(tpl_args._subobjects, data)
                 else
                 else
                     cont = false
                     cont = false
                     if i == 1 then
                     if i == 1 then
                         value = nil
                         tpl_args.atlas_connections = nil
                     end
                     end
                 end
                 end
               
                 i = i + 1
                 i = i + 1
             end
             end
            return value
         end,
         end,
        default = nil,
     },
     },
     --
     --
Line 1,535: Line 1,502:
         field = 'stack_size',
         field = 'stack_size',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('stack_size'),
     },
     },
     stack_size_currency_tab = {
     stack_size_currency_tab = {
         field = 'stack_size_currency_tab',
         field = 'stack_size_currency_tab',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('stack_size_currency_tab'),
     },
     },
     description = {
     description = {
         field = 'description',
         field = 'description',
         type = 'Text',
         type = 'Text',
         func = h.proc.text,
         func = h.factory.cast_text('description'),
     },
     },
     cosmetic_type = {
     cosmetic_type = {
         field = 'cosmetic_type',
         field = 'cosmetic_type',
         type = 'String',
         type = 'String',
         func = h.proc.text,
         func = h.factory.cast_text('cosmetic_type'),
     },
     },
     -- for essences
     -- for essences
     is_essence = {
     is_essence = {
         field = nil,
         field = nil,
        type = nil,
         func = m_util.cast.factory.boolean('is_essence'),
         func = h.proc.boolean,
         default = false,
         default = false,
     },
     },
Line 1,562: Line 1,528:
         field = 'level_restriction',
         field = 'level_restriction',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('essence_level_restriction'),
     },
     },
     essence_level = {
     essence_level = {
         field = 'level',
         field = 'level',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('essence_level'),
     },
     },
     essence_type = {
     essence_type = {
         field = 'type',
         field = 'type',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('essence_type'),
     },
     },
     essence_category = {
     essence_category = {
Line 1,583: Line 1,549:
         field = 'tier',
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('blight_item_tier'),
     },
     },
     -- harvest seeds
     -- harvest seeds
Line 1,589: Line 1,555:
         field = 'type_id',
         field = 'type_id',
         type = 'String',
         type = 'String',
        func = nil,
     },
     },
     seed_type = {
     seed_type = {
         field = 'type',
         field = 'type',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function (tpl_args, frame)
             if tpl_args.seed_type_id ~= 'none' or tpl_args.seed_type_id ~= nil then
             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]
                 tpl_args.seed_type = m_game.seed_types[tpl_args.seed_type_id]
             end
             end
            return value
         end
         end
     },
     },
Line 1,604: Line 1,568:
         field = nil,
         field = nil,
         type = nil,
         type = nil,
         func = function (tpl_args, frame, value)
         func = function (tpl_args, frame)
             if tpl_args.seed_type ~= nil then
             if tpl_args.seed_type ~= nil then
                 value = m_util.html.poe_color(tpl_args.seed_type_id, tpl_args.seed_type)
                 tpl_args.seed_type_html = m_util.html.poe_color(tpl_args.seed_type_id, tpl_args.seed_type)
             end
             end
            return value
         end
         end
     },
     },
Line 1,614: Line 1,577:
         field = 'effect',
         field = 'effect',
         type = 'Text',
         type = 'Text',
        func = nil,
     },
     },
     seed_tier = {
     seed_tier = {
         field = 'tier',
         field = 'tier',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('seed_tier'),
     },
     },
     seed_growth_cycles = {
     seed_growth_cycles = {
         field = 'growth_cycles',
         field = 'growth_cycles',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('seed_growth_cycles'),
     },
     },
     seed_required_nearby_seed_tier = {
     seed_required_nearby_seed_tier = {
         field = 'required_nearby_seed_tier',
         field = 'required_nearby_seed_tier',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('seed_required_nearby_seed_tier'),
     },
     },
     seed_required_nearby_seed_amount = {
     seed_required_nearby_seed_amount = {
         field = 'required_nearby_seed_amount',
         field = 'required_nearby_seed_amount',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('seed_required_nearby_seed_amount'),
     },
     },
     seed_consumed_wild_lifeforce_percentage = {
     seed_consumed_wild_lifeforce_percentage = {
         field = 'consumed_wild_lifeforce_percentage',
         field = 'consumed_wild_lifeforce_percentage',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('seed_consumed_wild_lifeforce_percentage'),
         default = 0,
         default = 0,
     },
     },
Line 1,645: Line 1,607:
         field = 'consumed_vivid_lifeforce_percentage',
         field = 'consumed_vivid_lifeforce_percentage',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('seed_consumed_vivid_lifeforce_percentage'),
         default = 0,
         default = 0,
     },
     },
Line 1,651: Line 1,613:
         field = 'consumed_primal_lifeforce_percentage',
         field = 'consumed_primal_lifeforce_percentage',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('seed_consumed_primal_lifeforce_percentage'),
         default = 0,
         default = 0,
     },
     },
Line 1,657: Line 1,619:
         field = 'granted_craft_option_ids',
         field = 'granted_craft_option_ids',
         type = 'List (,) of String',
         type = 'List (,) of String',
         func = h.proc.list,
         func = m_util.cast.factory.number('seed_grandted_craft_option_ids'),
         default = {},
         default = 0,
     },
     },
     --
     --
Line 1,666: Line 1,628:
         field = 'radius',
         field = 'radius',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('plant_booster_radius'),
     },
     },
     plant_booster_lifeforce = {
     plant_booster_lifeforce = {
         field = 'lifeforce',
         field = 'lifeforce',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('plant_booster_lifeforce'),
     },
     },
     plant_booster_additional_crafting_options = {
     plant_booster_additional_crafting_options = {
         field = 'additional_crafting_options',
         field = 'additional_crafting_options',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('plant_booster_additional_crafting_options'),
     },
     },
     plant_booster_extra_chances = {
     plant_booster_extra_chances = {
         field = 'extra_chances',
         field = 'extra_chances',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('plant_booster_extra_chances'),
     },
     },
     --
     --
Line 1,689: Line 1,651:
         field = 'required_job_id',
         field = 'required_job_id',
         type = 'String',
         type = 'String',
         func = h.proc.text,
         func = h.factory.cast_text('heist_required_job_id'),
     },
     },
     heist_required_job_level = {
     heist_required_job_level = {
         field = 'required_job_level',
         field = 'required_job_level',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('heist_required_job_level'),
     },
     },
     heist_data = {
     heist_data = {
        field = nil,
         func = function (tpl_args, frame)
        type = nil,
         func = function (tpl_args, frame, value)
             if tpl_args.heist_required_job_level then
             if tpl_args.heist_required_job_level then
                 if tpl_args.heist_required_job_id then
                 if tpl_args.heist_required_job_id then
Line 1,710: Line 1,670:
                         }
                         }
                     )
                     )
                   
                     local npcs = {}
                     local npcs = {}
                   
                     for _, row in ipairs(results) do
                     for _, row in ipairs(results) do
                         npcs[#npcs+1] = row['heist_npcs.name']
                         npcs[#npcs+1] = row['heist_npcs.name']
                     end
                     end
                   
                     tpl_args.heist_required_npcs = table.concat(npcs, ', ')
                     tpl_args.heist_required_npcs = table.concat(npcs, ', ')
                     tpl_args.heist_required_job = results[1]['heist_jobs.name']
                     tpl_args.heist_required_job = results[1]['heist_jobs.name']
Line 1,720: Line 1,683:
                 end
                 end
             end
             end
            return value
         end,
         end,
     },
     },
Line 1,729: Line 1,691:
         field = 'is_master_doodad',
         field = 'is_master_doodad',
         type = 'Boolean',
         type = 'Boolean',
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('is_master_doodad'),
     },
     },
     master = {
     master = {
         field = 'master',
         field = 'master',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
        -- todo validate against list of master names
            return m_util.validate.factory.in_table_keys{
         func = m_util.cast.factory.table('master', {key='full', tbl=m_game.constants.masters}),
                tbl = m_util.table.column(m_game.constants.masters, 'long_upper', 'full'),
                errmsg = i18n.errors.invalid_master,
                errlvl = 3,
            }(value)
        end,
     },
     },
     master_level_requirement = {
     master_level_requirement = {
         field = 'level_requirement',
         field = 'level_requirement',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('master_level_requirement'),
     },
     },
     master_favour_cost = {
     master_favour_cost = {
         field = 'favour_cost',
         field = 'favour_cost',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('master_favour_cost'),
     },
     },
     variation_count = {
     variation_count = {
         field = 'variation_count',
         field = 'variation_count',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('variation_count'),
     },
     },
     -- Propehcy
     -- Propehcy
Line 1,766: Line 1,723:
         field = 'prediction_text',
         field = 'prediction_text',
         type = 'Text',
         type = 'Text',
         func = h.proc.text,
         func = h.factory.cast_text('prediction_text'),
     },
     },
     seal_cost = {
     seal_cost = {
         field = 'seal_cost',
         field = 'seal_cost',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('seal_cost'),
     },
     },
     prophecy_reward = {
     prophecy_reward = {
         field = 'reward',
         field = 'reward',
         type = 'Text',
         type = 'Text',
         func = h.proc.text,
         func = h.factory.cast_text('prophecy_reward'),
     },
     },
     prophecy_objective = {
     prophecy_objective = {
         field = 'objective',
         field = 'objective',
         type = 'Text',
         type = 'Text',
        func = h.proc.text,
      func = h.factory.cast_text('prophecy_objective'),
     },
     },
     -- Divination cards
     -- Divination cards
Line 1,787: Line 1,744:
         field = 'card_art',
         field = 'card_art',
         type = 'Page',
         type = 'Page',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             return string.format(i18n.files.divination_card_art, value or tpl_args.name)
             tpl_args.card_art = string.format(i18n.files.divination_card_art, tpl_args.card_art or tpl_args.name)
         end,
         end,
     },
     },
Line 1,797: Line 1,754:
     -- For rarity != normal, rarity already verified
     -- For rarity != normal, rarity already verified
     base_item = {
     base_item = {
         inherit = false,
         no_copy = true,
         field = 'base_item',
         field = 'base_item',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             return tpl_args.base_item_data['items.name']
             tpl_args.base_item = tpl_args.base_item_data['items.name']
         end,
         end,
     },
     },
     base_item_id = {
     base_item_id = {
         inherit = false,
         no_copy = true,
         field = 'base_item_id',
         field = 'base_item_id',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             return tpl_args.base_item_data['items.metadata_id']
             tpl_args.base_item_id = tpl_args.base_item_data['items.metadata_id']
         end,
         end,
     },
     },
     base_item_page = {
     base_item_page = {
         inherit = false,
         no_copy = true,
         field = 'base_item_page',
         field = 'base_item_page',
         type = 'Page',
         type = 'Page',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             return tpl_args.base_item_data['items._pageName']
             tpl_args.base_item_page = tpl_args.base_item_data['items._pageName']
         end,
         end,
     },
     },
     name_list = {
     name_list = {
         inherit = false,
         no_copy = true,
         field = 'name_list',
         field = 'name_list',
         type = 'List (�) of String',
         type = 'List (�) of String',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             value = m_util.cast.table(value)
             if tpl_args.name_list ~= nil then
            value[#value+1] = tpl_args.name
                tpl_args.name_list = m_util.string.split(tpl_args.name_list, ',%s*')
             return value
                tpl_args.name_list[#tpl_args.name_list+1] = tpl_args.name
             else
                tpl_args.name_list = {tpl_args.name}
            end
         end,
         end,
        default = {},
     },
     },
     frame_type = {
     frame_type = {
         inherit = false,
         no_copy = true,
         field = 'frame_type',
         field = 'frame_type',
         type = 'String',
         type = 'String',
         func = function (tpl_args, frame, value)
        property = nil,
             if value then
         func = function(tpl_args, frame)
                 return value
             if tpl_args.frame_type then
                 return
             end
             end
             if tpl_args._flags.is_prophecy then
             if tpl_args._flags.is_prophecy then
                 return 'prophecy'
                 tpl_args.frame_type = 'prophecy'
                return
             end
             end
             local var = cfg.class_specifics[tpl_args.class_id]
             local var = cfg.class_specifics[tpl_args.class_id]
             if var ~= nil and var.frame_type ~= nil then
             if var ~= nil and var.frame_type ~= nil then
                 return var.frame_type
                 tpl_args.frame_type = var.frame_type
                return
             end
             end
             if tpl_args.is_relic then
             if tpl_args.is_relic then
                 return 'relic'
                 tpl_args.frame_type = 'relic'
                return
             end
             end
             return tpl_args.rarity_id
             tpl_args.frame_type = tpl_args.rarity_id
         end,
         end,
     },
     },
Line 1,856: Line 1,819:
     --  
     --  
     mods = {
     mods = {
         field = nil,
         default = function (tpl_args, frame) return {} end,
        type = nil,
        func = nil,
        default = {},
         func_fetch = function (tpl_args, frame)
         func_fetch = function (tpl_args, frame)
             -- Fetch implicit mods from base item
             -- Fetch implicit mods from base item
Line 1,887: Line 1,847:
     },
     },
     physical_damage_html = {
     physical_damage_html = {
         inherit = false,
         no_copy = true,
         field = 'physical_damage_html',
         field = 'physical_damage_html',
         type = 'Text',
         type = 'Text',
         func = h.proc.factory.damage_html{type = 'physical'},
         func = core.factory.damage_html{key='physical'},
     },
     },
     fire_damage_html = {
     fire_damage_html = {
         inherit = false,
         no_copy = true,
         field = 'fire_damage_html',
         field = 'fire_damage_html',
         type = 'Text',
         type = 'Text',
         func = h.proc.factory.damage_html{type = 'fire'},
         func = core.factory.damage_html{key='fire'},
     },
     },
     cold_damage_html = {
     cold_damage_html = {
         inherit = false,
         no_copy = true,
         field = 'cold_damage_html',
         field = 'cold_damage_html',
         type = 'Text',
         type = 'Text',
         func = h.proc.factory.damage_html{type = 'cold'},
         func = core.factory.damage_html{key='cold'},
     },
     },
     lightning_damage_html = {
     lightning_damage_html = {
         inherit = false,
         no_copy = true,
         field = 'lightning_damage_html',
         field = 'lightning_damage_html',
         type = 'Text',
         type = 'Text',
         func = h.proc.factory.damage_html{type = 'lightning'},
         func = core.factory.damage_html{key='lightning'},
     },
     },
     chaos_damage_html = {
     chaos_damage_html = {
         inherit = false,
         no_copy = true,
         field = 'chaos_damage_html',
         field = 'chaos_damage_html',
         type = 'Text',
         type = 'Text',
         func = h.proc.factory.damage_html{type = 'chaos'},
         func = core.factory.damage_html{key='chaos'},
     },
     },
     damage_avg = {
     damage_avg = {
         inherit = false,
         no_copy = true,
         field = 'damage_avg',
         field = 'damage_avg',
         type = 'Text',
         type = 'Text',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             local dmg = {min=0, max=0}
             local dmg = {min=0, max=0}
             for key, _ in pairs(dmg) do
             for key, _ in pairs(dmg) do
Line 1,927: Line 1,887:
                 end
                 end
             end
             end
           
             dmg = (dmg.min + dmg.max) / 2
             dmg = (dmg.min + dmg.max) / 2
             return dmg
              
            tpl_args.damage_avg = dmg
         end,
         end,
     },
     },
     damage_html = {
     damage_html = {
         inherit = false,
         no_copy = true,
         field = 'damage_html',
         field = 'damage_html',
         type = 'Text',
         type = 'Text',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             local text = {}
             local text = {}
             for _, dkey in ipairs(m_game.constants.damage_type_order) do
             for _, dkey in ipairs(m_game.constants.damage_type_order) do
                 local range = tpl_args[dkey .. '_damage_html']
                 local value = tpl_args[dkey .. '_damage_html']
                 if range ~= nil then
                 if value ~= nil then
                     text[#text+1] = range
                     text[#text+1] = value
                 end
                 end
             end
             end
             if #text > 0 then
             if #text > 0 then
                 value = table.concat(text, '<br>')
                 tpl_args.damage_html = table.concat(text, '<br>')
             end
             end
            return value
         end,
         end,
     },
     },
     item_limit = {
     item_limit = {
         inherit = false,
         no_copy = true,
         field = 'item_limit',
         field = 'item_limit',
         type = 'Integer',
         type = 'Integer',
         func = h.proc.number,
         func = m_util.cast.factory.number('item_limit'),
     },
     },
     jewel_radius_html = {
     jewel_radius_html = {
         inherit = false,
         no_copy = true,
         field = 'radius_html',
         field = 'radius_html',
         type = 'Text',
         type = 'Text',
         func = function (tpl_args, frame, value)
         func = function(tpl_args, frame)
             -- Get radius from stats
             -- Get radius from stats
             local radius = tpl_args._stats.local_jewel_effect_base_radius
             local radius = tpl_args._stats.local_jewel_effect_base_radius
             if radius then
             if radius then
                 radius = radius.min
                 radius = radius.min
                 local size = m_game.constants.item.jewel_radius_to_size[radius] or radius
                 local size = m_game.constants.item.jewel_radius_to_size[radius] or radius
                 local color =  radius == 0 and 'mod' or 'value'
                 local color =  radius == 0 and 'mod' or 'value'
                 value = m_util.html.poe_color(color, size)
                 tpl_args.jewel_radius_html = m_util.html.poe_color(color, size)
             end
             end
            return value
         end,
         end,
     },
     },
     incubator_effect = {
     incubator_effect = {
         no_copy = true,
         inherit = false,
         field = 'effect',
         field = 'effect',
         type = 'Text',
         type = 'Text',
         func = nil,
         func = nil,
     },
     },
     drop_areas_html = {
     drop_areas_html = {
         no_copy = true,
         inherit = false,
         field = 'drop_areas_html',
         field = 'drop_areas_html',
         type = 'Text',
         type = 'Text',
         func = function(tpl_args, frame)
         func = function (tpl_args, frame, value)
             if tpl_args.drop_areas_data == nil then
             if tpl_args.drop_areas_data == nil then
                 return
                 return value
             end
             end
           
             if value ~= nil then
             if tpl_args.drop_areas_html ~= nil then
                 return value
                 return
             end
             end
             local areas = {}
           
             for _, data in pairs(tpl_args.drop_areas_data) do
             local areas = {}
                 -- skip legacy maps in the drop html listing
             for _, data in pairs(tpl_args.drop_areas_data) do
                 if not string.match(data['areas.id'], '^Map.*') or string.match(data['areas.id'], '^MapWorlds.*') or string.match(data['areas.id'], '^MapAtziri.*') then
                 -- skip legacy maps in the drop html listing
                     areas[#areas+1] = string.format('[[%s|%s]]', data['areas.main_page'] or data['areas._pageName'], data['areas.main_page'] or data['areas.name'])
                 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'])
             end
                 end
             return table.concat(areas, ' • ')
             end
              
            tpl_args.drop_areas_html = table.concat(areas, ' • ')
         end,
         end,
     },
     },
     release_version = {
     release_version = {
         inherit = false,
         no_copy = true,
         field = 'release_version',
         field = 'release_version',
         type = 'String',
         type = 'String'
        func = nil,
     },
     },
     removal_version = {
     removal_version = {
         inherit = false,
         no_copy = true,
         field = 'removal_version',
         field = 'removal_version',
         type = 'String',
         type = 'String',
        func = nil,
     },
     },
     --
     --
Line 2,014: Line 1,975:
     --  
     --  
     suppress_improper_modifiers_category = {
     suppress_improper_modifiers_category = {
         inherit = false,
         no_copy = true,
         field = nil,
         field = nil,
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('suppress_improper_modifiers_category'),
         default = false,
         default = false,
     },
     },
     upgraded_from_disabled = {
     upgraded_from_disabled = {
         inherit = false,
         no_copy = true,
         field = nil,
         field = nil,
         func = h.proc.boolean,
         func = m_util.cast.factory.boolean('upgraded_from_disabled'),
         default = false,
         default = false,
     },
     },
Line 2,393: Line 2,354:
     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:02, 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:Item 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()

-- 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 = {}

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

-- 
-- Factory
-- 

h.factory = {}

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

-- ----------------------------------------------------------------------------
-- 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
       allow_zero: allow zero values
       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.factory.damage_html(args)
    return function(tpl_args, frame)
        local keys = {
            min = args.key .. '_damage_min',
            max = args.key .. '_damage_max',
        }
        local value = {}
        for ktype, key in pairs(keys) do
            value[ktype] = core.factory.infobox_line{
                parts = {
                    {
                        key = key,
                        color = false,
                        hide_default = 0,
                    }
                }
            }(tpl_args, frame)
        end
        if value.min and value.max then
            local color = args.key 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.key == 'physical' then
                    color = 'mod'
                end
                range_fmt = i18n.fmt.variable_damage_range
            else
                -- Standard damage range
                if args.key == 'physical' then
                    color = 'value'
                end
                range_fmt = i18n.fmt.standard_damage_range
            end
            value = string.format(range_fmt, value.min, value.max)
            if color then
                value = m_util.html.poe_color(color, value)
            end
            tpl_args[args.key .. '_damage_html'] = value
        end
    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

--
-- argument mapping
--
-- format:
-- tpl_args key = {
--   no_copy = true or nil           -- When loading an base item, dont copy this key 
--   property = 'prop',              -- Property associated with this key
--   property_func = function or nil -- Function to unpack the property into a native lua value. 
--                                      If not specified, func is used. 
--                                      If neither is specified, value is copied as string
--   func = function or nil          -- Function to unpack the argument into a native lua value and validate it. 
--                                      If not specified, value will not be set.
--   default = object                -- Default value if the parameter is nil
-- }
core.map = {
    -- special params
    html = {
        no_copy = true,
        field = 'html',
        type = 'Text',
        func = nil,
    },
    html_extra = {
        no_copy = true,
        field = 'html_extra',
        type = 'Text',
        func = nil,
    },
    implicit_stat_text = {
        field = 'implicit_stat_text',
        type = 'Text',
        func = function(tpl_args, frame)
            tpl_args.implicit_stat_text = h.process_mod_stats(tpl_args, {is_implicit=true})
        end,
    },
    explicit_stat_text = {
        field = 'explicit_stat_text',
        type = 'Text',
        func = function(tpl_args, frame)
            tpl_args.explicit_stat_text = h.process_mod_stats(tpl_args, {is_implicit=false})
            
            if tpl_args.is_talisman or tpl_args.is_corrupted then
                if tpl_args.explicit_stat_text == nil or tpl_args.explicit_stat_text == '' then
                    tpl_args.explicit_stat_text = i18n.tooltips.corrupted
                else
                    tpl_args.explicit_stat_text = (tpl_args.explicit_stat_text or '') .. '<br>' .. i18n.tooltips.corrupted
                end
            end
        end,
    },
    stat_text = {
        field = 'stat_text',
        type = 'Text',
        func = function(tpl_args, frame)
            local sep = ''
            if tpl_args.implicit_stat_text and tpl_args.explicit_stat_text then
                sep = string.format('<span class="item-stat-separator -%s"></span>', tpl_args.frame_type)
            end
            local text = (tpl_args.implicit_stat_text or '') .. sep .. (tpl_args.explicit_stat_text or '')
            
            if string.len(text) > 0 then
                tpl_args.stat_text = text
            end
        end,
    },
    class = {
        no_copy = true,
        field = 'class',
        type = 'String',
        func = function (tpl_args, frame)
            tpl_args.class = m_game.constants.item.classes[tpl_args.class_id]['long_upper']
            -- Avoids errors with empty item class names later on
            if tpl_args.class == '' then
                tpl_args.class = nil
            end
        end,
    },
    -- processed in build_item_classes
    class_id = {
        no_copy = true,
        field = 'class_id',
        type = 'String',
        func = function (tpl_args, frame)
            if m_game.constants.item.classes[tpl_args.class_id] == nil then
                error(string.format(i18n.errors.invalid_class_id, tostring(tpl_args.class_id)))
            end
        end
    },
    -- generic
    rarity_id = {
        no_copy = true,
        field = 'rarity_id',
        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
    },
    rarity = {
        no_copy = true,
        field = 'rarity',
        type = 'String',
        func = function(tpl_args, frame)
            tpl_args.rarity = m_game.constants.rarities[tpl_args.rarity_id]['long_upper']
        end
    },
    name = {
        no_copy = true,
        field = 'name',
        type = 'String',
        func = nil,
    },
    size_x = {
        field = 'size_x',
        type = 'Integer',
        func = m_util.cast.factory.number('size_x'),
    },
    size_y = {
        field = 'size_y',
        type = 'Integer',
        func = m_util.cast.factory.number('size_y'),
    },
    drop_rarities_ids = {
        no_copy = true,
        field = 'drop_rarity_ids',
        type = 'List (,) of Text',
        func = function(tpl_args, frame)
            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 = {
        no_copy = true,
        field = nil,
        type = 'List (,) of Text',
        func = function(tpl_args, frame)
            tpl_args.drop_rarities = nil
            if true then return end
            -- Drop rarities only matter for base items.
            if tpl_args.rarity_id ~= 'normal' then
                return
            end
            
            local rarities = {}
            for _, rarity_id in ipairs(tpl_args.drop_rarities_ids) do
                rarities[#rarities+1] = m_game.constants.rarities[rarity_id].full
            end
            tpl_args.drop_rarities = rarities
        end,
    },
    drop_enabled = {
        no_copy = true,
        field = 'drop_enabled',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('drop_enabled'),
        default = true,
    },
    drop_level = {
        no_copy = true,
        field = 'drop_level',
        type = 'Integer',
        func = m_util.cast.factory.number('drop_level'),
    },
    drop_level_maximum = {
        no_copy = true,
        field = 'drop_level_maximum',
        type = 'Integer',
        func = m_util.cast.factory.number('drop_level_maximum'),
    },
    drop_leagues = {
        no_copy = true,
        field = 'drop_leagues',
        type = 'List (,) of String',
        func = m_util.cast.factory.assoc_table('drop_leagues', {tbl=m_game.constants.leagues, errmsg=i18n.errors.invalid_league}),
    },
    drop_areas = {
        no_copy = true,
        field = 'drop_areas',
        type = 'List (,) of String',
        func = function(tpl_args, frame)
            if tpl_args.drop_areas ~= nil then
                tpl_args.drop_areas = m_util.string.split(tpl_args.drop_areas, ',%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=tpl_args.drop_areas,
                    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 tpl_args.drop_areas == nil then
                        tpl_args.drop_areas = {}
                        tpl_args.drop_areas_data = {}
                    end
                    local drop_areas_assoc = {}
                    for _, id in ipairs(tpl_args.drop_areas) do
                        drop_areas_assoc[id] = true
                    end
                    
                    local duplicates = {}
                    
                    for _, row in ipairs(query_data) do
                        if drop_areas_assoc[row['areas.id']] == nil then
                            tpl_args.drop_areas[#tpl_args.drop_areas+1] = row['areas.id']
                            tpl_args.drop_areas_data[#tpl_args.drop_areas_data+1] = row
                        else
                            duplicates[#duplicates+1] = row['areas.id']
                        end
                    end
                    
                    if #duplicates > 0 then
                        tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors.duplicate_area_id_from_query, table.concat(duplicates, ', '))
                        tpl_args._flags.duplicate_query_area_ids = true
                    end
                end
            end
        end,
    },
    drop_monsters = {
        no_copy = true,
        field = 'drop_monsters',
        type = 'List (,) of Text',
        func = function (tpl_args, frame) 
            if tpl_args.drop_monsters ~= nil then
                tpl_args.drop_monsters = m_util.string.split(tpl_args.drop_monsters, ',%s*')
            end
        end,
    },
    drop_text = {
        no_copy = true,
        field = 'drop_text',
        type = 'Text',
        func = h.factory.cast_text('drop_text'),
    },
    required_level = {
        field = 'required_level_base',
        type = 'Integer',
        func = m_util.cast.factory.number('required_level'),
        default = 1,
    },
    required_level_final = {
        field = 'required_level',
        type = 'Integer',
        func = function(tpl_args, frame)
            tpl_args.required_level_final = tpl_args.required_level
            if tpl_args.required_level_final < cfg.base_item_required_level_threshold then
                tpl_args.required_level_final = 1
            end
        end,
        default = 1,
    },
    required_dexterity = {
        field = 'required_dexterity',
        type = 'Integer',
        func = m_util.cast.factory.number('required_dexterity'),
        default = 0,
    },
    required_strength = {
        field = 'required_strength',
        type = 'Integer',
        func = m_util.cast.factory.number('required_strength'),
        default = 0,
    },
    required_intelligence = {
        field = 'required_intelligence',
        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
            tpl_args.inventory_icon_id = tpl_args.inventory_icon or tpl_args.name
            tpl_args.inventory_icon = 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 = {
        no_copy = true,
        field = 'alternate_art_inventory_icons',
        type = 'List (,) of String',
        func = function(tpl_args, frame)
            local icons = {}
            if tpl_args.alternate_art_inventory_icons ~= nil then
                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
            tpl_args.alternate_art_inventory_icons = icons
        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,
    },
    help_text = {
        debug_ignore_nil = true,
        field = 'help_text',
        type = 'Text',
        func = h.factory.cast_text('help_text'),
    },
    flavour_text = {
        no_copy = true,
        field = 'flavour_text',
        type = 'Text',
        func = h.factory.cast_text('flavour_text'),
    },
    flavour_text_id = {
        no_copy = true,
        field = 'flavour_text_id',
        type = 'String',
        func = nil,
    },
    tags = {
        field = 'tags',
        type = 'List (,) of String',
        func = m_util.cast.factory.assoc_table('tags', {
            tbl = m_game.constants.tags,
            errmsg = i18n.errors.invalid_tag,
        }),
    },
    metadata_id = {
        no_copy = true,
        field = 'metadata_id',
        type = 'String',
        --type = 'String(unique; size=200)',
        func = function(tpl_args, frame)
            if tpl_args.metadata_id == nil then
                return
            end
            local results = m_cargo.query(
                {'items'},
                {'items._pageName'},
                {
                    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,
    },
    influences = {
        no_copy = true,
        field = 'influences',
        type = 'List (,) of String',
        func = m_util.cast.factory.assoc_table('influences', {
            tbl = m_game.constants.influences,
            errmsg = i18n.errors.invalid_influence,
        }),
    },
    is_fractured = {
        no_copy = true,
        field = 'is_fractured',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('is_fractured'),
        default = false,
    },
    is_synthesised = {
        no_copy = true,
        field = 'is_synthesised',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('is_synthesised'),
        default = false,
    },
    is_veiled = {
        no_copy = true,
        field = 'is_veiled',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('is_veiled'),
        default = false,
    },
    is_replica = {
        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,
        default = false,
    },
    is_fated = {
        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,
        default = false,
    },
    is_prophecy = {
        no_copy = true,
        field = nil,
        type = nil,
        func = function(tpl_args, frame)
            tpl_args._flags.is_prophecy = (tpl_args.metadata_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' or tpl_args.base_item == 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
    },
    is_blight_item = {
        no_copy = true,
        field = nil,
        type = nil,
        func = function(tpl_args, frame)
            tpl_args._flags.is_blight_item = (tpl_args.blight_item_tier ~= nil)
        end
    },
    is_drop_restricted = {
        no_copy  = true,
        field = 'is_drop_restricted',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('is_drop_restricted'),
        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 = {
        func = function(tpl_args, frame)
            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
            
            tpl_args.purchase_costs = 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 = {
        no_copy = true,
        func = function(tpl_args, frame)
            -- 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
        end,
    },
    --
    -- specific section
    --
    
    -- Most item classes
    quality = {
        no_copy = true,
        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)
            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
    is_talisman = {
        field = 'is_talisman',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('is_talisman'),
        default = false,
    },
    
    talisman_tier = {
        field = 'talisman_tier',
        type = 'Integer',
        func = m_util.cast.factory.number('talisman_tier'),
    },
    
    -- flasks
    charges_max = {
        field = 'charges_max',
        type = 'Integer',
        func = m_util.cast.factory.number('charges_max'),
    },
    charges_per_use = {
        field = 'charges_per_use',
        type = 'Integer',
        func = m_util.cast.factory.number('charges_per_use'),
    },
    flask_mana = {
        field = 'mana',
        type = 'Integer',
        func = m_util.cast.factory.number('flask_mana'),
    },
    flask_life = {
        field = 'life',
        type = 'Integer',
        func = m_util.cast.factory.number('flask_life'),
    },
    flask_duration = {
        field = 'duration',
        type = 'Float',
        func = m_util.cast.factory.number('flask_duration'),
    },
    buff_id = {
        field = 'id',
        type = 'String',
        func = nil,
    },
    buff_values = {
        field = 'buff_values',
        type = 'List (,) of Integer',
        func = function(tpl_args, frame)
            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
                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 = {
        field = 'stat_text',
        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',
        func = m_util.cast.factory.number('weapon_range'),
    },
    physical_damage_min = {
        field = 'physical_damage_min',
        type = 'Integer',
        func = m_util.cast.factory.number('physical_damage_min'),
    },
    physical_damage_max = {
        field = 'physical_damage_max',
        type = 'Integer',
        func = m_util.cast.factory.number('physical_damage_max'),
    },
    fire_damage_min = {
        field = 'fire_damage_min',
        type = 'Integer',
        func = m_util.cast.factory.number('fire_damage_min'),
        default = 0,
    },
    fire_damage_max = {
        field = 'fire_damage_max',
        type = 'Integer',
        func = m_util.cast.factory.number('fire_damage_max'),
        default = 0,
    },
    cold_damage_min = {
        field = 'cold_damage_min',
        type = 'Integer',
        func = m_util.cast.factory.number('cold_damage_min'),
        default = 0,
    },
    cold_damage_max = {
        field = 'cold_damage_max',
        type = 'Integer',
        func = m_util.cast.factory.number('cold_damage_max'),
        default = 0,
    },
    lightning_damage_min = {
        field = 'lightning_damage_min',
        type = 'Integer',
        func = m_util.cast.factory.number('lightning_damage_min'),
        default = 0,
    },
    lightning_damage_max = {
        field = 'lightning_damage_max',
        type = 'Integer',
        func = m_util.cast.factory.number('lightning_damage_max'),
        default = 0,
    },
    chaos_damage_min = {
        field = 'chaos_damage_min',
        type = 'Integer',
        func = m_util.cast.factory.number('chaos_damage_min'),
        default = 0,
    },
    chaos_damage_max = {
        field = 'chaos_damage_max',
        type = 'Integer',
        func = m_util.cast.factory.number('chaos_damage_max'),
        default = 0,
    },
    -- armor-type stuff
    armour = {
        field = 'armour',
        type = 'Integer',
        func = m_util.cast.factory.number('armour'),
        default = 0,
    },
    energy_shield = {
        field = 'energy_shield',
        type = 'Integer',
        func = m_util.cast.factory.number('energy_shield'),
        default = 0,
    },
    evasion = {
        field = 'evasion',
        type = 'Integer',
        func = m_util.cast.factory.number('evasion'),
        default = 0,
    },
    -- This is the inherent penality from the armour piece if any
    movement_speed = {
        field = 'movement_speed',
        type = 'Integer',
        func = m_util.cast.factory.number('movement_speed'),
        default = 0,
    },
    -- shields
    block = {
        field = 'block',
        type = 'Integer',
        func = m_util.cast.factory.number('block'),
    },
    -- skill gem stuff
    gem_description = {
        field = 'gem_description',
        type = 'Text',
        func = h.factory.cast_text('gem_description'),
    },
    dexterity_percent = {
        field = 'dexterity_percent',
        type = 'Integer',
        func = m_util.cast.factory.percentage('dexterity_percent'),
    },
    strength_percent = {
        field = 'strength_percent',
        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',
        func = function(tpl_args, frame)
            for _, attr in ipairs(m_game.constants.attribute_order) do
                local val = tpl_args[attr .. '_percent'] 
                if val and val >= 60 then
                    tpl_args['primary_attribute'] = attr
                    return
                end
            end
            tpl_args['primary_attribute'] = 'none'
        end,
    },
    gem_tags = {
        field = 'gem_tags',
        type = 'List (,) of String',
        -- TODO: default rework
        func = function(tpl_args, frame)
            if tpl_args.gem_tags then
                tpl_args.gem_tags = m_util.string.split(tpl_args.gem_tags, ',%s*')
            end
        end,
        default = function (tpl_args, frame) return {} end,
    },
    -- 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)
            if tpl_args.support_gem_letter == nil then
                return
            end
        
            -- TODO replace this with a loop possibly
            local css_map = {
                strength = 'red',
                intelligence = 'blue',
                dexterity = 'green',
            }
            local id
            for k, v in pairs(css_map) do
                k = string.format('%s_percent', k)
                if tpl_args[k] and tpl_args[k] > 50 then
                    id = v
                    break
                end
            end
            
            if id ~= nil then
                local container = mw.html.create('span')
                container
                    :attr('class', string.format('support-gem-id-%s', id))
                    :wikitext(tpl_args.support_gem_letter)
                    :done()
                tpl_args.support_gem_letter_html = tostring(container)
            end
        end,
    },
    --
    -- Maps
    --
    map_tier = {
        field = 'tier',
        type = 'Integer',
        func = m_util.cast.factory.number('map_tier'),
    },
    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 = m_util.cast.factory.number('map_area_level'),
    },
    unique_map_guild_character = {
        field = 'unique_guild_character',
        type = 'String(size=1)',
        func_copy = function(tpl_args, frame)
            tpl_args.map_guild_character = tpl_args.unique_map_guild_character
        end,
        func = nil,
    },
    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 = m_util.cast.factory.number('unique_map_area_level'),
        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)
            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',
        func = m_util.cast.factory.number('atlas_x'),
    },
    atlas_y = {
        field = 'y',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_y'),
    },
    atlas_region_id = {
        field = 'region_id',
        type = 'String',
        func = nil,
    },
    atlas_region_minimum = {
        field = 'region_minimum',
        type = 'Integer',
        func = m_util.cast.factory.number('atlas_region_minimum'),
    },
    atlas_x0 = {
        field = 'x0',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_x0'),
    },
    atlas_x1 = {
        field = 'x1',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_x1'),
    },
    atlas_x2 = {
        field = 'x2',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_x2'),
    },
    atlas_x3 = {
        field = 'x3',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_x3'),
    },
    atlas_x4 = {
        field = 'x4',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_x4'),
    },
    atlas_y0 = {
        field = 'y0',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_0'),
    },
    atlas_y1 = {
        field = 'y1',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_y1'),
    },
    atlas_y2 = {
        field = 'y2',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_y2'),
    },
    atlas_y3 = {
        field = 'y3',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_y3'),
    },
    atlas_y4 = {
        field = 'y4',
        type = 'Float',
        func = m_util.cast.factory.number('atlas_y4'),
    },
    atlas_map_tier0 = {
        field = 'map_tier0',
        type = 'Integer',
        func = m_util.cast.factory.number('atlas_map_tier0'),
    },
    atlas_map_tier1 = {
        field = 'map_tier1',
        type = 'Integer',
        func = m_util.cast.factory.number('atlas_map_tier1'),
    },
    atlas_map_tier2 = {
        field = 'map_tier2',
        type = 'Integer',
        func = m_util.cast.factory.number('atlas_map_tier2'),
    },
    atlas_map_tier3 = {
        field = 'map_tier3',
        type = 'Integer',
        func = m_util.cast.factory.number('atlas_map_tier3'),
    },
    atlas_map_tier4 = {
        field = 'map_tier4',
        type = 'Integer',
        func = m_util.cast.factory.number('atlas_map_tier4'),
    },
    atlas_connections = {
        field = nil,
        type = nil,
        func = function(tpl_args, frame)
            tpl_args.atlas_connections = {}
            
            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
                    
                    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,
    },
    --
    -- Currency-like items
    --
    stack_size = {
        field = 'stack_size',
        type = 'Integer',
        func = m_util.cast.factory.number('stack_size'),
    },
    stack_size_currency_tab = {
        field = 'stack_size_currency_tab',
        type = 'Integer',
        func = m_util.cast.factory.number('stack_size_currency_tab'),
    },
    description = {
        field = 'description',
        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',
        func = m_util.cast.factory.number('essence_level_restriction'),
    },
    essence_level = {
        field = 'level',
        type = 'Integer',
        func = m_util.cast.factory.number('essence_level'),
    },
    essence_type = {
        field = 'type',
        type = 'Integer',
        func = m_util.cast.factory.number('essence_type'),
    },
    essence_category = {
        field = 'category',
        type = 'String',
        func = nil,
    },
    -- blight crafting items (i.e. oils)
    blight_item_tier = {
        field = 'tier',
        type = 'Integer',
        func = m_util.cast.factory.number('blight_item_tier'),
    },
    -- harvest seeds
    seed_type_id = {
        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',
        func = m_util.cast.factory.number('seed_tier'),
    },
    seed_growth_cycles = {
        field = 'growth_cycles',
        type = 'Integer',
        func = m_util.cast.factory.number('seed_growth_cycles'),
    },
    seed_required_nearby_seed_tier = {
        field = 'required_nearby_seed_tier',
        type = 'Integer',
        func = m_util.cast.factory.number('seed_required_nearby_seed_tier'),
    },
    seed_required_nearby_seed_amount = {
        field = 'required_nearby_seed_amount',
        type = 'Integer',
        func = m_util.cast.factory.number('seed_required_nearby_seed_amount'),
    },
    seed_consumed_wild_lifeforce_percentage = {
        field = 'consumed_wild_lifeforce_percentage',
        type = 'Integer',
        func = m_util.cast.factory.number('seed_consumed_wild_lifeforce_percentage'),
        default = 0,
    },
    seed_consumed_vivid_lifeforce_percentage = {
        field = 'consumed_vivid_lifeforce_percentage',
        type = 'Integer',
        func = m_util.cast.factory.number('seed_consumed_vivid_lifeforce_percentage'),
        default = 0,
    },
    seed_consumed_primal_lifeforce_percentage = {
        field = 'consumed_primal_lifeforce_percentage',
        type = 'Integer',
        func = m_util.cast.factory.number('seed_consumed_primal_lifeforce_percentage'),
        default = 0,
    },
    seed_granted_craft_option_ids = {
        field = 'granted_craft_option_ids',
        type = 'List (,) of String',
        func = m_util.cast.factory.number('seed_grandted_craft_option_ids'),
        default = 0,
    },
    --
    -- harvest planet boosters
    --
    plant_booster_radius = {
        field = 'radius',
        type = 'Integer',
        func = m_util.cast.factory.number('plant_booster_radius'),
    },
    plant_booster_lifeforce = {
        field = 'lifeforce',
        type = 'Integer',
        func = m_util.cast.factory.number('plant_booster_lifeforce'),
    },
    plant_booster_additional_crafting_options = {
        field = 'additional_crafting_options',
        type = 'Integer',
        func = m_util.cast.factory.number('plant_booster_additional_crafting_options'),
    },
    plant_booster_extra_chances = {
        field = 'extra_chances',
        type = 'Integer',
        func = m_util.cast.factory.number('plant_booster_extra_chances'),
    },
    --
    -- Heist properties
    --
    heist_required_job_id = {
        field = 'required_job_id',
        type = 'String',
        func = h.factory.cast_text('heist_required_job_id'),
    },
    heist_required_job_level = {
        field = 'required_job_level',
        type = 'Integer',
        func = m_util.cast.factory.number('heist_required_job_level'),
    },
    heist_data = {
        func = function (tpl_args, frame)
            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
        end,
    },
    --
    -- hideout doodads (HideoutDoodads.dat)
    --
    is_master_doodad = {
        field = 'is_master_doodad',
        type = 'Boolean',
        func = m_util.cast.factory.boolean('is_master_doodad'),
    },
    master = {
        field = 'master',
        type = 'String',
        -- todo validate against list of master names
        func = m_util.cast.factory.table('master', {key='full', tbl=m_game.constants.masters}),
    },
    master_level_requirement = {
        field = 'level_requirement',
        type = 'Integer',
        func = m_util.cast.factory.number('master_level_requirement'),
    },
    master_favour_cost = {
        field = 'favour_cost',
        type = 'Integer',
        func = m_util.cast.factory.number('master_favour_cost'),
    },
    variation_count = {
        field = 'variation_count',
        type = 'Integer',
        func = m_util.cast.factory.number('variation_count'),
    },
    -- Propehcy
    prophecy_id = {
        field = 'prophecy_id',
        type = 'String',
        func = nil,
    },
    prediction_text = {
        field = 'prediction_text',
        type = 'Text',
        func = h.factory.cast_text('prediction_text'),
    },
    seal_cost = {
        field = 'seal_cost',
        type = 'Integer',
        func = m_util.cast.factory.number('seal_cost'),
    },
    prophecy_reward = {
        field = 'reward',
        type = 'Text',
        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,
    },
    -- ------------------------------------------------------------------------
    -- derived stats
    -- ------------------------------------------------------------------------
    
    -- For rarity != normal, rarity already verified
    base_item = {
        no_copy = true,
        field = 'base_item',
        type = 'String',
        func = function(tpl_args, frame)
            tpl_args.base_item = tpl_args.base_item_data['items.name']
        end,
    },
    base_item_id = {
        no_copy = true,
        field = 'base_item_id',
        type = 'String',
        func = function(tpl_args, frame)
            tpl_args.base_item_id = tpl_args.base_item_data['items.metadata_id']
        end,
    },
    base_item_page = {
        no_copy = true,
        field = 'base_item_page',
        type = 'Page',
        func = function(tpl_args, frame)
            tpl_args.base_item_page = tpl_args.base_item_data['items._pageName']
        end,
    },
    name_list = {
        no_copy = true,
        field = 'name_list',
        type = 'List (�) of String',
        func = function(tpl_args, frame)
            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 = {
        no_copy = true,
        field = 'frame_type',
        type = 'String',
        property = nil,
        func = function(tpl_args, frame)
            if tpl_args.frame_type then
                return
            end
            if tpl_args._flags.is_prophecy then
                tpl_args.frame_type = 'prophecy'
                return
            end
            local var = cfg.class_specifics[tpl_args.class_id]
            if var ~= nil and var.frame_type ~= nil then
                tpl_args.frame_type = var.frame_type
                return
            end
            if tpl_args.is_relic then
                tpl_args.frame_type = 'relic'
                return
            end
            tpl_args.frame_type = tpl_args.rarity_id
        end,
    },
    --
    -- args populated by mod validation
    -- 
    mods = {
        default = function (tpl_args, frame) return {} end,
        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 = {
        no_copy = true,
        field = 'physical_damage_html',
        type = 'Text',
        func = core.factory.damage_html{key='physical'},
    },
    fire_damage_html = {
        no_copy = true,
        field = 'fire_damage_html',
        type = 'Text',
        func = core.factory.damage_html{key='fire'},
    },
    cold_damage_html = {
        no_copy = true,
        field = 'cold_damage_html',
        type = 'Text',
        func = core.factory.damage_html{key='cold'},
    },
    lightning_damage_html = {
        no_copy = true,
        field = 'lightning_damage_html',
        type = 'Text',
        func = core.factory.damage_html{key='lightning'},
    },
    chaos_damage_html = {
        no_copy = true,
        field = 'chaos_damage_html',
        type = 'Text',
        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
            
            dmg = (dmg.min + dmg.max) / 2
            
            tpl_args.damage_avg = dmg
        end,
    },
    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
            if #text > 0 then
                tpl_args.damage_html = table.concat(text, '<br>')
            end
        end,
    },
    item_limit = {
        no_copy = true,
        field = 'item_limit',
        type = 'Integer',
        func = m_util.cast.factory.number('item_limit'),
    },
    jewel_radius_html = {
        no_copy = true,
        field = 'radius_html',
        type = 'Text',
        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,
    },
    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',
    },
    --
    -- args governing use of the template itself
    -- 
    suppress_improper_modifiers_category = {
        no_copy = true,
        field = nil,
        func = m_util.cast.factory.boolean('suppress_improper_modifiers_category'),
        default = false,
    },
    upgraded_from_disabled = {
        no_copy = true,
        field = nil,
        func = m_util.cast.factory.boolean('upgraded_from_disabled'),
        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