Module:Item2: Difference between revisions
(Hide attribute requirement if <= minimum character attribute) |
(Avoiding magic numbers is one of the most fundamental rules of programming.) |
||
Line 303: | Line 303: | ||
for _, key in ipairs(keys) do | for _, key in ipairs(keys) do | ||
local req = math.floor(tonumber(data['mods.required_level']) * | local req = math.floor(tonumber(data['mods.required_level']) * cfg.item_required_level_modifier_contribution) | ||
if req > tpl_args[key] then | if req > tpl_args[key] then | ||
tpl_args[key] = req | tpl_args[key] = req |
Revision as of 00:27, 13 June 2021
This module is used on 13,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 module implements {{item}}.
Overview
This module is responsible for creating item boxes, various item lists, item links and other item-related tasks. In the process a lot of the input data is verified and also added as semantic property to pages; as such, any templates deriving from this module should not be used on user pages other then for temporary testing purposes.
This template is also backed by an export script in PyPoE which can be used to export item data from the game files which then can be used on the wiki. Use the export when possible. de:Modul:Item2 ru:Модуль:Item2
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
-------------------------------------------------------------------------------
--
-- Module:Item2
--
-- This module implements Template:Item and Template:Itembox
-------------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- TODO
-- ----------------------------------------------------------------------------
-- Items
-- -----
-- Check if abyss jewels actually have radius modifiers
--
-- Aggregate ids from skill id from modifiers
--
--
-- unique items:
-- 3D art
-- supporter attribution
--
-- Maps:
-- Area level can be retrieved eventually
--
-- Essence:
-- type column
-- monster modifier info
--
-- random modifier
-- -> sell price must consider random mods
--
-- ----------
-- Item class
-- ----------
--
-- remove the ul if name_list is not provided
-- maybe smw
local getArgs = require('Module:Arguments').getArgs
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_skill = require('Module:Skill')
local m_area = require('Module:Area')
local m_game = mw.loadData('Module:Game')
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox()
-- Lazy loading
local f_item_link -- require('Module:Item link').item_link
local f_process_upgraded_from -- require('Module:Item2/upgrade').process_upgraded_from
local f_build_cargo_data -- require('Module:Item2/cargo').build_cargo_data
-- 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
local core = use_sandbox and require('Module:Item2/core/sandbox') or require('Module:Item2/core')
local c = {}
c.item_classes = {}
-- ----------------------------------------------------------------------------
-- Helper functions
-- ----------------------------------------------------------------------------
local h = {}
function h.debug(tpl_args, func)
if tpl_args.debug == nil then
return
end
func()
end
-- Lazy loading for Module:Item link
function h.item_link(args)
if not f_item_link then
f_item_link = require('Module:Item link').item_link
end
return f_item_link(args)
end
function h.validate_mod(tpl_args, frame, args)
-- args:
-- key - implict or explicit
-- i
-- value
local prefix = args.key .. args.i
local value = tpl_args[prefix]
local is_implicit = args.key == 'implicit'
local out = {
result=nil,
-- commited to cargo at a later point
id=nil,
stat_text=nil,
is_implicit=is_implicit,
is_random=nil,
}
local mods_table = is_implicit and tpl_args._defined_implicit_mods or tpl_args._mods
if value ~= nil then
out.id = value
out.stat_text = tpl_args[prefix .. '_text']
out.is_random = false
table.insert(mods_table, out)
return true
elseif tpl_args[prefix .. '_random_list'] then
tpl_args._flags.random_mods = true
value = m_util.string.split(tpl_args[prefix .. '_random_list'], ',%s*')
for _, mod_id in ipairs(value) do
table.insert(mods_table, {
result = nil,
id = mod_id,
stat_text = tpl_args[prefix .. '_text'] or string.format(i18n.tooltips.random_mod, args.i),
is_implicit = is_implicit,
is_random = true,
})
end
return true
elseif tpl_args[prefix .. '_text'] then
value = tpl_args[prefix .. '_text']
tpl_args._flags.text_modifier = true
out.result = value
out.stat_text = value
out.is_random = false
table.insert(mods_table, out)
return true
end
return false
end
function h.handle_range_args(tpl_args, frame, argument_key, field, value, fmt_options)
fmt_options = mw.clone(fmt_options)
fmt_options.return_color = true
local html, colour = m_util.html.format_value(tpl_args, frame, value, fmt_options)
tpl_args[argument_key .. '_html'] = html
tpl_args[field .. '_html'] = html
tpl_args[field .. '_range_colour'] = colour
fmt_options = mw.clone(fmt_options)
fmt_options.no_color = true
tpl_args[field .. '_range_text'] = m_util.html.format_value(tpl_args, frame, value, fmt_options)
end
h.stat = {}
function h.stat.add(value, stat_cached)
value.min = value.min + stat_cached.min
value.max = value.max + stat_cached.max
end
function h.stat.more(value, stat_cached)
value.min = value.min * (1 + stat_cached.min / 100)
value.max = value.max * (1 + stat_cached.max / 100)
end
function h.stat.more_inverse(value, stat_cached)
value.min = value.min / (1 + stat_cached.min / 100)
value.max = value.max / (1 + stat_cached.max / 100)
end
--
-- Processing
--
function h.process_arguments(tpl_args, frame, params)
for _, k in ipairs(params) do
local data = core.map[k]
if data == nil then
error(string.format('Invalid key or missing data for "%s"', k))
end
if data.no_copy == nil then
table.insert(tpl_args._base_item_args, k)
end
if data.func ~= nil then
data.func(tpl_args, frame)
--[[local status, err = pcall(data.func)
-- an error was raised, return the error string instead of the template
if not status then
return err
end
]]--
end
if tpl_args[k] == nil then
if tpl_args.class_id and c.item_classes[tpl_args.class_id].defaults ~= nil and c.item_classes[tpl_args.class_id].defaults[k] ~= nil then
tpl_args[k] = c.item_classes[tpl_args.class_id].defaults[k]
elseif data.default ~= nil then
if type(data.default) == 'function' then
tpl_args[k] = data.default(tpl_args, frame)
else
tpl_args[k] = data.default
end
end
end
end
end
-- add defaults from class specifics and class groups
function h.build_item_classes(tpl_args, frame)
core.map.class.func(tpl_args, frame)
c.item_classes[tpl_args.class_id] = {
tables = {},
args = {},
late_args = {},
defaults = {},
}
for _, table_name in ipairs(cfg.tables) do
table.insert(c.item_classes[tpl_args.class_id].tables, table_name)
end
local item_classes_extend = {'tables', 'args', 'late_args'}
for _, row in pairs(cfg.class_groups) do
for class, _ in pairs(row.keys) do
if class == tpl_args.class_id then
for _, k in ipairs(item_classes_extend) do
if row[k] ~= nil then
for _, value in ipairs(row[k]) do
table.insert(c.item_classes[tpl_args.class_id][k], value)
end
end
end
break
end
end
end
local class_specifics = cfg.class_specifics[tpl_args.class_id]
if class_specifics then
for _, k in ipairs(item_classes_extend) do
if class_specifics[k] ~= nil then
for _, value in ipairs(class_specifics[k]) do
table.insert(c.item_classes[tpl_args.class_id][k], value)
end
end
end
if class_specifics.defaults ~= nil then
for key, value in pairs(class_specifics.defaults) do
c.item_classes[tpl_args.class_id].defaults[key] = value
end
end
end
end
function h.process_mods(tpl_args, frame)
-- If the item does not have its own implicit mods, fall back to the implicit mods on the base item.
local implicit_mods = tpl_args._defined_implicit_mods
if #implicit_mods == 0 then
implicit_mods = tpl_args._base_implicit_mods
end
for _, v in ipairs(implicit_mods) do
table.insert(tpl_args._mods, v)
end
if #tpl_args._mods > 0 then
local mods = {}
local mod_ids = {}
local non_random_mod_ids = {}
for _, mod_data in ipairs(tpl_args._mods) do
if mod_data.result == nil then
mods[mod_data.id] = mod_data
mod_ids[#mod_ids+1] = mod_data.id
if not mod_data.is_random then
table.insert(non_random_mod_ids, mod_data.id)
end
end
tpl_args._subobjects[#tpl_args._subobjects+1] = {
_table = 'item_mods',
id = mod_data.id,
text = mod_data.stat_text,
is_implicit = mod_data.is_implicit,
is_random = mod_data.is_random,
}
end
local results = m_cargo.array_query{
tables={'mods'},
fields={'mods._pageName', 'mods.id', 'mods.required_level', 'mods.stat_text'},
id_field='mods.id',
id_array=mod_ids,
}
for _, data in ipairs(results) do
local mod_data = mods[data['mods.id']]
mod_data.result = data
if mod_data.is_random == false then
-- update item level requirement
local keys = {'required_level_final'}
-- only update base item requirement if this is an implicit
if mod_data.key == 'implicit' then
keys[#keys+1] = 'required_level'
end
for _, key in ipairs(keys) do
local req = math.floor(tonumber(data['mods.required_level']) * cfg.item_required_level_modifier_contribution)
if req > tpl_args[key] then
tpl_args[key] = req
end
end
end
end
-- fetch stats
results = m_cargo.query(
{'mods', 'mod_stats'},
{'mods.id', 'mod_stats.id', 'mod_stats.min', 'mod_stats.max'},
{
join='mods._pageID=mod_stats._pageID',
where=string.format('mod_stats.id IS NOT NULL AND mods.id IN ("%s")', table.concat(mod_ids, '", "')),
}
)
for _, data in ipairs(results) do
-- Stat subobject
local mod_data = mods[data['mods.id']]
if mod_data.result.stats == nil then
mod_data.result.stats = {data, }
else
mod_data.result.stats[#mod_data.result.stats+1] = data
end
local id = data['mod_stats.id']
local value = {
min = tonumber(data['mod_stats.min']),
max = tonumber(data['mod_stats.max']),
}
value.avg = (value.min+value.max)/2
local prefix = ''
if mod_data.is_random then
prefix = '_random'
end
core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_stats')
if mod_data.is_implicit then
core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_implicit_stats')
else
core.stats_update(tpl_args, id, value, mod_data.result['mods.id'], prefix .. '_explicit_stats')
end
end
if tpl_args._flags.sell_prices_override ~= true then
-- fetch sell prices
results = m_cargo.query(
{'mods', 'mod_sell_prices'},
{'mods.id', 'mod_sell_prices.amount', 'mod_sell_prices.name'},
{
join='mods._pageID=mod_sell_prices._pageID',
-- must be non random mods to avoid accumulating sell prices of randomized modifiers
where=string.format('mod_sell_prices.amount IS NOT NULL AND mods.id IN ("%s")', table.concat(non_random_mod_ids, '", "')),
}
)
for _, data in ipairs(results) do
local mod_data = mods[data['mods.id']]
if not mod_data.is_implicit then
local values = {
name = data['mod_sell_prices.name'],
amount = tonumber(data['mod_sell_prices.amount']),
}
-- sell_prices is defined in tpl_args.sell_prices_override
tpl_args.sell_prices[values.name] = (tpl_args.sell_prices[values.name] or 0) + values.amount
end
end
end
end
if tpl_args._flags.sell_prices_override ~= true then
local missing_sell_price = true
for _, _ in pairs(tpl_args.sell_prices) do
missing_sell_price = false
break
end
if missing_sell_price then
tpl_args.sell_prices[i18n.tooltips.default_vendor_offer] = 1
end
-- Set sell price on page
for name, amount in pairs(tpl_args.sell_prices) do
-- sell_price_order is defined in tpl_args.sell_prices_override
tpl_args.sell_price_order[#tpl_args.sell_price_order+1] = name
tpl_args._subobjects[#tpl_args._subobjects+1] = {
_table = 'item_sell_prices',
amount = amount,
name = name,
}
end
table.sort(tpl_args.sell_price_order)
end
end
function h.process_base_item(tpl_args, frame)
local where
if tpl_args.base_item_id ~= nil then
where = string.format('items.metadata_id="%s"', tpl_args.base_item_id)
elseif tpl_args.base_item_page ~= nil then
where = string.format('items._pageName="%s"' , tpl_args.base_item_page)
elseif tpl_args.base_item ~= nil then
where = string.format('items.name="%s"' , tpl_args.base_item)
elseif tpl_args.rarity_id ~= 'normal' then
error(m_util.html.error{msg=i18n.errors.missing_base_item})
else
return
end
if where ~= nil and tpl_args.rarity_id == 'normal' and not tpl_args._flags.is_prophecy then
error(m_util.html.error{msg=i18n.errors.missing_rarity})
end
where = string.format('%s AND items.class_id="%s" AND items.rarity_id="normal"', where, tpl_args.class_id)
local join = {}
for _, table_name in ipairs(c.item_classes[tpl_args.class_id].tables) do
if table_name ~= 'items' then
join[#join+1] = string.format('items._pageID=%s._pageID', table_name)
end
end
local fields = {
'items._pageName',
'items.name',
'items.metadata_id',
}
for _, k in ipairs(tpl_args._base_item_args) do
local data = core.map[k]
if data.field ~= nil then
fields[#fields+1] = string.format('%s.%s', data.table, data.field)
end
end
local result = m_cargo.query(
c.item_classes[tpl_args.class_id].tables,
fields,
{
where=where,
join=table.concat(join, ','),
groupBy='items._pageID',
}
)
if #result > 1 then
error(m_util.html.error{msg=i18n.errors.duplicate_base_items})
-- TODO be more explicit in the error?
elseif #result == 0 then
error(m_util.html.error{msg=i18n.errors.base_item_not_found})
end
result = result[1]
tpl_args.base_item_data = result
h.process_arguments(tpl_args, frame, {'base_item', 'base_item_page', 'base_item_id'})
--Copy values..
for _, k in ipairs(tpl_args._base_item_args) do
local data = core.map[k]
if data.field ~= nil and data.func_fetch == nil then
local value = result[string.format('%s.%s', data.table, data.field)]
-- I can just use data.default since it will be nil if not provided (nil == nil). Neat! ;)
if value ~= nil and (tpl_args[k] == data.default or type(data.default) == 'function') then
tpl_args[k] = value
if data.func ~= nil then
data.func(tpl_args, frame)
end
if data.func_copy ~= nil then
data.func_copy(tpl_args, frame)
end
elseif value == nil and not data.debug_ignore_nil then
h.debug(tpl_args, function ()
error(string.format(i18n.debug.base_item_field_not_found, data.table, data.field))
end)
elseif tpl_args[k] ~= data.default then
h.debug(tpl_args, function ()
error(string.format(i18n.debug.field_value_mismatch, k, tostring(tpl_args[k])))
end)
end
elseif data.func_fetch ~= nil then
data.func_fetch(tpl_args, frame)
end
end
end
function h.process_quest_rewards(tpl_args, frame)
local rid = 1
local continue
tpl_args.quest_rewards = {}
tpl_args.vendor_rewards = {}
repeat
continue = true
local prefix = string.format('quest_reward%s_', rid)
local input_args = {
shared = {
['type'] = true,
['quest'] = false,
['quest_id'] = false,
['act'] = true,
['class_ids'] = false,
},
vendor = {
['npc'] = true,
},
quest = {
['sockets'] = false,
['item_level'] = false,
['rarity_id'] = false,
['notes'] = false,
},
}
local rdata = {}
for key, is_required in pairs(input_args.shared) do
rdata[key] = tpl_args[prefix .. key]
if is_required then
if rdata[key] == nil then
continue = false
break
end
end
end
if rdata.quest == nil or rdata.quest_id == nil then
continue = false
end
if continue and rdata.type == 'vendor' or rdata.type == 'quest' then
for key, is_required in pairs(input_args[rdata.type]) do
rdata[key] = tpl_args[prefix .. key]
if is_required then
if rdata[key] == nil then
continue = false
break
end
end
end
else
continue = false
end
if continue then
rdata.classes = {}
if rdata.class_ids ~= nil then
rdata.class_ids = m_util.string.split(rdata.class_ids, ',%s*')
for index, class_id in ipairs(rdata.class_ids) do
local class = m_game.constants.characters[class_id]
if class == nil then
error(string.format('Class id %s is invalid', class_id))
else
rdata.class_ids[index] = class.str_id
rdata.classes[index] = class.name
end
end
end
if rdata.item_level then
rdata.item_level = m_util.cast.number(rdata.item_level)
end
if rdata.rarity_id then
if m_game.constants.rarities[rdata.rarity_id] == nil then
error(string.format(i18n.errors.invalid_rarity_id, tostring(rdata.rarity_id)))
end
end
rdata._table = rdata.type .. '_rewards'
rdata.type = nil
tpl_args[rdata._table] = rdata
m_cargo.store(frame, rdata)
-- TODO: Verify quests and quest ids?
end
rid = rid + 1
until continue == false
end
-- Lazy loading for Module:Item2/cargo
function h.build_cargo_data(tpl_args, frame, item_classes)
if not f_build_cargo_data then
f_build_cargo_data = use_sandbox and require('Module:Item2/cargo/sandbox').build_cargo_data or require('Module:Item2/cargo').build_cargo_data
end
return f_build_cargo_data(tpl_args, frame, item_classes)
end
-- Lazy loading for Module:Item2/upgrade
function h.process_upgraded_from(tpl_args, frame)
if not f_process_upgraded_from then
f_process_upgraded_from = use_sandbox and require('Module:Item2/upgrade/sandbox').process_upgraded_from or require('Module:Item2/upgrade').process_upgraded_from
end
return f_process_upgraded_from(tpl_args, frame)
end
--
-- Display
--
function h.strip_random_stats(tpl_args, frame, stat_text, container_type)
if tpl_args._flags.random_mods and container_type == 'inline' then
repeat
local text = string.match(stat_text, '<th class="mw%-customtoggle%-31">(.+)</th>')
if text ~= nil then
stat_text = string.gsub(stat_text, '<table class="random%-modifier%-stats.+</table>', text, 1)
end
until text == nil
end
return stat_text
end
function h.add_to_container_from_map(tpl_args, frame, container, mapping, container_type)
local statcont = mw.html.create('span')
statcont
:attr('class', 'item-stats')
:done()
local count = 0 -- Number of groups in container
for _, group in ipairs(mapping) do
local lines = {}
if group.func == nil then
for _, line in ipairs(group) do
local show = true
if container_type == 'inline' and line.inline == false then -- TODO: This is not used currently. Need to address what is hidden in inline view.
show = false
elseif line.show == false then
show = false
elseif type(line.show) == 'function' then
show = line.show(tpl_args, frame, container_type)
end
if show then
lines[#lines+1] = line.func(tpl_args, frame, container_type)
end
end
else
lines = group.func(tpl_args, frame, container_type)
end
if #lines > 0 then
count = count + 1
local heading = ''
if group.heading == nil then
elseif type(group.heading) == 'function' then
heading = group.heading()
else
heading = string.format('<em class="header">%s</em><br>', group.heading)
end
statcont
:tag('span')
:attr('class', 'group ' .. (group.class or ''))
:wikitext(heading .. table.concat(lines, '<br>'))
:done()
end
end
-- Add groups to container
if count > 0 then
container:node(statcont)
end
end
function h.make_main_container(tpl_args, frame, container_type)
local container = mw.html.create('span')
:attr('class', 'item-box -' .. tpl_args.frame_type)
if tpl_args.class_id == 'DivinationCard' then
container
:tag('span')
:attr('class', 'divicard-wrapper')
:tag('span')
:attr('class', 'divicard-art')
:wikitext( '[[' .. tpl_args.card_art .. '|link=|alt=]]' )
:done()
:tag('span')
:attr('class', 'divicard-frame')
:wikitext( '[[File:Divination card frame.png|link=|alt=]]' )
:done()
:tag('span')
:attr('class', 'divicard-header')
:wikitext(tpl_args.name)
:done()
:tag('span')
:attr('class', 'divicard-stack')
:wikitext(tpl_args.stack_size)
:done()
:tag('span')
:attr('class', 'divicard-reward')
:tag('span')
:wikitext(tpl_args.description)
:done()
:done()
:tag('span')
:attr('class', 'divicard-flavour text-color -flavour')
:tag('span')
:wikitext(tpl_args.flavour_text)
:done()
:done()
:done()
--TODO Extras?
else
local header_css
if tpl_args.base_item and tpl_args.rarity_id ~= 'normal' then
line_type = 'double'
else
line_type = 'single'
end
local name_line = tpl_args.name
if tpl_args.base_item and not tpl_args._flags.is_prophecy then
name_line = name_line .. '<br>' .. tpl_args.base_item
end
-- Symbols - These are displayed in the item box header to indicate certain flags and/or item influences
local symbols
local influences = tpl_args.influences or {}
if tpl_args.is_replica then
symbols = {'replica', 'replica'}
if #influences > 0 then
symbols[2] = influences[1]
end
elseif #influences > 0 then
symbols = {influences[1], influences[1]}
if #influences > 1 then
symbols[2] = influences[2]
end
elseif tpl_args.is_fractured then
symbols = {'fractured', 'fractured'}
elseif tpl_args.is_synthesised then
symbols = {'synthesised', 'synthesised'}
elseif tpl_args.is_veiled then
symbols = {'veiled', 'veiled'}
end
container
:tag('span')
:attr( 'class', 'header -' .. line_type )
:tag('span')
:attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[1] or '') )
:done()
:wikitext(name_line)
:tag('span')
:attr( 'class', 'symbol' .. (symbols and ' -' .. symbols[2] or '') )
:done()
:done()
h.add_to_container_from_map(tpl_args, frame, container, c.item_infobox_groups, container_type)
end
if tpl_args.skill_icon ~= nil then
container:wikitext(string.format('[[%s]]', tpl_args.skill_icon))
end
return container
end
--
-- Factory
--
h.factory = {}
function h.factory.args_present(...)
local args = {...}
return function (tpl_args)
for _, k in ipairs(args) do
if tpl_args[k] == nil then
return false
end
end
return true
end
end
function h.factory.display_raw_value(key)
return function(tpl_args, frame)
return tpl_args[key]
end
end
function h.factory.descriptor_value(args)
-- Arguments:
-- key
-- tbl
args = args or {}
return function (tpl_args, frame, value)
args.tbl = args.tbl or tpl_args
if args.tbl[args.key] then
value = m_util.html.abbr(value, args.tbl[args.key])
end
return value
end
end
-- ----------------------------------------------------------------------------
-- Additional configuration
-- ----------------------------------------------------------------------------
-- helper to loop over the range variables easier
c.range_map = {
min = {
var = '_range_minimum',
},
max = {
var = '_range_maximum',
},
avg = {
var = '_range_average',
},
}
--
-- Contents here are meant to resemble the ingame infobox of items
--
c.item_infobox_groups = {
-- [n]:
-- class: Additional css class added to group tag
-- heading: Group heading text (used for extras)
-- lines:
-- [n]:
-- show: Show line if this function returns true; Always show if boolean true. Default: Always show
-- func: Function that returns line text
{
-- Cosmetic item type
{
show = h.factory.args_present('cosmetic_type'),
func = core.factory.infobox_line{
parts = {
{
key = 'cosmetic_type',
fmt = '%s',
color = 'default'
},
},
},
},
-- Weapon type
{
show = function(tpl_args, frame)
if tpl_args.class_id == nil then
return false
end
return cfg.class_groups.weapons.keys[tpl_args.class_id] ~= nil
end,
func = function(tpl_args, frame)
local v = i18n.item_class_map[tpl_args.class_id]
return m_util.html.format_value(tpl_args, frame, {min=v, max=v}, {color = 'default'})
end,
},
-- Hideout item type
{
show = function(tpl_args, frame)
return tpl_args.class_id == 'HideoutDoodad'
end,
func = function(tpl_args, frame)
return i18n.item_class_map[tpl_args.class_id]
end,
},
{
show = h.factory.args_present('gem_tags'),
func = function(tpl_args, frame)
local out = {}
for i, tag in ipairs(tpl_args.gem_tags) do
out[#out+1] = string.format(i18n.gem_tag_category, tag, tag)
end
return table.concat(out, ', ')
end,
},
{
show = h.factory.args_present('support_gem_letter_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'support_gem_letter_html',
},
},
fmt = i18n.tooltips.support_icon,
},
},
{
show = h.factory.args_present('radius'),
func = core.factory.infobox_line{
parts = {
{
key = 'radius',
func = h.factory.descriptor_value{key='radius_description'},
},
{
key = 'radius_secondary',
func = h.factory.descriptor_value{key='radius_secondary_description'},
},
{
key = 'radius_tertiary',
func = h.factory.descriptor_value{key='radius_tertiary_description'},
},
},
sep = ' / ',
fmt = i18n.tooltips.radius,
},
},
-- TODO: gem level here. Maybe put max level here?
{
-- TODO: Rework for 3.14.0 skill cost and reservation changes
show = true,
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'mana_cost',
hide_default = 100,
fmt = function (tpl_args, frame)
if tpl_args.has_percentage_mana_cost then
return '%i%%'
else
return '%i'
end
end,
inline = function (tpl_args, frame)
if tpl_args.has_reservation_mana_cost then
return i18n.tooltips.mana_reserved
else
return i18n.tooltips.mana_cost
end
end,
},
{
key = 'life_cost',
hide_default = 100,
fmt = '%i',
inline = function (tpl_args, frame)
return i18n.tooltips.life_cost
end,
},
},
},
},
{
show = true, -- TODO: Show only if has mana_multiplier
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'mana_multiplier',
hide_default = 100,
fmt = '%i%%',
},
},
fmt = i18n.tooltips.mana_multiplier,
},
},
{
show = true, -- TODO: Show only if has cooldown
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'cooldown',
hide_default = 0,
fmt = '%.2f ' .. m_game.units.seconds.short_lower,
},
},
fmt = i18n.tooltips.cooldown_time,
},
},
{
-- TODO: Combine with cooldown. Multi-use non-vaal skills display uses together with cooldown time. E.g., Cooldown Time: 8.00 sec (3 uses)
show = true,
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'stored_uses',
hide_default = 0,
fmt = '%i',
},
},
fmt = i18n.tooltips.stored_uses,
},
},
{
show = true, -- TODO: Show only if has vaal_souls_requirement
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'vaal_souls_requirement',
hide_default = 0,
fmt = '%i',
},
},
fmt = i18n.tooltips.vaal_souls_per_use,
},
},
{
show = true, -- TODO: Show only if has vaal_stored_uses
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'vaal_stored_uses',
hide_default = 0,
fmt = '%i',
},
},
fmt = i18n.tooltips.stored_uses, -- TODO: Singular or plural based on number
},
},
{
show = true, -- TODO: Show only if has vaal_soul_gain_prevention_time
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'vaal_soul_gain_prevention_time',
hide_default = 0,
-- Technically it rounds to nearest, but it is given in milliseconds in the data,
fmt = '%i ' .. m_game.units.seconds.short_lower,
},
},
fmt = i18n.tooltips.vaal_soul_gain_prevention_time,
},
},
{
show = h.factory.args_present('cast_time'),
func = core.factory.infobox_line{
parts = {
{
key = 'cast_time',
fmt = function (tpl_args, frame, value)
if value.min == 0 then
return i18n.tooltips.instant_cast_time
end
return '%.2f ' .. m_game.units.seconds.short_lower
end,
},
},
fmt = i18n.tooltips.cast_time,
},
},
{
show = true, -- TODO: Show only if has critical_strike_chance
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'critical_strike_chance',
hide_default = 0,
fmt = '%.2f%%',
},
},
fmt = i18n.tooltips.critical_strike_chance,
},
},
{
show = true, -- TODO: Show only if has attack_speed_multiplier
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'attack_speed_multiplier',
hide_default = 100,
fmt = '%i%%',
inline = '%s ' .. i18n.tooltips.of_base_stat,
inline_color = 'value',
},
},
fmt = i18n.tooltips.attack_speed_multiplier,
},
},
{
show = true, -- TODO: Show only if has damage_multiplier
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'damage_multiplier',
hide_default = 100,
fmt = '%i%%',
inline = '%s ' .. i18n.tooltips.of_base_stat,
inline_color = 'value',
},
},
fmt = i18n.tooltips.damage_multiplier,
},
},
{
show = true, -- TODO: Show only if has damage_effectiveness
func = core.factory.infobox_line{
type = 'gem',
parts = {
{
key = 'damage_effectiveness',
hide_default = 100,
fmt = '%i%%',
},
},
fmt = i18n.tooltips.damage_effectiveness,
},
},
{
show = h.factory.args_present('projectile_speed'),
func = core.factory.infobox_line{
parts = {
{
key = 'projectile_speed',
},
},
fmt = i18n.tooltips.projectile_speed,
},
},
-- Quality is before item stats, but after gem stuff and item class
{
show = h.factory.args_present('quality'),
func = core.factory.infobox_line{
parts = {
{
key = 'quality',
fmt = '+%i%%',
color = 'mod',
hide_default = 0,
},
},
fmt = i18n.tooltips.quality,
},
},
-- Weapon only
{
show = h.factory.args_present('physical_damage_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'physical_damage_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.physical_damage,
},
},
{
show = true, -- Elemental Damage
func = function(tpl_args, frame)
local keys = {'fire_damage_html', 'cold_damage_html', 'lightning_damage_html'}
local elements = {}
for _, key in ipairs(keys) do
if tpl_args[key] then
elements[#elements+1] = tpl_args[key]
end
end
local text = table.concat(elements, ', ') -- returns empty string if elements is empty
if text ~= '' then
return string.format(i18n.tooltips.elemental_damage, text)
end
return
end,
},
{
show = h.factory.args_present('chaos_damage_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'chaos_damage_html',
fmt = '%s',
color = false, -- html already has color
},
},
fmt = i18n.tooltips.chaos_damage,
},
},
{
show = h.factory.args_present('critical_strike_chance_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'critical_strike_chance_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.critical_strike_chance,
},
},
{
show = h.factory.args_present('attack_speed_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'attack_speed_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.attacks_per_second,
},
},
{
show = h.factory.args_present('weapon_range_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'weapon_range_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.weapon_range,
},
},
-- Map only
{
show = h.factory.args_present('map_area_level'),
func = core.factory.infobox_line{
parts = {
{
key = 'map_area_level',
fmt = '%i',
},
},
fmt = i18n.tooltips.map_level,
},
},
{
show = h.factory.args_present('map_tier'),
func = core.factory.infobox_line{
parts = {
{
key = 'map_tier',
fmt = '%i',
},
},
fmt = i18n.tooltips.map_tier,
},
},
{
show = function(tpl_args, frame)
return tpl_args.map_guild_character ~= nil and tpl_args.rarity_id == 'normal'
end,
func = core.factory.infobox_line{
parts = {
{
key = 'map_guild_character',
fmt = '%s',
},
},
fmt = i18n.tooltips.map_guild_character,
},
},
{
show = function(tpl_args, frame)
return tpl_args.unique_map_guild_character ~= nil and tpl_args.rarity_id == 'unique'
end,
func = core.factory.infobox_line{
parts = {
{
key = 'unique_map_guild_character',
fmt = '%s',
},
},
fmt = i18n.tooltips.map_guild_character,
},
},
{
show = true,
func = core.factory.infobox_line{
type = 'stat',
parts = {
{
key = 'map_item_drop_quantity_+%',
fmt = '+%i%%',
color = 'mod',
hide_default = 0,
},
},
fmt = i18n.tooltips.item_quantity,
},
},
{
show = true,
func = core.factory.infobox_line{
type = 'stat',
parts = {
{
key = 'map_item_drop_rarity_+%',
fmt = '+%i%%',
color = 'mod',
hide_default = 0,
},
},
fmt = i18n.tooltips.item_rarity,
},
},
{
show = true,
func = core.factory.infobox_line{
type = 'stat',
parts = {
{
key = 'map_pack_size_+%',
fmt = '+%i%%',
color = 'mod',
hide_default = 0,
},
},
fmt = i18n.tooltips.monster_pack_size,
},
},
-- Jewel Only
{
show = true,
func = core.factory.infobox_line{
parts = {
{
key = 'item_limit',
fmt = '%i',
},
},
fmt = i18n.tooltips.limited_to,
},
},
{
show = h.factory.args_present('jewel_radius_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'jewel_radius_html',
fmt = '%s',
color = false, -- html already has color
},
},
fmt = i18n.tooltips.radius,
},
},
-- Flask only
{
show = h.factory.args_present('flask_mana_html', 'flask_duration_html'),
--func = core.factory.display_flask('flask_mana'),
func = core.factory.infobox_line{
parts = {
{
key = 'flask_mana_html',
fmt = '%s',
},
{
key = 'flask_duration_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.flask_mana_recovery,
},
},
{
show = h.factory.args_present('flask_life_html', 'flask_duration_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'flask_life_html',
fmt = '%s',
},
{
key = 'flask_duration_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.flask_life_recovery,
},
},
{
-- don't display for mana/life flasks
show = function(tpl_args, frame)
for _, k in ipairs({'flask_life_html', 'flask_mana_html'}) do
if tpl_args[k] ~= nil then
return false
end
end
return tpl_args['flask_duration_html'] ~= nil
end,
func = core.factory.infobox_line{
parts = {
{
key = 'flask_duration_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.flask_duration,
},
},
{
show = h.factory.args_present('charges_per_use_html', 'charges_max_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'charges_per_use_html',
fmt = '%s',
},
{
key = 'charges_max_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.flask_charges_per_use,
},
},
{
show = h.factory.args_present('buff_stat_text'),
func = core.factory.infobox_line{
parts = {
{
key = 'buff_stat_text',
color = 'mod',
},
},
},
},
-- armor
{
show = h.factory.args_present('block_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'block_html',
fmt = '%s',
hide_default = 0,
hide_default_key = 'block',
},
},
fmt = i18n.tooltips.chance_to_block,
},
},
{
show = h.factory.args_present('armour_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'armour_html',
fmt = '%s',
hide_default = 0,
hide_default_key = 'armour',
},
},
fmt = i18n.tooltips.armour,
},
},
{
show = h.factory.args_present('evasion_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'evasion_html',
fmt = '%s',
hide_default = 0,
hide_default_key = 'evasion',
},
},
fmt = i18n.tooltips.evasion,
},
},
{
show = h.factory.args_present('energy_shield_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'energy_shield_html',
fmt = '%s',
hide_default = 0,
hide_default_key = 'energy_shield',
},
},
fmt = i18n.tooltips.energy_shield,
},
},
{
show = h.factory.args_present('movement_speed'),
func = core.factory.infobox_line{
parts = {
{
key = 'movement_speed',
fmt = '%s%%',
hide_default = 0,
},
},
fmt = i18n.tooltips.movement_speed,
},
},
-- Amulet only
{
show = h.factory.args_present('talisman_tier'),
func = core.factory.infobox_line{
parts = {
{
key = 'talisman_tier',
fmt = '%i',
},
},
fmt = i18n.tooltips.talisman_tier,
},
},
-- Misc
{
show = h.factory.args_present('stack_size'),
func = core.factory.infobox_line{
parts = {
{
key = 'stack_size',
fmt = '%i',
hide_default = 1,
},
},
fmt = i18n.tooltips.stack_size,
},
},
-- Essence stuff
{
show = h.factory.args_present('essence_level'),
func = core.factory.infobox_line{
parts = {
{
key = 'essence_level',
fmt = '%i',
},
},
fmt = i18n.tooltips.essence_level,
},
},
-- Blight items
{
show = h.factory.args_present('blight_item_tier'),
func = core.factory.infobox_line{
parts = {
{
key = 'blight_item_tier',
fmt = '%i',
},
},
fmt = i18n.tooltips.blight_item_tier,
},
},
-- Harvest seeds (upper section)
{
show = h.factory.args_present('seed_tier'),
func = core.factory.infobox_line{
parts = {
{
key = 'seed_tier',
fmt = '%i',
},
},
fmt = i18n.tooltips.seed_tier,
},
},
{
show = h.factory.args_present('seed_tier'),
func = function (tpl_args, value)
return i18n.tooltips.seed_monster
end,
},
{
show = h.factory.args_present('seed_type_html'),
func = core.factory.infobox_line{
parts = {
{
key = 'seed_type_html',
fmt = '%s',
},
},
fmt = i18n.tooltips.seed_lifeforce_gained,
},
},
{
show = h.factory.args_present('seed_growth_cycles'),
func = core.factory.infobox_line{
parts = {
{
key = 'seed_growth_cycles',
fmt = '%s',
},
},
fmt = i18n.tooltips.seed_growth_cycles,
},
},
-- Harvest plant boosters
-- TODO: Remaining enhancements
{
show = h.factory.args_present('plant_booster_radius'),
func = core.factory.infobox_line{
parts = {
{
key = 'plant_booster_radius',
fmt = '%s',
},
},
fmt = i18n.tooltips.radius,
},
},
{
show = h.factory.args_present('heist_required_npcs'),
func = core.factory.infobox_line{
parts = {
{
key = 'heist_required_npcs',
fmt = '%s',
},
},
fmt = i18n.tooltips.heist_required_npc,
},
},
},
-- Requirements
{
-- TODO: i18n Master name?
{
show = h.factory.args_present('master', 'master_level_requirement'),
func = function(tpl_args, frame)
-- masters have been validated before
local data
for i, rowdata in ipairs(m_game.constants.masters) do
if tpl_args.master == rowdata.full then
data = rowdata
break
end
end
return m_util.html.poe_color('default', i18n.tooltips.requires, string.format('[[%s|%s %s]]', data.full, data.short_upper, tpl_args.master_level_requirement))
end
},
-- Instead of item level, show drop level if any
{
show = true, -- Requires...
func = function(tpl_args, frame)
local parts = {
{
key = 'required_level_final_html',
hide_default = 1,
hide_default_key = 'required_level_final',
inline = i18n.tooltips.level_inline,
inline_color = false,
},
}
for _, attr in ipairs(m_game.constants.attribute_order) do
parts[#parts+1] = {
key = string.format('required_%s_html', attr),
hide = function (tpl_args, frame, value)
local min = m_game.constants.characters.minimum_attributes[m_game.constants.attributes[attr].arg]
return value.min <= min and value.max <= min
end,
hide_key = string.format('required_%s', attr),
inline = ', %s ' .. m_game.constants.attributes[attr].short_upper,
inline_color = false,
}
end
local requirements = core.factory.infobox_line{parts = parts}(tpl_args, frame)
if requirements == nil then -- return early
return
end
requirements = string.gsub(requirements, '^, ', '')
return string.format(i18n.tooltips.requires, requirements)
end,
},
{
show = h.factory.args_present('heist_required_job', 'heist_required_job_level'),
func = core.factory.infobox_line{
parts = {
{
key = 'heist_required_job_level',
fmt = '%s',
},
{
key = 'heist_required_job',
fmt = '%s',
},
},
fmt = i18n.tooltips.heist_required_job,
},
},
},
-- Gem description
{
class = 'tc -gemdesc',
{
show = h.factory.args_present('gem_description'),
func = h.factory.display_raw_value('gem_description'),
},
},
-- Gem Quality Stats
{
class = 'tc -mod',
{
show = h.factory.args_present('skill_quality'),
func = function(tpl_args, frame)
local span = mw.html.create('span')
span
:addClass('quality')
local span2 = span:tag('span')
span2
:addClass('quality-header')
:tag('span')
:wikitext(m_util.html.poe_color('default', i18n.tooltips.gem_quality))
for i, quality_data in ipairs(tpl_args.skill_quality) do
local span_inner = span2:tag('span')
span_inner
:addClass('quality-section-select')
:addClass('quality-' .. i)
:wikitext(i)
:tag('span')
:wikitext(i18n.tooltips['gem_quality_' .. i])
if i == 1 then
span_inner:addClass('quality-selected')
end
end
for i, quality_data in ipairs(tpl_args.skill_quality) do
local span_inner = span:tag('span')
span_inner
:addClass('quality-box')
:addClass('quality-' .. i)
:wikitext(quality_data.stat_text)
if i == 1 then
span_inner:addClass('quality-selected')
end
end
return tostring(span)
end,
},
},
-- Gem Implicit Stats
{
class = 'tc -mod',
{
show = function(tpl_args, frame)
return cfg.class_groups.gems.keys[tpl_args.class_id] and tpl_args.stat_text
end,
func = function(tpl_args, frame)
lines = {}
lines[#lines+1] = tpl_args.stat_text
for _, tag in ipairs(tpl_args.gem_tags) do
if tag == m_game.constants.item.gem_tags.vaal.tag then
lines[#lines+1] = i18n.tooltips.corrupted
break
end
end
return table.concat(lines, '<br>')
end,
},
},
-- Implicit Stats
{
class = 'tc -mod',
func = function(tpl_args, frame, container_type)
if tpl_args.implicit_stat_text then
return {h.strip_random_stats(tpl_args, frame, tpl_args.implicit_stat_text, container_type)}
else
return {}
end
end,
},
-- Stats
{
class = 'tc -mod',
func = function(tpl_args, frame, container_type)
if tpl_args.explicit_stat_text then
return {h.strip_random_stats(tpl_args, frame, tpl_args.explicit_stat_text, container_type)}
else
return {}
end
end,
},
-- Experience
--[[{
{
show = h.factory.args_present('experience'),
func = core.factory.infobox_line{
parts = {
{
key = 'experience',
fmt = '%i',
},
},
},
},
},]]--
-- Harvest seeds (lower section)
{
class = 'tc -mod',
{
show = h.factory.args_present('seed_consumed_wild_lifeforce_percentage'),
func = function (tpl_args, frame)
if tpl_args.seed_consumed_wild_lifeforce_percentage > 0 then
return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_wild_lifeforce_percentage, m_util.html.poe_color('wild', m_game.seed_types.wild))
end
end
},
{
show = h.factory.args_present('seed_consumed_vivid_lifeforce_percentage'),
func = function (tpl_args, frame)
if tpl_args.seed_consumed_vivid_lifeforce_percentage > 0 then
return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_vivid_lifeforce_percentage, m_util.html.poe_color('vivid', m_game.seed_types.vivid))
end
end
},
{
show = h.factory.args_present('seed_consumed_primal_lifeforce_percentage'),
func = function (tpl_args, frame)
if tpl_args.seed_consumed_primal_lifeforce_percentage > 0 then
return string.format(i18n.tooltips.seed_lifeforce_consumed, tpl_args.seed_consumed_primal_lifeforce_percentage, m_util.html.poe_color('primal', m_game.seed_types.primal))
end
end
},
{
show = h.factory.args_present('seed_required_nearby_seed_tier', 'seed_type_html', 'seed_required_nearby_seed_amount'),
func = core.factory.infobox_line{
parts = {
{
key = 'seed_required_nearby_seed_amount',
fmt = '%s',
},
{
key = 'seed_type_html',
fmt = '%s',
},
{
key = 'seed_required_nearby_seed_tier',
fmt = '%s',
},
},
fmt = i18n.tooltips.seed_required_seeds,
color = 'mod',
},
},
},
{
class = 'tc -crafted',
{
show = h.factory.args_present('seed_effect'),
func = h.factory.display_raw_value('seed_effect'),
},
},
-- Description (currency, doodads)
{
class = 'tc -mod',
{
show = h.factory.args_present('description'),
func = h.factory.display_raw_value('description'),
},
--[[{
show = h.factory.args_present('plant_booster_additional_crafting_options'),
func = core.factory.infobox_line{
parts = {
{
key = 'plant_booster_additional_crafting_options',
fmt = '%s',
},
},
fmt = i18n.tooltips.plant_booster_additional_crafting_options,
},
},
{
show = h.factory.args_present('plant_booster_extra_chances'),
func = core.factory.infobox_line{
parts = {
{
key = 'plant_booster_extra_chances',
fmt = '%s%%',
},
},
fmt = i18n.tooltips.plant_booster_extra_chances,
},
},
{
show = h.factory.args_present('plant_booster_lifeforce'),
func = core.factory.infobox_line{
parts = {
{
key = 'plant_booster_lifeforce',
fmt = '%s%%',
},
},
fmt = i18n.tooltips.plant_booster_lifeforce,
},
},]]
},
{
class = 'tc -crafted',
{
show = h.factory.args_present('incubator_effect'),
func = h.factory.display_raw_value('incubator_effect'),
},
},
-- Variations (for doodads)
{
class = 'tc -mod',
{
show = h.factory.args_present('variation_count'),
func = function(tpl_args, frame)
local txt
if tpl_args.variation_count == 1 then
txt = i18n.tooltips.variation_singular
else
txt = i18n.tooltips.variation_plural
end
return string.format('%i %s', tpl_args.variation_count, txt)
end,
},
},
-- Flavour Text
{
class = 'tc -flavour',
{
show = h.factory.args_present('flavour_text'),
func = h.factory.display_raw_value('flavour_text'),
},
},
-- Prophecy text
{
class = 'tc -value',
{
show = h.factory.args_present('prediction_text'),
func = h.factory.display_raw_value('prediction_text'),
},
},
-- Can not be traded or modified
{
class = 'tc -canttradeormodify',
{
show = h.factory.args_present('cannot_be_traded_or_modified'),
func = function(tpl_args, frame)
if tpl_args.cannot_be_traded_or_modified == true then
return i18n.tooltips.cannot_be_traded_or_modified
end
end,
},
},
-- Help text
{
class = 'tc -help',
{
show = h.factory.args_present('help_text'),
func = h.factory.display_raw_value('help_text'),
},
},
-- Cost (i.e. vendor costs)
{
--class = '',
{
show = h.factory.args_present('master_favour_cost'),
func = core.factory.infobox_line{
parts = {
{
key = 'master_favour_cost',
color = 'currency',
},
},
fmt = i18n.tooltips.favour_cost,
},
},
{
show = h.factory.args_present('seal_cost'),
func = core.factory.infobox_line{
parts = {
{
key = 'seal_cost',
fmt = function (tpl_args, frame, value)
return '%dx ' .. h.item_link{metadata_id='Metadata/Items/Currency/CurrencySilverCoin', html=''}
end,
color = 'currency',
},
},
fmt = i18n.tooltips.seal_cost,
},
},
},
}
--
-- This is meant to show additional information about the item in a separate infobox
--
c.extra_display_groups = {
{
heading = i18n.tooltips.extra_info,
class = '',
{
show = h.factory.args_present('atlas_connections'),
func = function(tpl_args, frame)
local fields = {
[false] = {
value = '✗',
sort = 0,
class = 'table-cell-xmark',
},
[true] = {
value = '✓',
sort = 1,
class = 'table-cell-checkmark',
},
}
local tbl = mw.html.create('table')
tbl
:attr('class', 'wikitable')
:attr('style', 'width:100%;')
:tag('tr')
:tag('th')
:attr('colspan', 6)
:attr('style', 'text-decoration: underline;')
:wikitext(i18n.tooltips.header_overall)
:done()
:done()
:tag('tr')
:tag('th')
:wikitext(i18n.tooltips.header_upgrades)
:done()
:tag('th')
:wikitext(0)
:done()
:tag('th')
:wikitext(1)
:done()
:tag('th')
:wikitext(2)
:done()
:tag('th')
:wikitext(3)
:done()
:tag('th')
:wikitext(4)
:done()
:done()
for _, vtype in ipairs({'tier', 'level'}) do
local tr = tbl:tag('tr')
tr
:tag('th')
:wikitext(i18n.tooltips['header_map_' .. vtype])
:done()
for i=0,4 do
local value = tpl_args['atlas_map_tier' .. i]
if value == 0 then
value = fields[false].value
elseif vtype == 'level' then
value = value + 67
end
tr
:tag('td')
:wikitext(value)
:done()
end
tr:done()
end
tbl
:tag('tr')
:tag('th')
:attr('colspan', 6)
:attr('style', 'text-decoration: underline;')
:wikitext(i18n.tooltips.header_connections)
:done()
:done()
-- sort alphabetically
local sorted = {}
for key, value in pairs(tpl_args.atlas_connections) do
sorted[#sorted+1] = key
end
table.sort(sorted)
for _, key in ipairs(sorted) do
local tr = tbl:tag('tr')
tr
:tag('th')
:wikitext(key)
:done()
for i=0,4 do
local field = fields[tpl_args.atlas_connections[key]['region' .. i]]
tr
:tag('td')
:attr('data-sort-value', field.sort)
:attr('class', field.class)
:wikitext(field.value)
:done()
end
tr:done()
end
return tostring(tbl)
end
},
},
-- Drop info
{
heading = i18n.tooltips.drop_restrictions,
class = '',
{
show = h.factory.args_present('drop_enabled'),
func = core.factory.infobox_line{
parts = {
{
key = 'drop_level',
fmt = '%i',
},
{
key = 'drop_level_maximum',
fmt = '%i',
hide_default = 100,
},
},
sep = ' / ',
fmt = i18n.tooltips.level,
},
},
{
show = h.factory.args_present('drop_leagues'),
func = function(tpl_args, frame)
return string.format(i18n.tooltips.league_restriction, m_util.html.poe_color('value', table.concat(tpl_args.drop_leagues, ', ')))
end
},
{
show = h.factory.args_present('drop_areas_html'),
func = h.factory.display_raw_value('drop_areas_html'),
},
{
show = h.factory.args_present('drop_text'),
func = h.factory.display_raw_value('drop_text'),
},
{
show = function(tpl_args, frame)
if tpl_args.drop_enabled == true then
return false
end
return true
end,
func = function(tpl_args, frame)
local span = mw.html.create('span')
span
:attr('class', 'infobox-disabled-drop')
:wikitext(i18n.tooltips.drop_disabled)
:done()
return tostring(span)
end,
},
},
{
heading = i18n.tooltips.purchase_costs,
{
show = function(tpl_args, frame)
for rarity, data in pairs(tpl_args.purchase_costs) do
if #data > 0 then
return true
end
end
return false
end,
func = function(tpl_args, frame)
local tbl = mw.html.create('table')
tbl
--:attr('class', 'wikitable')
:attr('style', 'width: 100%; margin-top: 0px;')
for _, rarity_id in ipairs(m_game.constants.rarity_order) do
local data = tpl_args.purchase_costs[rarity_id]
if #data > 0 then
local tr = tbl:tag('tr')
tr
:tag('td')
:wikitext(m_game.constants.rarities[rarity_id].long_upper)
local td = tr:tag('td')
for _, purchase_data in ipairs(data) do
td:wikitext(string.format('%dx [[%s]]<br />', purchase_data.amount, purchase_data.name))
end
end
end
return tostring(tbl)
end,
},
},
{
heading = i18n.tooltips.sell_price,
{
show = h.factory.args_present('sell_price_order'),
func = function(tpl_args, frame)
local out = {}
for _, item_name in ipairs(tpl_args.sell_price_order) do
out[#out+1] = string.format('%dx [[%s]]', tpl_args.sell_prices[item_name], item_name)
end
return table.concat(out, '<br />')
end,
},
},
-- Damage per second
{
heading = i18n.tooltips.damage_per_second,
-- Autoinsert here from dps map
},
{
heading = i18n.tooltips.misc,
{
show = h.factory.args_present('class'),
func = core.factory.infobox_line{
parts = {
{
key = 'class',
fmt = '%s',
},
},
fmt = i18n.tooltips.item_class,
},
},
{
show = h.factory.args_present('metadata_id'),
func = core.factory.infobox_line{
parts = {
{
key = 'metadata_id',
fmt = '%s',
truncate = true,
},
},
fmt = i18n.tooltips.metadata_id,
},
},
},
}
for i, data in ipairs(core.dps_map) do
table.insert(c.extra_display_groups[4], {
show = h.factory.args_present(data.name .. '_html'),
func = core.factory.infobox_line{
parts = {
{
key = data.name .. '_html',
inline = data.label_infobox .. ': %s',
fmt = '%s',
-- the html already contains the colour
color = false,
},
},
},
})
if i == 5 then
table.insert(c.extra_display_groups[4], {
show = function (tpl_args, frame)
return tpl_args.elemental_dps_html ~= nil or tpl_args.poison_dps_html ~= nil
end,
func = function (tpl_args, frame)
return ''
end,
})
elseif i == 7 then
table.insert(c.extra_display_groups[4], {
show = h.factory.args_present('dps_html'),
func = function (tpl_args, frame)
return ''
end,
})
end
end
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
local p = {}
--
-- Template:Item
--
function p.item(frame)
local t = os.clock()
local tpl_args = getArgs(frame, {
parentFirst = true
})
frame = m_util.misc.get_frame(frame)
--
-- Shared args
--
tpl_args._flags = {}
tpl_args._base_item_args = {}
tpl_args._base_implicit_mods = {}
tpl_args._defined_implicit_mods = {}
tpl_args._mods = {}
for _, k in ipairs({'', '_random'}) do
for _, prefix in ipairs({'', '_implicit', '_explicit'}) do
tpl_args[k .. prefix .. '_stats'] = {}
end
end
tpl_args._subobjects = {}
tpl_args._properties = {}
tpl_args._errors = {}
h.process_arguments(tpl_args, frame, {'class_id'})
h.build_item_classes(tpl_args, frame)
h.build_cargo_data(tpl_args, frame, c.item_classes)
-- Using general purpose function to handle release and removal versions
m_util.args.version(tpl_args, {frame=frame, set_properties=true})
-- Must validate some argument early. It is required for future things
h.process_arguments(tpl_args, frame, cfg.default_args)
h.process_arguments(tpl_args, frame, c.item_classes[tpl_args.class_id].args)
-- Base Item
h.process_base_item(tpl_args, frame)
-- Prophecy special snowflake
if tpl_args._flags.is_prophecy then
err = h.process_arguments(tpl_args, frame, cfg.prophecy_args)
if err then
return err
end
end
-- Mods
for _, k in ipairs({'implicit', 'explicit'}) do
local success = true
local i = 1
while success do
success = h.validate_mod(tpl_args, frame, {key=k, i=i})
i = i + 1
end
end
h.process_mods(tpl_args, frame)
-- Add stats - this is for when mods are not set, but we still need stats to calcuate new armour values etc
m_util.args.stats(tpl_args, {prefix='extra_'})
for _, stat in ipairs(tpl_args.extra_stats) do
if stat.value ~= nil then
stat.min = stat.value
stat.max = stat.value
stat.avg = stat.value
end
core.stats_update(tpl_args, stat.id, stat, nil, '_stats')
core.stats_update(tpl_args, stat.id, stat, nil, '_explicit_stats')
end
-- Transpose stats into cargo data
for _, random_prefix in ipairs({'', '_random'}) do
for _, type_prefix in ipairs({'', '_implicit', '_explicit'}) do
for id, data in pairs(tpl_args[random_prefix .. type_prefix .. '_stats']) do
local is_implicit
if type_prefix == '_implicit' then
is_implicit = true
elseif type_prefix == '_explicit' then
is_implicit = false
end
tpl_args._subobjects[#tpl_args._subobjects+1] = {
_table = 'item_stats',
id = id,
min = data.min,
max = data.max,
avg = data.avg,
is_implicit = is_implicit,
is_random = random_prefix == '_random',
}
end
end
end
-- Handle extra stats (for gems)
if cfg.class_groups.gems.keys[tpl_args.class_id] then
m_skill._skill(tpl_args, frame)
end
--
-- Handle local stats increases/reductions/additions
--
local skip = {}
-- general stats
for k, data in pairs(core.stat_map) do
local value = tpl_args[k]
if value ~= nil and skip[k] == nil then
value = {min=value, max=value, base=value}
-- If stats are overriden we scan save some CPU time here
local overridden = false
if data.stats_override ~= nil then
for stat_id, override_value in pairs(data.stats_override) do
local stat_value = tpl_args._stats[stat_id]
if stat_value ~= nil then
-- Use the value of stat
if override_value == true then
value.min = stat_value.min
value.max = stat_value.max
overridden = true
elseif stat_value ~= 0 then
value.min = override_value.min
value.max = override_value.max
overridden = true
end
end
end
end
if overridden == false then
-- The simple cases; this must be using ipairs as "add" must apply before
for _, operator in ipairs({'add', 'more'}) do
local st = data['stats_' .. operator]
if st ~= nil then
for _, statid in ipairs(st) do
if tpl_args._stats[statid] ~= nil then
h.stat[operator](value, tpl_args._stats[statid])
end
end
end
end
-- For increased stats we need to add them up first
for stat_key, stat_func in pairs({increased=h.stat.more, increased_inverse=h.stat.more_inverse}) do
local st = data['stats_' .. stat_key]
if st ~= nil then
local total_increase = {min=0, max=0}
for _, statid in ipairs(st) do
if tpl_args._stats[statid] ~= nil then
for var, current_value in pairs(total_increase) do
total_increase[var] = current_value + tpl_args._stats[statid][var]
end
end
end
stat_func(value, total_increase)
end
end
if data.minimum ~= nil then
for _, key in ipairs({'min', 'max'}) do
if value[key] < data.minimum then
value[key] = data.minimum
end
end
end
else
end
value.avg = (value.min + value.max) / 2
-- don't add the properties unless we need to
if (data.default ~= nil and (value.min ~= data.default or value.max ~= data.default)) or data.default == nil then
for short_key, range_data in pairs(c.range_map) do
tpl_args[data.field .. range_data.var] = value[short_key]
end
-- process to HTML to use on list pages or other purposes
h.handle_range_args(tpl_args, frame, k, data.field, value, data.html_fmt_options or {})
end
for short_key, range_data in pairs(c.range_map) do
tpl_args[k .. range_data.var] = value[short_key]
end
end
end
-- calculate and handle weapon dps
if cfg.class_groups.weapons.keys[tpl_args.class_id] then
for _, data in ipairs(core.dps_map) do
local damage = {
min = {},
max = {},
}
for var_type, value in pairs(damage) do
-- covers the min/max/avg range
for short_key, range_data in pairs(c.range_map) do
value[short_key] = 0
for _, damage_key in ipairs(data.damage_args) do
value[short_key] = value[short_key] + (tpl_args[string.format('%s_%s%s', damage_key, var_type, range_data.var)] or 0)
end
end
end
local value = {}
for short_key, range_data in pairs(c.range_map) do
local result = (damage.min[short_key] + damage.max[short_key]) / 2 * tpl_args[string.format('attack_speed%s', range_data.var)]
value[short_key] = result
tpl_args[string.format('%s%s', data.field, range_data.var)] = result
end
if value.avg > 0 then
h.handle_range_args(tpl_args, frame, data.name, data.field, value, data.html_fmt_options or {})
end
end
end
-- late processing
h.process_arguments(tpl_args, frame, cfg.late_args) -- General late args
h.process_arguments(tpl_args, frame, c.item_classes[tpl_args.class_id].late_args) -- Class-specific late args
-- Handle upgrade from restrictions/info
h.process_upgraded_from(tpl_args, frame)
-- Quest reward info
h.process_quest_rewards(tpl_args, frame)
-- ------------------------------------------------------------------------
-- Infobox handling
-- ------------------------------------------------------------------------
-- Store the infobox so it can be accessed with ease on other pages
tpl_args.html = tostring(h.make_main_container(tpl_args, frame, 'inline'))
local container = h.make_main_container(tpl_args, frame, 'infobox')
if tpl_args.inventory_icon ~= nil and tpl_args.class_id ~= 'DivinationCard' then
container:wikitext(string.format('[[%s|%sx%spx]]', tpl_args.inventory_icon, cfg.image_size_full*tpl_args.size_x, cfg.image_size_full*tpl_args.size_y))
end
--
-- Secondary infobox
--
tpl_args.extra_infobox = mw.html.create('span')
:attr( 'class', 'item-box -' .. tpl_args.frame_type)
h.add_to_container_from_map(tpl_args, frame, tpl_args.extra_infobox, c.extra_display_groups)
--
-- Output
--
local infobox = mw.html.create('span')
infobox
:attr('class', 'infobox-page-container')
:node(container)
:node(tpl_args.extra_infobox)
-- skill_screenshot is set in skill module
if tpl_args.skill_screenshot then
infobox:wikitext(string.format('<br>[[%s|300px]]', tpl_args.skill_screenshot))
end
local out = tostring(infobox)
tpl_args.html_extra = out
-- ------------------------------------------------------------------------
-- Category handling
-- ------------------------------------------------------------------------
local cats = {}
if tpl_args.rarity_id == 'unique' then
cats[#cats+1] = string.format(i18n.categories.unique_affix, tpl_args.class)
elseif tpl_args._flags.is_prophecy then
cats[#cats+1] = i18n.categories.prophecies
elseif tpl_args._flags.is_blight_item then
cats[#cats+1] = i18n.categories.blight_item
elseif tpl_args.is_talisman then
cats[#cats+1] = i18n.categories.talismans
elseif tpl_args.is_essence then
cats[#cats+1] = i18n.categories.essences
elseif tpl_args.class_id == 'Map' then
cats[#cats+1] = string.format('%s %s', tpl_args.map_series, tpl_args.class)
else
cats[#cats+1] = tpl_args.class
end
if tpl_args.rarity_id ~= 'normal' or tpl_args.base_item_id == 'Metadata/Items/Currency/CurrencyItemisedProphecy' then
cats[#cats+1] = i18n.categories.derived_items
else
cats[#cats+1] = i18n.categories.base_items
end
for _, attr in ipairs(m_game.constants.attribute_order) do
if tpl_args[attr .. '_percent'] then
cats[#cats+1] = string.format('%s %s', m_game.constants.attributes[attr].long_upper, tpl_args.class)
end
end
local affix
if tpl_args.class_id == 'Active Skill Gem' or tpl_args.class_id == 'Support Skill Gem' then
affix = i18n.categories.gem_tag_affix
end
if affix ~= nil then
for _, tag in ipairs(tpl_args.gem_tags) do
cats[#cats+1] = string.format(affix, tag)
end
end
if #tpl_args._defined_implicit_mods > 0 and tpl_args.rarity_id ~= 'normal' then
cats[#cats+1] = i18n.categories.implicit_modifier_override
end
if #tpl_args.alternate_art_inventory_icons > 0 then
cats[#cats+1] = i18n.categories.alternate_artwork
end
-- TODO: add maintenance categories
if tpl_args.release_version == nil then
cats[#cats+1] = i18n.categories.missing_release_version
end
if tpl_args._flags.text_modifier and not tpl_args.suppress_improper_modifiers_category then
cats[#cats+1] = i18n.categories.improper_modifiers
end
--
for _, k in ipairs({'broken_upgraded_from_reference', 'duplicate_upgraded_from_reference', 'duplicate_query_area_ids', 'sell_prices_override'}) do
if tpl_args._flags[k] then
cats[#cats+1] = i18n.categories[k]
end
end
out = out .. m_util.misc.add_category(cats, {ingore_blacklist=tpl_args.debug})
--
-- Misc
--
-- Also show the infobox for areas right away for maps, since they're both on the same page
-- if tpl_args.map_series ~= i18n.misc.betrayal then
local query_id
if tpl_args.rarity_id == 'normal' and tpl_args.map_area_id ~= nil then
query_id = tpl_args.map_area_id
elseif tpl_args.rarity_id == 'unique' and tpl_args.unique_map_area_id ~= nil then
query_id = tpl_args.unique_map_area_id
end
if query_id then
out = out .. m_area.query_area_info{cats=yes, where=string.format('areas.id="%s"', query_id)}
end
-- ------------------------------------------------------------------------
-- Store cargo data
-- ------------------------------------------------------------------------
-- Map argument values for cargo storage
for _, table_name in ipairs(c.item_classes[tpl_args.class_id].tables) do
tpl_args._subobjects[table_name] = {
_table = table_name,
}
end
for k, v in pairs(tpl_args) do
local data = core.map[k]
if data ~= nil then
if data.table ~= nil and data.field ~= nil then
if data.type == 'Integer' then
v = tonumber(string.format("%.0f", v))
if v ~= tpl_args[k] then
mw.logObject(string.format('Float value "%s" for integer field "%s.%s"', tpl_args[k], data.table, data.field))
end
end
tpl_args._subobjects[data.table][data.field] = v
elseif data.table == nil and data.field ~= nil then
error(string.format('Missing table for field "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.field, k, mw.dumpObject(v), mw.dumpObject(data)))
elseif data.table ~= nil and data.field == nil then
error(string.format('Missing field for table "%s", key "%s", \nvalue:\n "%s" \ndata:\n%s', data.table, k, mw.dumpObject(v), mw.dumpObject(data)))
end
end
end
-- Don't actually save data in testing mode
if not tpl_args.test then
local attach = {}
for _, data in pairs(tpl_args._subobjects) do
if attach[data._table] == nil then
local i = 0
for _, _ in pairs(data) do
i = i + 1
-- Don't attach to tables we don't store data to. _table is always present so we need to check for 2 or more entries.
if i > 1 then
attach[data._table] = true
-- Keeping this here in case it might be useful in the future.
-- Recreating table is very slow and doing anything while the queue is running just creates a big mess.
frame:expandTemplate{title=string.format('Template:Item/cargo/attach/%s', data._table), args={}}
break
end
end
end
m_cargo.store(frame, data, {
debug=tpl_args.debug,
sep={
name_list='�',
},
})
end
end
-- Show additional error messages in console to help fixing them
if #tpl_args._errors > 0 then
mw.logObject(table.concat(tpl_args._errors, '\n'))
end
if tpl_args.test then
tpl_args.out = out
return tpl_args
else
mw.logObject(os.clock() - t)
return out
end
end
p.itembox = p.item -- Apparently this is still used somewhere???
--
-- Template:Itembox
--
function p.query_itembox(frame)
--[[
Queries for an item box.
Examples
--------
=p.query_itembox{page='Beach Map (Betrayal)'}
]]
local tpl_args = getArgs(frame, {
parentFirst = true
})
frame = m_util.misc.get_frame(frame)
local results = m_cargo.query(
{'items', 'maps', 'areas'},
{'items.html_extra', 'maps.area_id', 'areas.id', 'areas.infobox_html'},
{
join='items._pageID=maps._pageID, maps.area_id=areas.id',
where=string.format('items._pageName="%s"', tpl_args.page),
}
)
if #results == 0 then
error(i18n.errors.no_results_found)
end
local out = {
results[1]['items.html_extra'],
results[1]['areas.infobox_html'],
}
return table.concat(out, '')
end
return p