Module:Item2

From Path of Exile Wiki
Revision as of 16:24, 30 September 2015 by >OmegaK2 (Item module rework beta)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]


This module is used on 12,000+ pages.

To avoid major disruption and server load, do not make unnecessary edits to this module. Test changes to this module first using its /sandbox and /testcases subpages . All of the changes can then be applied to this module in a single edit.

Consider discussing changes on the talk page or on Discord before implementing them.

The item module provides functionality for various item-related templates.

Overview

This module is responsible for creating item boxes, various item lists, item links and other item-related tasks. In the process a lot of the input data is verified and also added as semantic property to pages; as such, any templates deriving from this module should not be used on user pages other then for temporary testing purposes.

This template is also backed by an export script in PyPoE which can be used to export item data from the game files which then can be used on the wiki. Use the export when possible,

Item templates

Module:Item2

All templates defined in Module:Item2:

Module:Item table

All templates defined in Module:Item table:

Module:Item link

All templates defined in Module:Item link:

Module:Item acquisition

de:Modul:Item2 ru:Модуль:Item2

-- SMW reworked item module

-- TODO: 
-- included mods and base items should override the internal values and then set them as semantic properties so items can be queried for 
--  > subobjects/property in display code
-- drop location (+drop difficulty) support
-- divinatation card support

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

local p = {}
local v = {}
local g_frame, g_args

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

local h = {}
function h.new_color(label, text)
    if text == nil or text == '' then
        return nil
    end
    return tostring(mw.html.create('span')
        :attr('class', 'text-' .. label)
        :wikitext(text))
end

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

local core = {}

function core.validate_table(args)
    -- args: Table containing:
    --  arg: value of the argument
    --  tbl: table of valid options
    --  key: key or table of key of in tbl
    --  rtrkey: if key is table, return this key instead of the value instead
    if type(args.key) == 'table' then
        for _, item in ipairs(args.tbl) do
            for _, k in ipairs(args.key) do
                if item[k] == args.arg then
                    if args.rtrkey ~= nil then 
                        return item[args.rtrkey]
                    else
                        return k
                    end
                end
            end
        end
    elseif args.key == nil then
        for _, item in ipairs(args.tbl) do
            if item == args.arg then
                return item
            end
        end
    else
        for _, item in ipairs(args.tbl) do
            if item[args.key] == args.arg then
                return args.arg
            end
        end
    end
    return nil
end

function core.validate_mod(args)
    local value = g_args[args.key .. args.i]
    local out = {
        result=nil,
        type=args.key,
        is_text=false,
    }
    
    if value ~= nil then
        --semantic search
        local query = {
            string.format('[[Is mod::%s]]', value),
            '?#=Page',
            '?Is Mod#',
            '?Has mod group#',
            '?Has mod type#',
            '?Has stat text#',
        }
        
        out.result = util.smw.query(query, g_frame)
        
        query = {
            string.format('[[-Has subobject::%s]]', result['?Page']),
            '?Is stat number#',
            '?Has stat id#',
            '?Has minimum stat value#',
            '?Has maximum stat value#',
        }
        out.result.stats = util.smw.query(query, g_frame)
        
        table.insert(g_args._subobjects, {
            ['Is mod number'] = args.i,
            ['Has mod id'] = g_args[value],
        })
        
        -- process and cache stats
        local id, min, max
        for _, stat in ipairs(out.result.stats) do
            id = stat['?Has stat id']
            min = tonumber(stat['?Has minimum stat value'])
            max = tonumber(stat['?Has maximum stat value'])
            
            if g_args._stats[id] == nil then
                g_args._stats[id] = {
                    references = #out+1,
                    min = min,
                    max = max,
                }
            else
                table.insert(g_args._stats[id].references, #out+1)
                g_args._stats[id].min = g_args._stats[id].min + min
                g_args._stats[id].max = g_args._stats[id].max + max
            end
        end
    else
        value = g_args[args.key .. args.i .. '_text']
        if value ~= nil then
            table.insert(g_args._subobjects, {
                ['Is mod number'] = args.i,
                ['Has mod text'] = value,
            })
            
            out.is_text = true
            out.result = value
        end
    end
    
    if out.result ~= nil then
        table.insert(g_args._mods, out)
        return true
    else
        return false
    end
end

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

--
-- function factory
--
core.factory = {}
function core.factory.table_cast(k, args)
    return function ()
        args.arg = g_args[k]
        g_args[k] = core.validate_table(args)
        if g_args[k] == nil then
            error(core.err{key=k})
        end
    end
end


function core.factory.number_cast(k, default)
    return function ()
        g_args[k] = tonumber(g_args[k]) or default
    end
end

function core.factory.percentage(k)
    return function ()
        local v = tonumber(g_args[k])
        
        if v == nil then
            return core.err{key=k}
        end
        
        if v < 0 or v > 100 then
            return core.err{msg=k .. ' must be in range 0-100.'}
        end
        
        g_args[k] = v
    end
end

function core.factory.display_mods(args)
    return function ()
        lines = {}
        
        if args.type == 'quality' then
            table.insert(lines, h.new_color('default', 'Per 1% Quality:'))
        end
        
        for _, modinfo in ipairs(g_args._mods) do
            if modinfo.type == args.type then
                if modinfo.is_text then
                    table.insert(lines, modinfo.result)
                else
                    table.insert(lines, modinfo.result['?Has stat text'])
                end
            end
        end
        
        return lines
    end
end

function core.factory.display_value(args)
    -- TODO: sane defaults for the prepend string "before" & after arguments
    --
    -- args:
    --  args<Array>: Array of arguments to use for displaying
    --  values<Array>: Array of base values to use for displaying; useful if no args are present
    --  options<Array>:
    --   fmt: formatter to use for the value instead of valfmt
    --   allow_zero: allow zero values
    --   before: add this string before the coloured string with colour gray
    --   after: add this string after the coloured string with colour gray
    --   stats_add: Array of stats to add
    --   stats_multiplay: Array of stats to multiply
    local _map = {
        add = function (value, stat_cached) 
            value.min = value.min + stat_cached.min
            value.max = value.max + stat_cached.max
        end,
        multiply = function (value, stat_cached)
            value.min = value.min * stat_cached.min / 100
            value.max = value.max * stat_cached.max / 100
        end,
    }
    
    return function ()
        for k, default in pairs({stats = {}, options = {}}) do
            if args[k] == nil then
                args[k] = default
            end
        end
        
        local values = {}
        if args.values == nil then
            values = {}
            for i, k in ipairs(args.keys) do
                local value = g_args[k]
                if g_args.base_item then
                    value = tonumber(g_args.base_item['?' .. core.map[k].property]) or value
                end
                
                values[i] = value
            end
        else
            values = args.values
        end
        
        local final_values = {}
        for i, base_value in ipairs(values) do
            local value = {min=base_value, max=base_value}
                
            for _, operator in ipairs({'add', 'multiply'}) do
                local st = args.options[i]['stats_' .. operator]
                if st ~= nil then
                    for _, statid in ipairs(st) do
                        if g_args._stats[statid] ~= nil then
                            _map[operator](value, g_args._stats[statid])
                        end
                    end
                end
            end
            
            local prop = args.options[i].property
            
            if prop ~= nil then
                g_args._properties[prop .. ' range minimum'] = value.min
                g_args._properties[prop .. ' range maximum'] = value.max
                g_args._properties[prop .. ' range avarage'] = (value.min + value.max) / 2
            end
            
            if not (value.min == 0 and value.max == 0 and args.options[i].allow_zero == nil) then
                table.insert(final_values, {value=value, index=i})
            end
        end
        
        -- all zeros = dont display and return early
        if #final_values == 0 then
            return nil
        end
        
        local out = {}
        
        if args.before then
            out[#out+1] = h.new_color('default', args.before)
        end
        
        for i, data in ipairs(final_values) do
            local value = data.value
            local base_value = values[data.index]
            local fmt = args.options[data.index] or {}
            
            if base_value ~= value.min or base_value ~= value.max then
                value.color = 'mod'
            else
                value.color = 'value'
            end
            
            if value.min == value.max then
                value.out = string.format(fmt.fmt or '%s', value.min)
            else
                value.out = string.format(string.format("(%s to %s)", fmt.fmt or '%s', fmt.fmt or '%s'), value.min, value.max)
            end
            
            value.out = h.new_color(value.color, value.out)
            
            local before = ''
            if fmt.before ~= nil then
                before = h.new_color('default', fmt.before)
            end
            
            local after = ''
            if fmt.after ~= nil then
                after = h.new_color('default', fmt.after)
            end
            
            out[#out+1] = before .. value.out .. after
        end
        
        if args.after then
            out[#out+1] = h.new_color('default', args.after)
        end
        
        return table.concat(out, ' ')
    end
end

function core.factory.display_value_only(key)
    return function()
        return g_args[key]
    end
end

--
-- argument mapping
--
core.map = {
    -- generic
    class = {
        property = 'Has item class',
        func = core.factory.table_cast('class', {key='full', tbl=game.constants.item.class}),
    },
    rarity = {
        property = 'Has rarity',
        func = core.factory.table_cast('rarity', {key={'full', 'long_lower'}, tbl=game.constants.item.rarity, rtrkey='full'}),
    },
    name = {
        property = 'Has name',
        func = nil,
    },
    size_x = {
        property = 'Has inventory width',
        func = core.factory.number_cast('size_x'),
    },
    size_y = {
        property = 'Has inventory height',
        func = core.factory.number_cast('size_y'),
    },
    drop_level = {
        property = 'Has drop level',
        func = core.factory.number_cast('drop_level'),
    },
    required_level = {
        property = 'Has level requirement',
        func = core.factory.number_cast('required_level'),
    },
    required_dexterity = {
        property = 'Has base dexterity requirement',
        func = core.factory.number_cast('required_dexterity'),
    },
    required_strength = {
        property = 'Has base strength requirement',
        func = core.factory.number_cast('required_strength'),
    },
    required_intelligence = {
        property = 'Has base intelligence requirement',
        func = core.factory.number_cast('required_intelligence'),
    },
    inventory_icon = {
        property = 'Has inventory icon',
        func = function ()
            -- TODO readd support if needed.
            g_args['inventory_icon'] = string.format('File:%s inventory icon.png', g_args.name) 
        end,
    },
    help_text = {
        property = 'Has help text',
        func = nil,
    },
    flavour_text = {
        property = 'Has flavour text',
        func = nil,
    },
    -- For rarity != normal, rarity already verified
    base_item = {
        property = 'Has base item',
        func = function ()
            if g_args['rarity'] ~= 'Normal' then
                local query = {
                    string.format('[[Has name::%s]]', g_args['base_item']),
                    string.format('[[Has item class::%s]]', g_args['class']),
                    '[[Has rarity::normal]]',
                }
                
                for _, k in ipairs(g_args['total_args']) do
                    query[#query+1] = string.format('?%s#', core.map[k].property)
                end
                
                local result = util.smw.query(query, g_frame)
                
                if #result > 1 then
                    error(core.err{msg='More then one result found for the specified base item.'})
                    -- TODO be more explicit in the error?
                end
                
                g_args['base_item'] = result
                -- TODO Semantic search - verify ?
            elseif g_args['base_item'] ~= nil then
                error(core.err{msg='Base item requries item with rarity above normal.'})
            end
        end,
    },
    --
    -- specific section
    --
    
    -- flasks
    charges_max = {
        property = 'Has base maximum flask charges',
        func = core.factory.number_cast('charges_max'),
    },
    
    charges_per_use = {
        property = 'Has base flask charges per use',
        func = core.factory.number_cast('changes_per_use'),
    },
    
    flask_mana = {
        property = 'Has base flask mana recovery',
        func = core.factory.number_cast('flask_mana'),
    },
    
    flask_life = {
        property = 'Has base flask life recovery',
        func = core.factory.number_cast('flask_life'),
    },
    
    flask_duration = {
        property = 'Has base flask duration',
        func = core.factory.number_cast('flask_duration'),
    },
    
    -- weapons & active skills
    critical_strike_chance = {
        property = 'Has base critical strike chance',
        func = core.factory.number_cast('critical_strike_chance'),
    },
    -- weapons
    attack_speed = {
        property = 'Has base attack speed',
        func = core.factory.number_cast('attack_speed'),
    },
    damage_min = {
        property = 'Has base minimum damage',
        func = core.factory.number_cast('damage_min'),
    },
    damage_max = {
        property = 'Has base maximum damage',
        func = core.factory.number_cast('damage_max'),
    },
    range = {
        property = 'Has base weapon range',
        func = core.factory.number_cast('range'),
    },
    -- armor-type stuff
    armour = {
        property = 'Has base armour',
        func = core.factory.number_cast('armour', 0),
    },
    energy_shield = {
        property = 'Has base energy shield',
        func = core.factory.number_cast('energy shield', 0),
    },
    evasion = {
        property = 'Has base evasion',
        func = core.factory.number_cast('evasion', 0),
    },
    -- shields
    block = {
        property = 'Has base block chance',
        func = core.factory.number_cast('block'),
    },
    -- skill gem stuff
    gem_description = {
        property = 'Has description',
        func = nil,
    },
    dex_percent = {
        property = 'Has dexterity percentage',
        func = core.factory.percentage('dex_percent'),
    },
    str_percent = {
        property = 'Has strength percentage',
        func = core.factory.percentage('str_percent'),
    },
    int_percent = {
        property = 'Has intelligence percentage',
        func = core.factory.percentage('int_percent'),
    },
    gem_tags = {
        property = 'Has gem tag',
        func = function ()
            local tags = {}
            
            if g_args.gem_tags ~= nil then
                for s in mw.text.gsplit(g_args.gem_tags, ', ') do
                    local r = core.validate_table{arg=s, tbl=game.constants.item.gem_tags, key='full'}
                    if r == nil then
                        error(core.err{msg=s .. ' is not a valid gem tag.'})
                    end
                    
                    tags[#tags+1] = r
                end
            end
            
            g_args.gem_tags = tags
        end,
    },
    experience = {
        property = 'Has maximum experience',
        func = core.factory.percentage('experience'),
    },
    -- Active gems only
    gem_icon = {
        property = 'Has skill gem icon',
        func = function ()
            -- TODO readd support if needed.
            g_args['gem_icon'] = string.format('File:%s skill icon.png', g_args.name) 
        end,
    },
    mana_reserved = {
        property = 'Has mana reserved',
        func = core.factory.number_cast('mana_reserved'),
    },
    cooldown_time = {
        property = 'Has cooldown time',
        func = core.factory.number_cast('cooldown_time'),
    },
    cast_time = {
        property = 'Has cast time',
        func = core.factory.number_cast('cast_time'),
    },
    damage_effectiveness = {
        property = 'Has damage effectiveness',
        func = core.factory.number_cast('damage_effectiveness', 100),
    },
    mana_cost = {
        property = 'Has mana cost',
        func = core.factory.number_cast('mana_cost'),
    },
    souls_normal = {
        property = 'Has normal souls requirement',
        func = core.factory.number_cast('souls_normal'),
    },
    souls_cruel = {
        property = 'Has cruel souls requirement',
        func = core.factory.number_cast('souls_cruel'),
    },
    souls_merciless = {
        property = 'Has merciless souls requirement',
        func = core.factory.number_cast('souls_merciless'),
    },
    stored_uses = {
        property = 'Has stored uses',
        func = core.factory.number_cast('stored_uses'),
    },
    -- Support gems only
    mana_cost_multiplier = {
        property = 'Has mana cost multiplier',
        func = core.factory.number_cast('mana_cost_multiplier'),
    },
    support_gem_letter = {
        property = 'Has support gem letter',
        func = nil,
    },
    -- Maps
    map_level = {
        property = 'Has map level',
        func = core.factory.number_cast('map_level'),
    },
    -- TODO: aren't these given by mods ... removeable?
    map_item_quantity = {
        property = 'Has base map item quantity',
        func = core.factory.number_cast('map_item_quantity'),
    },
    map_item_rarity = {
        property = 'Has base map item rarity',
        func = core.factory.number_cast('map_item_rarity'),
    },
    map_pack_size = {
        property = 'Has base map pack size',
        func = core.factory.number_cast('map_pack_size'),
    },
    -- Misc
    stack_size = {
        property = 'Has stack size',
        func = core.factory.number_cast('stack_size'),
    },
}

-- base item is default, but will be validated later
core.default_args = {'class', 'rarity', 'name', 'size_x', 'size_y', 'drop_level', 'required_level', 'required_dexterity', 'required_intelligence', 'required_strength', 'inventory_icon', 'flavour_text', 'help_text'}
core.class_groups = {
    flasks = {
        keys = xtable:new({'Life Flasks', 'Mana Flasks', 'Hybrid Flasks', 'Utility Flasks', 'Critical Utility Flasks'}),
        args = {'flask_duration', 'charges_max', 'charges_per_use'},
    },
    weapons = {
        keys = xtable:new({'Claws', 'Daggers', 'Wands', 'One Hand Swords', 'Thrusting One Hand Swords', 'One Hand Axes', 'One Hand Maces', 'Bows', 'Staves', 'Two Hand Swords', 'Two Hand Axes', 'Two Hand Maces', 'Sceptres'}),
        args = {'critical_strike_chance', 'attack_speed', 'damage_min', 'damage_max', 'range'},
    },
    gems = {
        keys = xtable:new({'Active Skill Gems', 'Support Skill Gems'}),
        args = {'gem_description', 'dex_percent', 'str_percent', 'int_percent', 'gem_tags', 'experience'},
    },
    armor = {
        keys = xtable:new({'Gloves', 'Boots', 'Body Armours', 'Helmets', 'Shields'}),
        args = {'armour', 'energy_shield', 'evasion'},
    },
    stackable = {
        keys = xtable:new({'Currency', 'Stackable Currency', 'Hideout Doodads', 'Microtransactions', 'Divination Card'}),
        args = {'stack_size'},
    },
}

core.class_specifics = {
    ['Life Flasks'] = {
        args = {'flask_life'},
    },
    ['Mana Flasks'] = {
        args = {'flask_mana'},
    },
    ['Hybrid Flasks'] = {
        args = {'flask_life', 'flask_mana'},
    },
    ['Active Skill Gems'] = {
        args = {'gem_icon', 'mana_cost', 'mana_reserved', 'souls_normal', 'souls_cruel', 'souls_merciless', 'cooldown_time', 'cast_time', 'critical_strike_chance', 'damage_effectiveness', 'stored_uses'},
        defaults = {
            help_text = 'Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.',
            size_x = 1,
            size_y = 1,
        },
    },
    ['Support Skill Gems'] = {
        args = {'mana_cost_multiplier', 'support_gem_letter'},
        defaults = {
            help_text = 'This is a Support Gem. It does not grant a bonus to your character, but skills in sockets connected to it. Place into an item socket connected to a socket contianing the Active Skill Gem you wish to agument. Right click to remove from a socket.',
            size_x = 1,
            size_y = 1,
        },
    },
    ['Shields'] = {
        args = {'block'},
    },
    ['Hideout Doodads'] = {
        defaults = {
            help_text = 'Right click on this item then left click on a location on the ground to create the object.',
        },
    },
    ['Jewel'] = {
        defaults = {
            help_text = 'Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.',
        },
    },
}

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

for _, row in pairs(core.class_groups) do
    for _, k in ipairs(row.keys) do
        core.item_classes[k].args:insertT(row.args)
    end
end


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


-- GroupTable -> RowTable -> formatter function 
--
--
core.display_groups = {
    -- Tags, stats, level, etc
    {
        {
            args = {'gem_tags'},
            func = function ()
                local out = {}
                for i, tag in ipairs(g_args.gem_tags) do
                    out[#out+1] = '[[' .. tag .. ']]'
                end
                
                return table.concat(out, ', ')
            end,
        },
        -- TODO: gem level here. Maybe put max level here?
        {
            args = {'mana_cost'},
            func = core.factory.display_value{
                keys={'mana_cost'},
                options = {
                    [1] = {
                        --property = 'Has mana cost',
                        fmt = '%i',
                        before = 'Mana Cost: ',
                    },
                },
            },
        },
        {
            args = {'mana_reserved'},
            func = core.factory.display_value{
                keys={'mana_reserved'},
                options = {
                    [1] = {
                        --property = 'Has mana reserved',
                        fmt = '%i',
                        before = 'Mana Reserved: ',
                    },
                },
            },
        },
        {
            args = {'mana_multiplier'},
            func = core.factory.display_value{
                keys={'mana_multiplier'},
                options = {
                    [1] = {
                        --property = 'Has mana multiplier',
                        fmt = '%i',
                        before = 'Mana Multiplier: ',
                    },
                },
            },
        },
        {
            args = {'souls_normal', 'souls_cruel', 'souls_merciless'},
            func = core.factory.display_value{
                keys={'souls_normal', 'souls_cruel', 'souls_merciless'},
                options = {
                    [1] = {
                        --property = 'Has normal souls requirement',
                        fmt = '%i (N) / ',
                        before = 'Souls per use: ',
                    },
                    [2] = {
                        --property = 'Has cruel souls requirement',
                        fmt = '%i (C) / ',
                    },
                    [3] = {
                        --property = 'Has merciless souls requirement',
                        fmt = '%i (M)',
                    },
                },
            },
        },
        {
            args = {'stored_uses'},
            func = core.factory.display_value{
                keys={'stored_uses'},
                options = {
                    [1] = {
                        --property = 'Has stored uses',
                        fmt = '%i',
                        before = 'Can store ',
                        after = ' Use',
                    },
                },
            },
        },
        {
            args = {'cooldown_time'},
            func = core.factory.display_value{
                keys={'cooldown_time'},
                options = {
                    [1] = {
                        --property = 'Has cooldown time',
                        fmt = '%.2f sec',
                        before = 'Cooldown Time: ',
                    },
                },
            },
        },
        {
            args = {'cast_time'},
            func = core.factory.display_value{
                keys={'cast_time'},
                options = {
                    [1] = {
                        --property = 'Has cast time',
                        fmt = '%.2f sec',
                        before = 'Cast Time: ',
                    },
                },
            },
        },
        {
            args = {'critical_strike_chance'},
            func = core.factory.display_value{
                keys={'critical_strike_chance'},
                options = {
                    [1] = {
                        property = 'Has critical strike chance',
                        fmt = '%.2f%%',
                        before = 'Critical Strike Chance: ',
                        stats_add = {
                            'local_critical_strike_chance',
                        },
                        stats_multiply = {
                            'local_critical_strike_chance_+%',
                        },
                    },
                },
            },
        },
        {
            args = {'damage_effectiveness'},
            func = core.factory.display_value{
                keys={'damage_effectiveness'},
                options = {
                    [1] = {
                        --property = 'Has damage effectiveness',
                        fmt = '%i%%',
                        before = 'Damage Effectiveness: ',
                    },
                },
            },
        },
        -- Weapon only
        {
            args = {'damage_min', 'damage_max'},
            func = core.factory.display_value{
                keys={'damage_min', 'damage_max'},
                options = {
                    [1] = {
                        property = 'Has minimum physical damage',
                        fmt = '%i',
                        stats_add = {
                            'local_minimum_added_physical_damage',
                        },
                        stats_multiply = {
                            'local_physical_damage_+%',
                        },
                    },
                    [2] = {
                        property = 'Has maximum physical damage',
                        fmt = '%i',
                        stats_add = {
                            'local_maximum_added_physical_damage',
                        },
                        stats_multiply = {
                            'local_physical_damage_+%',
                        },
                    },
                },
            },       
        },
        {
            args = nil,
            func = core.factory.display_value{
                before='Elemental Damage: ',
                values={0, 0, 0, 0, 0, 0},
                options = {
                    [1] = {
                        property = 'Has minimum fire damage',
                        fmt = '%i',
                        before = '<span class="text-fire">',
                        stats_add = {
                            'local_minimum_added_fire_damage',
                        },
                    },
                    [2] = {
                        property = 'Has maximum fire damage',
                        fmt = '%i',
                        after = '</span>',
                        stats_add = {
                            'local_maximum_added_fire_damage',
                        },
                    },
                    [3] = {
                        property = 'Has minimum cold damage',
                        fmt = '%i',
                        before = '<span class="text-cold">',
                        stats_add = {
                            'local_minimum_added_cold_damage',
                        },
                    },
                    [4] = {
                        property = 'Has maximum cold damage',
                        fmt = '%i',
                        after = '</span>',
                        stats_add = {
                            'local_maximum_added_cold_damage',
                        },
                    },
                    [5] = {
                        property = 'Has minimum lightning damage',
                        fmt = '%i',
                        before = '<span class="text-lightning">',
                        stats_add = {
                            'local_minimum_added_lightning_damage',
                        },
                    },
                    [6] = {
                        property = 'Has maximum lightning damage',
                        fmt = '%i',
                        after = '</span>',
                        stats_add = {
                            'local_maximum_added_lightning_damage',
                        },
                    },
                },
            },       
        },
        {
            args = nil,
            func = core.factory.display_value{
                before='Chaos Damage: ',
                values={0,0},
                options = {
                    [1] = {
                        property = 'Has minimum chaos damage',
                        fmt = '%i',
                        stats_add = {
                            'local_minimum_added_chaos_damage',
                        },
                    },
                    [2] = {
                        property = 'Has maximum chaos damage',
                        fmt='%i',
                        stats_add = {
                            'local_maximum_added_chaos_damage',
                        },
                    },
                },
            },       
        },
        -- Map only
        {
            args = {'map_level'},
            func = core.factory.display_value{
                keys={'map_level'},
                options = {
                    [1] = {
                        --property = 'Has map level',
                        fmt = '%i',
                        before = 'Map Level: ',
                    },
                },
            },
        },
        {
            args = {'map_item_quantity'},
            func = core.factory.display_value{
                keys={'map_item_quantity'},
                options = {
                    [1] = {
                        property = 'Has map item quantity',
                        fmt = '%i',
                        before = 'Item Quantity: ',
                        stats_add = {
                            'map_item_drop_quantity_+%',
                        },
                    },
                },
            },
        },
        {
            args = {'map_item_rarity'},
            func = core.factory.display_value{
                keys={'map_item_rarity'},
                options = {
                    [1] = {
                        property = 'Has map item rarity',
                        fmt = '%i',
                        before = 'Item Rarity: ',
                        stats_add = {
                            'map_item_drop_rarity_+%',
                        },
                    },
                },
            },
        },
        {
            args = {'map_pack_size'},
            func = core.factory.display_value{
                keys={'map_pack_size'},
                options = {
                    [1] = {
                        property = 'Has map pack size',
                        fmt = '%i',
                        before = 'Pack Size: ',
                        stats_add = {
                            'map_pack_size_+%',
                        },
                    },
                },
            },
        },
        -- Jewel Only
        {
            args = {'limit'},
            func = core.factory.display_value{
                keys={'limit'},
                options = {
                    [1] = {
                        --property = 'Has limit',
                        fmt = '%i',
                        before = 'Limit: ',
                    },
                },
            },
        },
        {
            args = {'jewel_radius'},
            func = core.factory.display_value{
                keys={'jewel_radius'},
                options = {
                    [1] = {
                        --property = 'Has jewel radius',
                        fmt = '%i',
                        before = 'Radius: ',
                    },
                },
            },
        },
        -- Flask only
        {
            args = {'flask_mana', 'flask_duration'},
            --func = core.factory.display_flask('flask_mana'),
            func = core.factory.display_value{
                keys = {'flask_mana', 'flask_duration'},
                options = {
                    [1] = {
                        property = 'Has flask mana recovery',
                        fmt = '%i Mana',
                        before = 'Recovers',
                        stats_add = {
                            'local_flask_mana_to_recover',
                        },
                        stats_multiply = {
                            'local_flask_mana_to_recover_+%',
                        },
                    },
                    [2] = {
                        property = 'Has flask duration',
                        fmt = '%.2f',
                        before = ' over ',
                        after = ' seconds',
                        stats_multiply = {
                            'local_flask_recovery_speed_+%',
                        },
                    },
                }
            },
        },
        {
            args = {'flask_life', 'flask_duration'},
            func = core.factory.display_value{
                keys = {'flask_life', 'flask_duration'},
                options = {
                    [1] = {
                        property = 'Has flask mana recovery',
                        fmt = '%i Life',
                        before = 'Recovers',
                        stats_add = {
                            'local_flask_life_to_recover',
                        },
                        stats_multiply = {
                            'local_flask_life_to_recover_+%',
                        },
                    },
                    [2] = {
                        property = 'Has flask duration',
                        fmt = '%.2f',
                        before = ' over ',
                        after = ' seconds',
                        stats_multiply = {
                            'local_flask_recovery_speed_+%',
                        },
                    },
                }
            },
        },
        {
            -- don't display for mana/life flasks
            args = function ()
                for _, k in ipairs({'flask_life', 'flask_mana'}) do
                    if g_args[k] ~= nil then
                        return false
                    end
                end
                
                return g_args['flask_duration'] ~= nil
            end,
            func = core.factory.display_value{
                keys = {'flask_duration'},
                options = {
                    [1] = {
                        property = 'Has flask duration',
                        before = 'Lasts ',
                        after = ' Seconds',
                        fmt = '%.2f',
                        stats_multiply = {
                            'local_flask_recovery_speed_+%',
                        },
                    },
                },
            },
        },
        {
            args = {'charges_per_use', 'charges_cap'},
            func = core.factory.display_value{
                keys = {'charges_per_use', 'charges_cap'},
                options = {
                    [1] = {
                        property = 'Has flask charges per use',
                        before = 'Consumes ',
                        fmt = '%i',
                        stats_multiply = {
                            'local_charges_used_+%',
                        },
                    },
                    [2] = {
                        property = 'Has flask maximum charges',
                        before = ' of ',
                        after = ' Charges on use',
                        fmt = '%i',
                        stats_add = {
                            'local_extra_max_charges',
                        },
                        stats_multiply = {
                            'local_max_charges_+%',
                            'local_charges_added_+%',
                        },
                    },
                },
            },
        },
        -- armor
        {
            args = {'block'},
            func = core.factory.display_value{
                keys={'block'},
                options = {
                    [1] = {
                        property = 'Has block',
                        fmt = '%i',
                        stats_add = {
                            'local_additional_block_chance_%',
                        },
                    },
                },
            },
        },
        {
            args = {'armour'},
            func = core.factory.display_value{
                keys={'armour'},
                options = {
                    [1] = {
                        property = 'Has armour',
                        fmt = '%i',
                        stats_add = {
                            'local_base_physical_damage_reduction_rating',
                        },
                        stats_multiply = {
                            'local_physical_damage_reduction_rating_+%',
                            'local_armour_and_energy_shield_+%',
                            'local_armour_and_evasion_+%',
                            'local_armour_and_evasion_and_energy_shield_+%',
                        },
                    },
                },
            },
        },
        {
            args = {'evasion'},
            func = core.factory.display_value{
                keys={'evasion'},
                options = {
                    [1] = {
                        property = 'Has evasion',
                        fmt = '%i',
                        stats_add = {
                            'local_base_evasion_rating',
                        },
                        stats_multiply = {
                            'local_evasion_rating_+%',
                            'local_evasion_and_energy_shield_+%',
                            'local_armour_and_evasion_+%',
                            'local_armour_and_evasion_and_energy_shield_+%',
                        },
                    },
                },
            },
        },
        {
            args = {'energy_shield'},
            func = core.factory.display_value{
                keys={'energy_shield'},
                options = {
                    [1] = {
                        property = 'Has energy shield',
                        fmt = '%i',
                        stats_add = {
                            'local_energy_shield'
                        },
                        stats_multiply = {
                            'local_energy_shield_+%',
                            'local_armour_and_energy_shield_+%',
                            'local_evasion_and_energy_shield_+%',
                            'local_armour_and_evasion_and_energy_shield_+%',
                        },
                    },
                },
            },
        },
        -- Misc
        {
            args = {'stack_size'},
            func = core.factory.display_value{
                keys={'stack_size'},
                options = {
                    [1] = {
                        --property = 'Has stack size',
                        fmt = '%i',
                        before = 'Stack Size: ',
                    },
                },
            },
        },
    },
    -- Requirements
    {
        -- Instead of item level, show drop level if any
        {
            args = {'drop_level'},
            func = core.factory.display_value{
                keys={'drop_level'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Drop Level: ',
                    },
                },
            },
        },
        {
            args = nil,
            func = function ()
                local requirements = {}
                local attr_label
                local use_short_label
                
                if g_args.required_level then
                    table.insert( requirements, 'Level ' .. tostring( h.new_color('value', g_args.required_level) ) )
                end
                
                for _, attr in ipairs(game.constants.attributes) do
                    local val = g_args['required_' .. attr['long_lower']]
                    if val then
                        use_short_label = false or g_args.required_level
                        for _, attr2 in ipairs(game.constants.attributes) do
                            if attr ~= attr2 then
                                use_short_label = use_short_label or args[attr2['long_lower']]
                            end
                        end
                        if use_short_label then
                            attr_label = attr['short_upper']
                        else
                            attr_label = attr['long_upper']
                        end
                        table.insert( requirements, h.new_color('value', val) .. ' ' .. attr_label )
                    end
                end
                
                -- return early
                if #requirements == 0 then
                    return
                end
                
                return h.new_color('default', 'Requires ') .. table.concat(requirements, ', ')
            end,
        },
    },
    -- Gem description
    {
        css_class = 'textwrap text-gemdesc',
        {
            args = {'gem_description'},
            func = core.factory.display_value_only('gem_description'),
        },
    },
    -- Quality Stats
    {
        css_class = 'text-mod',
        func = core.factory.display_mods{type='quality'},
    },
    -- Implicit Stats
    {
        css_class = 'text-mod',
        func = core.factory.display_mods{type='implicit'},
    },
    -- Stats
    {
        css_class = 'text-mod',
        func = core.factory.display_mods{type='mod'},
    },
    -- Experience
    {
        {
            args = {'experience'},
            func = core.factory.display_value{
                keys={'experience'},
                options = {
                    [1] = {
                        fmt = '%i',
                    },
                },
            },
        },
    },
    -- Flavour Text
    {
        css_class = 'textwrap text-flavour',
        {
            args = {'flavour_text'},
            func = core.factory.display_value_only('flavour_text'),
        },
    },
    -- Help text
    {
        css_class = 'textwrap text-help',
        {
            args = {'help_text'},
            func = core.factory.display_value_only('help_text'),
        },
    },
    -- Cost (i.e. vendor costs)
    {
    },
}

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

--
-- Frame args:
-- class
-- rarity

--[[ =p.itembox{
    name='Vaal Ground Slam', 
    class='Active Skill Gems', 
    rarity='Normal', 
    gem_tags='Vaal, Spell, Duration',
    str_percent='100',
    required_level='34',
    souls_normal='48',
    souls_cruel='72',
    souls_merciless='96',
    stored_uses='1',
    cast_time='0.85',
    gem_description='Discharges Endurance Charges, making the character unable to die for a short time, proportional to how many endurance charges were expended.',
    quality1_text='2% Increased cast speed',
    mod1_text='Base Duration is 0.4 seconds',
    mod2_text='Additional x seconds Base Duration per Endurance Charge',
}
]]--
function p.itembox (frame)
    --
    -- Args/Frame
    --
    
    g_args = getArgs(frame, {
        parentFirst = true
    })
    if frame == nil or type(frame) == 'table' then
        frame = mw.getCurrentFrame()
    end
    
    g_frame = frame
    
    --
    -- Shared args
    --
    
    g_args._total_args = {}
    local properties = {}
    
    -- Must validate some argument early. It is required for future things
    for _, k in ipairs(core.default_args) do
        table.insert(g_args._total_args, k)
        local data = core.map[k]
        if data.func ~= nil then
            local status, err = pcall(data.func)
            -- an error was raised, return the error string instead of the template
            if not status then
                return err
            end
        end
    end
    
    for _, k in ipairs(core.item_classes[g_args.class].args) do
        table.insert(g_args._total_args, k)
        local data = core.map[k]
        if data.func ~= nil then
            local status, err = pcall(data.func)
            -- an error was raised, return the error string instead of the template
            if not status then
                return err
            end
        end
    end
    
    -- set defaults
    
    for k, v in pairs(core.item_classes[g_args.class].defaults) do
        if g_args[k] == nil then
            g_args[k] = v
        end
    end
    
    -- Mods
    
    g_args._mods = {}
    g_args._stats = {}
    g_args._subobjects = {}
    for _, k in ipairs({'implicit', 'mod', 'quality'}) do
        local success = true
        local i = 1
        while success do
            success = core.validate_mod{key=k, i=i}
            i = i + 1
        end
    end
    
    -- Handle extra stats (for gems)
    
    if core.class_groups.gems.keys:contains(g_args.class) then
        local success = true
        local i = 1
        while success do
            local value = {
                id = g_args['stat' .. i .. '_id'],
                min = g_args['stat' .. i .. '_min'],
                max = g_args['stat' .. i .. '_max'],
            }
            
            if not (value.id == nil and value.min == nil and value.max == nil) then
                local onenil = nil
                for _, k in ipairs({'id', 'min', 'max'}) do
                    if value[k] == nil then
                        onenil = k
                        break
                    end
                end

                if onenil ~= nil then
                    error('"' .. id[onenil] .. '" is not set')
                end
                
                value.min = util.number.cast(value.min)
                value.max = util.number.cast(value.max)
                
                properties = {}
                properties['Is stat number'] = i
                properties['Has stat id'] = value.id
                properties['Has minimum stat value'] = util.number.cast(value.min)
                properties['Has maximum stat value'] = util.number.cast(value.max)
                
                g_frame:callParserFunction('#subobject:', properties)
            else
                success = false
            end
        end
    end
    
    -- base_item
    
    core.map.base_item.func()
    table.insert(g_args._total_args, 'base_item')
    
    -- Setting semantic properties Part 1 (base values)
    
    local val
    
    for _, k in ipairs(g_args._total_args) do
        val = g_args[k]
        if val == nil then
        elseif type(val) == 'table' then
            g_frame:callParserFunction('#set:', {
                [core.map[k].property] = table.concat(g_args[k], ';'),
                ['+sep'] = ';',
            })
        else
            properties[core.map[k].property] = g_args[k]
        end
    end
    
    g_frame:callParserFunction('#set:', properties)
    
    -- Subobjects
    for _, properties in ipairs(g_args._subobjects) do
        g_frame:callParserFunction('#subobject:', properties)
    end
    
    --
    -- Validate arguments
    -- 
    
    return v.itembox()
end

function p.new_func (frame)
    --
    -- Args/Frame
    --
    
    g_args = getArgs(frame, {
        parentFirst = true
    })
    if frame == nil or type(frame) == 'table' then
        frame = mw.getCurrentFrame()
    end
end

-- ----------------------------------------------------------------------------
-- Property views..
-- ----------------------------------------------------------------------------

function v.itembox ()
    --todo: remove g_args.frame ?
    local container = v._itembox_core()
    
    for _, k in ipairs({'inventory_icon', 'gem_icon'}) do
        container:wikitext(string.format('[[%s]]', g_args[k]))
    end
    
    return tostring(container) .. v._itembox_categories()
end

function v._itembox_core()
    -- needed later for setting caculated properties
    g_args._properties = {}
    local frame_css = {
        ['Currency'] = 'currency',
        ['Microtransactions'] = 'currency',
        ['Active Skill Gems'] = 'gem',
        ['Support Skill Gems'] = 'gem',
        ['Quest'] = 'quest',
        ['Divination Card'] = 'divicard'
    }
    local container = mw.html.create('div')
        :attr( 'class', 'itembox-' .. ( string.lower(g_args.frame or frame_css[g_args.class] or game.constants.item.rarity[g_args.rarity]['lower'] or 'normal') ) .. ' ' .. (g_args.class or '') )
    
    if g_args.class == 'Divination Card' then
        --TODO div card code
    else
        container
            :tag('span')
                :attr( 'class', 'itemboxheader-' .. (g_args.base_item and 'double' or 'single') )
            :tag('span')
                :attr('class', 'itemboxheaderleft')
                :done()
            :tag('span')
                :attr('class', 'itemboxheaderright')
                :done()
            :tag('span')
                :attr('class', 'itemboxheadertext')
                :wikitext( g_args.name .. (g_args.base_item and '<br>' .. g_args.base_item or '') )
                :done()
            :done()
            
        local grpcont
        local valid
        local statcont = container:tag('span')
        statcont
            :attr('class', 'itemboxstats')
            :done()
            
        for _, group in ipairs(core.display_groups) do
            grpcont = {}
            if group.func == nil then
                for _, disp in ipairs(group) do
                    valid = true
                    -- No args to verify which means always valid
                    if disp.args == nil then
                    elseif type(disp.args) == 'table' then
                        for _, key in ipairs(disp.args) do
                            if g_args[key] == nil then
                                valid = false
                                break
                            end
                        end
                    elseif type(disp.args) == 'function' then
                        valid = disp.args()
                    end
                    if valid then
                        --mw.logObject(disp)
                        grpcont[#grpcont+1] = disp.func()
                    end
                end
            else
                grpcont = group.func()
            end
            
            if #grpcont > 0 then
                statcont
                    :tag('span')
                    :attr('class', 'itemboxstatsgroup ' .. (group.css_class or ''))
                    :wikitext(table.concat(grpcont, '<br>'))
                    :done()
            end
        end
    end
    
    g_frame:callParserFunction('#set:', g_args._properties)
    
    return container
end

function v._itembox_categories()
    cats = {}
    cats[#cats+1] = g_args.class
    if g_args.rarity == 'Unique' then
        cats[#cats+1] = 'Unique ' .. g_args.class
    end
    
    if g_args.class == 'Active Skill Gems' then
        for _, tag in ipairs(g_args.gem_tags) do
            cats[#cats+1] = tag .. ' Skills'
        end
    elseif g_args.class == 'Support Skill Gems' then
        for _, tag in ipairs(g_args.gem_tags) do
            cats[#cats+1] = tag .. ' Supports'
        end
    end
    
    -- TODO: add maintenance categories
    
    --
    -- Output formatting
    --
    
    return util.misc.add_category(cats)
end

return p