Module:ItemData
From He is Coming Official Wiki
Documentation for this module may be created at Module:ItemData/doc
-- Item infoboxes, item lists and the cauldron grid, all from one place: [[Module:ItemData/data]].
--
-- Item pages keep calling {{Infobox item/as}} / {{Infobox cooked item/as}} exactly as before. Those templates call
-- this module, which takes rarity, base stats, effect and region (cooked items: stats, effect, ingredients) from the
-- data page, and everything else (found in, itemsets, tags, images) from the item page. Items that are not in the
-- data page (removed items, Darkland) show what their page says, like before.
--
-- After a patch, update [[Module:ItemData/data]] and every infobox, list and grid follows.
--
-- Functions:
-- {{#invoke:ItemData|infobox|kind=item}} used by Template:Infobox item/as
-- {{#invoke:ItemData|infobox|kind=cooked}} used by Template:Infobox cooked item/as
-- {{#invoke:ItemData|list|kind=weapon|region=Woodland|rarity=rare|tag=Wood|banner=Woodland Red|ingredient=Honeycomb|text=stun}}
-- kind: weapon, item, cooked (or several: kind=weapon,item); region: Woodland, Swampland or none (no bundle);
-- world: Woodland, Swampland, Kingmaker or All - the mode the item belongs to, including items without a bundle
-- (mythic, cursed, elixirs ...); weapon modifications count for the world of either weapon;
-- weaponmod: yes or no; notag: leave out these tags (notag=Jewelry,Food);
-- text: words in the effect; sort: rarity (default), region or name; all=yes also lists items without a page;
-- combo=yes adds the two weapons of a weapon modification, honey=yes adds what the food becomes with a Honeycomb
-- {{#invoke:ItemData|count|...same filters...}}
-- {{#invoke:ItemData|get|Item name|field}} field = rarity, stats, effect, region, tags, ingredients
-- {{#invoke:ItemData|cauldronGrid}}
local p = {}
local data = mw.loadData('Module:ItemData/data')
local kw = require('Module:Keyword')
local STAT_ORDER = {'attack', 'health', 'armor', 'speed'} -- the order most item pages use
local RARITY_ORDER = {common = 1, rare = 2, heroic = 3, mythic = 4, cursed = 5, weaponmod = 6}
local REGION_ORDER = {['Woodland Red'] = 1, ['Woodland Blue'] = 2, ['Woodland Yellow'] = 3,
['Swampland Red'] = 4, ['Swampland Blue'] = 5, ['Swampland Yellow'] = 6}
local function norm(name)
return mw.text.trim((name or ''):gsub('\226\128\153', "'")) -- curly apostrophe -> '
end
-- mw.loadData tables are read-only proxies: # and table.concat don't work on them, so copy lists first
local function copy(t)
local o = {}
for _, v in ipairs(t or {}) do
o[#o + 1] = v
end
return o
end
function p.lookup(name)
return data[norm(name)]
end
local function pageOf(name, d)
return (d and d.page) or name
end
---------------------------------------------------------------- formatting
local function regionText(r)
if not r or r == '' then
return nil
end
local area, color = r:match('^(%a+) (%a+)$')
if not area or not REGION_ORDER[r] then
return r -- already wikitext (Beehive, Halloween ...)
end
return string.format('[[%s_items|%s]] [[File:Banner %s %s.png]]', area, area,
area == 'Woodland' and 'wood' or 'swamp', color:lower())
end
local function worldText(w)
if w == 'Woodland' or w == 'Swampland' then
return string.format('[[%s_items|%s]]', w, w)
elseif w == 'Kingmaker' then
return '[[Kingmaker]]'
end
return nil
end
local function statsText(st, upgrades)
local out = {}
for _, k in ipairs(STAT_ORDER) do
local v = st and st[k]
if v then
local s = tostring(v)
if upgrades then
s = string.format('%d / %d / %d', v, v * 2, v * 4)
end
table.insert(out, '[[File:Icon ' .. k .. '.png]] ' .. s)
end
end
return #out > 0 and table.concat(out, '<br>') or '-'
end
local function rarityText(frame, r, mode)
if r == 'weaponmod' then
return 'Weapon modification'
end
return frame:preprocess('{{Category:Rarity_' .. r .. '|' .. (mode or '') .. '}}')
end
local function effectText(frame, e, mode, plain)
if not e or e == '' or e == '-' then
return '-'
end
local t = e:gsub('{{{1|}}}', mode or ''):gsub('{{{1}}}', mode or '')
t = frame:preprocess(t)
if plain then
return t
end
return kw.annotate(frame, t)
end
local function itemLink(frame, name)
local d = p.lookup(name)
if d and d.haspage then
local tpl = d.kind == 'cooked' and 'Infobox_cooked_item/iconLink' or 'Infobox_item/iconLink'
return frame:expandTemplate{title = tpl, args = {name = pageOf(name, d)}}
end
return '[[' .. name .. ']]'
end
---------------------------------------------------------------- infoboxes
local function renderItem(frame, a, d, mode, name)
local function A(k, def)
local v = a[k]
if v == nil then return def end
return v
end
local nm = name or 'Unknown'
local hasup = A('hasupgrades', 'false')
local basestats, effect, rarity, region
if d then
basestats = statsText(d.stats, mw.text.trim(hasup) == 'true')
effect = effectText(frame, d.effect, mode)
rarity = d.rarity and rarityText(frame, d.rarity, mode)
region = regionText(d.region)
else
effect = kw.annotate(frame, A('effect', '-'))
end
if mode == '' then
return frame:expandTemplate{title = 'Infobox item', args = {
hasupgrades = hasup,
image = A('image', nm .. ' 16x.png'),
goldimage = A('goldimage', nm .. ' Gold 16x.png'),
diamondimage = A('diamondimage', nm .. ' Diamond 16x.png'),
basestats = basestats or A('basestats', '-'),
effect = effect,
tags = A('tags', '-'),
rarity = rarity or A('rarity', '[[File:Rarity common.png]] Common'),
foundin = A('foundin', '[[DropSources/Treasure Chests|Treasure Chests]]'),
region = region or A('region', '[[Woodland_items|Woodland]] [[File:Banner wood blue.png]]'),
itemsets = A('itemsets', '-'),
}}
elseif mode == 'iconLink' then
return frame:expandTemplate{title = 'Infobox_item/iconLink', args = {name = name or 'Ironstone Greatsword'}}
elseif mode == 'regionalRow' or mode == 'regionalRowOne' then
return frame:expandTemplate{title = 'Infobox_item/regionalRow', args = {
name = name,
rarity = rarity or A('rarity', '{{{rarity}}}'),
basestats = basestats or A('summarystats', A('basestats', '{{{basestats}}}')),
effect = effect,
region = region or A('region', ''),
set_widths = mode == 'regionalRowOne' and 'true' or nil,
}}
end
return '<span class="error">Bad Template:Infobox_item output parameter!</span>'
end
local function renderCooked(frame, a, d, mode, name)
local function A(k, def)
local v = a[k]
if v == nil then return def end
return v
end
local nm = name or 'Unknown'
local basestats, effect, ing1, ing2, reg1, reg2
if d then
basestats = statsText(d.stats, false)
effect = effectText(frame, d.effect, mode)
local ing = d.ingredients or {}
local regs = d.ingredientRegions or {}
if ing[1] and ing[2] then
ing1, ing2 = itemLink(frame, ing[1]), itemLink(frame, ing[2])
reg1, reg2 = regionText(regs[1]) or '-', regionText(regs[2]) or '-'
end
else
effect = kw.annotate(frame, A('effect', '-'))
end
if mode == '' then
return frame:expandTemplate{title = 'Infobox cooked item', args = {
image = A('image', nm .. ' 16x.png'),
basestats = basestats or A('basestats', '-'),
effect = effect,
tags = A('tags', '-'),
ingredient1 = ing1 or A('ingredient1', nm .. ' 16x.png'),
ingredient2 = ing2 or A('ingredient2', nm .. ' 16x.png'),
region1 = reg1 or A('region1', '[[Woodland_items|Woodland]] [[File:Banner wood blue.png]]'),
region2 = reg2 or A('region2', '[[Woodland_items|Woodland]] [[File:Banner wood blue.png]]'),
}}
elseif mode == 'iconLink' then
return frame:expandTemplate{title = 'Infobox_cooked_item/iconLink', args = {name = name or 'Melonvine Whip'}}
elseif mode == 'regionalRow' or mode == 'regionalRowMirrored' then
return frame:expandTemplate{title = 'Infobox_cooked_item/' .. mode, args = {
name = name or 'Melonvine Whip',
ingredient1 = ing1 or A('ingredient1', '{{{ingredient1}}}'),
ingredient2 = ing2 or A('ingredient2', '{{{ingredient2}}}'),
basestats = basestats or A('summarystats', A('basestats', '{{{basestats}}}')),
effect = effect,
}}
end
return '<span class="error">Bad Template:Infobox_item output parameter!</span>'
end
function p.infobox(frame)
local parent = frame:getParent()
local a = parent and parent.args or {}
local mode = mw.text.trim(a[1] or '')
local name = a.name and mw.text.trim(a.name) or nil
if name == '' then name = nil end
local d = name and p.lookup(name)
-- trimmed like the old #ifeq/#switch version, so rows start with "|" and nothing adds blank lines
if (frame.args.kind or 'item') == 'cooked' then
return mw.text.trim(renderCooked(frame, a, d, mode, name))
end
return mw.text.trim(renderItem(frame, a, d, mode, name))
end
---------------------------------------------------------------- lists
local function inWorld(d, w)
if mw.ustring.lower(d.world or '') == w then return true end
for _, c in ipairs(d.components or {}) do -- weapon modifications: either weapon's world
local cd = data[c]
if cd and mw.ustring.lower(cd.world or '') == w then return true end
end
return false
end
local function hasTag(d, list)
for t in mw.ustring.lower(list):gmatch('[^,]+') do
t = mw.text.trim(t)
for _, dt in ipairs(d.tags or {}) do
if mw.ustring.lower(dt) == t then return true end
end
end
return false
end
local function matches(name, d, f)
if f.all ~= 'yes' and not d.haspage then return false end
if f.kind and f.kind ~= '' then
local kinds = {}
for k in f.kind:gmatch('[^,%s]+') do kinds[k] = true end
if not kinds[d.kind] then return false end
end
if f.world and f.world ~= '' and not inWorld(d, mw.ustring.lower(mw.text.trim(f.world))) then return false end
if f.weaponmod == 'yes' and d.rarity ~= 'weaponmod' then return false end
if f.weaponmod == 'no' and d.rarity == 'weaponmod' then return false end
if f.notag and f.notag ~= '' and hasTag(d, f.notag) then return false end
if f.rarity and f.rarity ~= '' and d.rarity ~= mw.ustring.lower(f.rarity) then return false end
if f.banner and f.banner ~= '' and d.region ~= f.banner then return false end
if f.region == 'none' then
if d.region or d.kind == 'cooked' then return false end
elseif f.region and f.region ~= '' then
local r = d.region or ''
if d.kind == 'cooked' then r = table.concat(copy(d.ingredientRegions), ' ') end
if not r:find(f.region, 1, true) then return false end
end
if f.tag and f.tag ~= '' then
local ok = false
for _, t in ipairs(d.tags or {}) do
if mw.ustring.lower(t) == mw.ustring.lower(f.tag) then ok = true end
end
if not ok then return false end
end
if f.ingredient and f.ingredient ~= '' then
local ing = d.ingredients or {}
if norm(ing[1]) ~= norm(f.ingredient) and norm(ing[2]) ~= norm(f.ingredient) then return false end
end
if f.text and f.text ~= '' and not mw.ustring.find(mw.ustring.lower(d.effect or ''), mw.ustring.lower(f.text), 1, true) then
return false
end
return true
end
local function pick(f)
local names = {}
for name, d in pairs(data) do
if matches(name, d, f) then table.insert(names, name) end
end
table.sort(names, function(x, y)
local a, b = data[x], data[y]
if f.sort == 'region' then
local ra, rb = REGION_ORDER[a.region or ''] or 9, REGION_ORDER[b.region or ''] or 9
if ra ~= rb then return ra < rb end
elseif f.sort ~= 'name' then
local ra, rb = RARITY_ORDER[a.rarity or ''] or 9, RARITY_ORDER[b.rarity or ''] or 9
if ra ~= rb then return ra < rb end
end
return x < y
end)
return names
end
function p.count(frame)
return #pick(frame.args)
end
local LISTARGS = {rarity = '-', basestats = '-', region = '-', ingredient1 = '-', ingredient2 = '-'}
-- Region column for items without a bundle: the weapons' regions for a weapon modification, otherwise the mode
local function listRegion(d)
if d.components then
local seen, out = {}, {}
for _, c in ipairs(d.components) do
local cd = data[c]
local r = cd and (regionText(cd.region) or worldText(cd.world))
if r and not seen[r] then
seen[r] = true
table.insert(out, r)
end
end
if #out > 0 then
return table.concat(out, '<br>')
end
end
return worldText(d.world) or '-'
end
local function honeyOf(name)
for n, d in pairs(data) do
if d.kind == 'cooked' and d.ingredients then
local a, b = d.ingredients[1], d.ingredients[2]
if (a == name and b == 'Honeycomb') or (b == name and a == 'Honeycomb') then
return n
end
end
end
end
function p.list(frame)
local f = frame.args
local names = pick(f)
if #names == 0 then
return "''No items.''"
end
local cooked = f.kind == 'cooked'
local out = {'{| class="wikitable sortable" style="width:100%;"'}
if cooked then
table.insert(out, '! style="width:170px;" | Item !! style="width:150px;" | Ingredient 1 !! style="width:150px;" | Ingredient 2 !! style="width:75px;" | Stats !! Effect')
else
table.insert(out, '! style="width:170px;" | ' .. (f.kind == 'weapon' and 'Weapon' or 'Item') ..
' !! style="width:95px;" | Rarity !! style="width:75px;" | Stats !! Effect !! style="width:130px;" | Region')
end
if f.combo == 'yes' then
table.insert(out, '! style="width:190px;" | Combination')
end
if f.honey == 'yes' then
table.insert(out, '! Cooked with ' .. itemLink(frame, 'Honeycomb'))
end
for _, name in ipairs(names) do
local d = data[name]
local row
if d.kind == 'cooked' then
row = renderCooked(frame, LISTARGS, d, 'regionalRow', name)
else
local a = {rarity = '-', basestats = '-', region = listRegion(d)}
row = renderItem(frame, a, d, 'regionalRow', name)
end
table.insert(out, '|-')
table.insert(out, mw.text.trim(row))
if f.combo == 'yes' then
local links = {}
for _, c in ipairs(d.components or {}) do
table.insert(links, itemLink(frame, c))
end
table.insert(out, '| ' .. (#links > 0 and table.concat(links, '<br>') or '-'))
end
if f.honey == 'yes' then
local h = honeyOf(name)
table.insert(out, '| ' .. (h and (itemLink(frame, h) .. '<br>' .. effectText(frame, data[h].effect, 'regionalRow')) or '-'))
end
end
table.insert(out, '|}')
return table.concat(out, '\n')
end
function p.get(frame)
local name, field = frame.args[1], mw.text.trim(frame.args[2] or 'effect')
local d = p.lookup(name)
if not d then return '' end
if field == 'stats' then return statsText(d.stats, false)
elseif field == 'effect' then return effectText(frame, d.effect, '')
elseif field == 'rarity' then return d.rarity and rarityText(frame, d.rarity, 'x') or ''
elseif field == 'region' then return regionText(d.region) or ''
elseif field == 'tags' then return table.concat(copy(d.tags), ', ')
elseif field == 'ingredients' then
return table.concat(copy(d.ingredients), ' + ')
end
return tostring(d[field] or '')
end
---------------------------------------------------------------- cauldron grid
function p.cauldronGrid(frame)
local recipe, ingSet = {}, {}
for name, d in pairs(data) do
if d.kind == 'cooked' and d.ingredients and d.ingredients[1] and d.ingredients[2] then
local x, y = d.ingredients[1], d.ingredients[2]
recipe[x .. '\0' .. y], recipe[y .. '\0' .. x] = name, name
ingSet[x], ingSet[y] = true, true
end
end
local ings = {}
for n in pairs(ingSet) do table.insert(ings, n) end
table.sort(ings, function(x, y)
local a, b = data[x] or {}, data[y] or {}
local ra, rb = REGION_ORDER[a.region or ''] or 9, REGION_ORDER[b.region or ''] or 9
if ra ~= rb then return ra < rb end
return x < y
end)
local function icon(n, size)
local d = data[n]
return string.format('[[File:%s.png|%dpx|link=%s|%s]]', n, size, pageOf(n, d), n)
end
local function tip(n)
local d = data[n] or {}
local lines = {'\'\'\'[[' .. pageOf(n, d) .. '|' .. n .. ']]\'\'\''}
local st = statsText(d.stats, false)
if st ~= '-' then
table.insert(lines, (st:gsub('<br>', ' ')))
end
if d.effect and d.effect ~= '-' then
table.insert(lines, effectText(frame, d.effect, 'regionalRow', true)) -- no tooltips inside the tooltip
end
return '<span class="hic-kw-tip">' .. table.concat(lines, '<br>') .. '</span>'
end
local out = {frame:extensionTag('templatestyles', '', {src = 'Template:Cauldron grid/styles.css'}),
frame:extensionTag('templatestyles', '', {src = 'Template:Keyword/styles.css'}),
'<div class="hic-grid-wrap">', '{| class="wikitable hic-grid"', '! class="hic-grid-corner" | '}
for _, n in ipairs(ings) do
table.insert(out, '! class="hic-grid-col" | ' .. icon(n, 24))
end
for _, rowIng in ipairs(ings) do
table.insert(out, '|-')
table.insert(out, '! class="hic-grid-row" | ' .. icon(rowIng, 24) .. ' <span class="hic-grid-name">[[' .. pageOf(rowIng, data[rowIng]) .. '|' .. rowIng .. ']]</span>')
for _, colIng in ipairs(ings) do
local r = recipe[rowIng .. '\0' .. colIng]
if r then
table.insert(out, '| class="hic-grid-hit" | <span class="hic-kw">' .. icon(r, 32) .. tip(r) .. '</span>')
else
table.insert(out, '| ')
end
end
end
table.insert(out, '|}')
table.insert(out, '</div>')
return table.concat(out, '\n')
end
return p


