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 '&#45;'
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 '&#45;'
	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', '&#45;'))
	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', '&#45;'),
			effect = effect,
			tags = A('tags', '&#45;'),
			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', '&#45;'),
		}}
	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 '&#45;', regionText(regs[2]) or '&#45;'
		end
	else
		effect = kw.annotate(frame, A('effect', '&#45;'))
	end
	if mode == '' then
		return frame:expandTemplate{title = 'Infobox cooked item', args = {
			image = A('image', nm .. ' 16x.png'),
			basestats = basestats or A('basestats', '&#45;'),
			effect = effect,
			tags = A('tags', '&#45;'),
			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 = '&#45;', basestats = '&#45;', region = '&#45;', ingredient1 = '&#45;', ingredient2 = '&#45;'}

-- 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 '&#45;'
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 = '&#45;', basestats = '&#45;', 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 '&#45;'))
		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 '&#45;'))
		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 ~= '&#45;' then
			table.insert(lines, (st:gsub('<br>', '&nbsp; ')))
		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