Jump to content

Module:ItemTable

From Creopedia

Documentation for this module may be created at Module:ItemTable/doc

require( 'strict' )

--- One table of items per item type, joined across both games.
---
--- Modelled on Module:MoveTable, which does the same job for moves: read each
--- game's own data page, match records by name, and render one sortable table.

local tableLua = require( 'Module:TableLua' )
local itemLink = require( 'Module:ItemLink' )._main
local ecGameData = require( 'Module:GameData/EvoCreo' )

local p = {}

local EC2_ITEMS = 'Module:GameData/EvoCreo2/ItemTable.json'
local EC1_ITEMS = 'Module:GameData/EvoCreo/ItemData.json'

--- Which items actually have an uploaded icon.
---
--- Module:Item.getThumbnail builds a file name from the item name and never
--- checks that the file exists, so an item without art renders a red file link.
--- Testing each one with mw.title would work but costs an expensive parser
--- function per row, and this table runs to roughly five hundred rows against a
--- cap of five hundred. The list is generated instead, by
--- .claude/tools/items.py, and verified against the wiki.
local ICONS = 'Module:ItemTable/Icons.json'

--- The same, for the items that only ever appeared in EvoCreo 1.
---
--- Those have no EvoCreo 2 artwork to be illustrated with, so they fall back to
--- the icon from the game they did appear in. Generated by
--- `.claude/tools/items.py ec1index`.
local EC1_ICONS = 'Module:ItemTable/EvoCreoIcons.json'

--- Creo names, used to keep creo out of the item tables.
---
--- EvoCreo 1 sells a dozen creo straight out of its general item data, with a
--- cost attached: Pepita, Elacat, Deor and friends. They are creo rather than
--- items, and their articles are {{Creo}} pages, so a row for one would point
--- an item at a creo article.
local CREO = 'Module:GameData/EvoCreo2/CreoElementIndex.json'

local PRESENT = '✓'
local ABSENT = '—'

--- EvoCreo 1 ids whose EvoCreo 2 display name splits into different words, so
--- getName() cannot reproduce it. This is also why Breadcrumbs is missing from
--- Category:Items in EvoCreo, which matches on the same derivation.
---
--- Only spacing needs an entry here. A name the two games merely capitalise
--- differently is handled by getKey() below.
local ALIASES = {
	BREAD_CRUMBS = 'Breadcrumbs'
}

--- Items whose own name is taken by another subject, so the article lives
--- elsewhere. Deter is a trait as well as an item.
local ARTICLES = {
	Deter = 'Deter (item)'
}

--- Records the wiki does not cover.
---
--- The five BGC pieces are equipment entries with no artwork anywhere and no
--- description in any export, so a row for one could only ever be a name. Their
--- stub articles were deleted, and dropping the rows here is what keeps the
--- table from pointing at them.
---
--- Black Gemma is the same shape: an EvoCreo 2 general item with no icon and no
--- description text in either export.
local EXCLUDED = {
	['BGC Belt'] = true,
	['BGC Boots'] = true,
	['BGC Drill'] = true,
	['BGC Hammer'] = true,
	['BGC Helmet'] = true,
	['Black Gemma'] = true
}

--- Built once per page; every section on the Items article asks for the same
--- join.
local cache


--- Fold a display name into the key the two games are joined on.
---
--- getName() capitalises every word, while EvoCreo 2 ships a handful of names
--- in sentence case: 'Creo medicine', 'Mahogany log'. Matching on the name as
--- spelled therefore split those items across two rows, one marked EvoCreo 2
--- only and one marked EvoCreo only whose link was red, no article carrying the
--- derived spelling.
---
--- Dropping case and punctuation is the same key .claude/tools/items.py joins
--- on. Re-run `items.py ec1casing` after a data refresh: a newly shared item
--- spelled in sentence case would otherwise hit this again, silently.
---
--- @param name string
--- @return string
local function getKey( name )
	return ( name:lower():gsub( '[^%w]', '' ) )
end

--- @return table
local function getData()
	if cache then
		return cache
	end

	local icons = mw.loadJsonData( ICONS ) or {}
	local evocreoIcons = mw.loadJsonData( EC1_ICONS ) or {}
	local creo = mw.loadJsonData( CREO ) or {}
	local rows = {}

	for _, item in ipairs( mw.loadJsonData( EC2_ITEMS ) or {} ) do
		if item.name and not EXCLUDED[item.name] then
			rows[getKey( item.name )] = {
				name = item.name,
				kind = item.type and item.type.name,
				evocreo = false,
				evocreo2 = true
			}
		end
	end

	for _, item in ipairs( mw.loadJsonData( EC1_ITEMS ) or {} ) do
		local name = ALIASES[item.id] or ecGameData.getName( item.id )

		if name and not creo[name] and not EXCLUDED[name] then
			local key = getKey( name )
			local row = rows[key]

			if not row then
				-- EvoCreo 1 only, so there is no EvoCreo 2 record to take a
				-- display name or a type from.
				row = {
					name = name,
					kind = item.type,
					evocreo = false,
					evocreo2 = false
				}
				rows[key] = row
			end

			row.evocreo = true
		end
	end

	cache = { rows = rows, icons = icons, evocreoIcons = evocreoIcons }

	return cache
end

--- @param row table
--- @param data table
--- @return string
local function getNameWikitext( row, data )
	local article = ARTICLES[row.name] or row.name

	if row.evocreo2 and data.icons[row.name] then
		return itemLink( { text = row.name, link = article } )
	end

	-- An item that never appeared in EvoCreo 2 has no artwork under that name,
	-- so asking for it would render a red file link. Illustrate it with the
	-- icon from the game it did appear in.
	if not row.evocreo2 and data.evocreoIcons[row.name] then
		return itemLink( { text = row.name, link = article, game = 'evocreo' } )
	end

	return string.format( '[[%s|%s]]', article, row.name )
end

--- @return TableColumn[]
local function getColumns()
	return {
		{ id = 'name', label = 'Item' },
		{ id = 'evocreo', label = '[[EvoCreo]]', textAlign = 'center' },
		{ id = 'evocreo2', label = '[[EvoCreo 2]]', textAlign = 'center' }
	}
end

--- @param kind string
--- @return TableRow[]
local function getRows( kind )
	local data = getData()

	local matched = {}
	for _, row in pairs( data.rows ) do
		if row.kind == kind then
			table.insert( matched, row )
		end
	end

	table.sort( matched, function ( a, b )
		return a.name < b.name
	end )

	local rows = {}
	for _, row in ipairs( matched ) do
		table.insert( rows, {
			getNameWikitext( row, data ),
			row.evocreo and PRESENT or ABSENT,
			row.evocreo2 and PRESENT or ABSENT
		} )
	end

	return rows
end

--- Lua entry point for the module
---
--- @param kind string one of KEY, GENERAL, LINK, HEALTH, MOVE, EQUIPMENT, OUTFITS
--- @return string
function p._main( kind )
	if type( kind ) ~= 'string' or kind == '' then
		error( 'No item type provided' )
	end

	kind = kind:upper()

	return tableLua.render( {
		caption = kind,
		hideCaption = true, -- the section heading already labels the table
		columns = getColumns(),
		data = getRows( kind ),
		class = 'sortable'
	} )
end

--- Wikitext entry point for the module
---
--- @param frame mw.frame
--- @return string
function p.main( frame )
	local getArgs = require( 'Module:Arguments' ).getArgs
	local args = getArgs( frame )

	return p._main( args.type or args[1] )
end

return p