Jump to content

Module:Move/Components/Infobox

From Creopedia

This documentation is transcluded from Module:Move/Components/Infobox/doc.

Output an infobox for Module:Move using Module:InfoboxLua.


require( 'strict' )

local infoboxLua = require( 'Module:InfoboxLua' )
local gameData = require( 'Module:GameData' )
local effect = require( 'Module:Effect' )
local getThumbnail = require( 'Module:Move' ).getThumbnail
local energy = require( 'Module:Energy' )._main
local badge = require( 'Module:BadgeLua' )
local element = require( 'Module:Element' )

local p = {}

--- Move icons are roughly 36px of pixel art. Shown at 4x, with pixelated
--- rendering so the upscale stays crisp instead of being smoothed.
local ICON_SIZE = 144

--- The games name the same move type differently, and EvoCreo has a NONE type
--- that carries no colour of its own.
local moveTypeIds = {
	NORMAL = 'normal',
	SUPER = 'super',
	ELITE = 'elite',
	HEAL = 'heal',
	HEALING = 'heal'
}


--- @param name string
--- @return string
local function titleCase( name )
	return name:sub( 1, 1 ):upper() .. name:sub( 2 ):lower()
end

--- Badges return an empty string rather than nil for a field the game does not
--- record, so the row can be concatenated without leaving holes in the table.
---
--- @param name string|nil
--- @return string
local function getMoveTypeBadge( name )
	if not name then
		return ''
	end

	local id = moveTypeIds[name:upper()]

	return badge.render( {
		text = name,
		class = id and ( 't-move-type t-move-type--' .. id ) or nil
	} )
end

--- @param name string|nil
--- @return string
local function getClassBadge( name )
	if not name then
		return ''
	end

	return badge.render( {
		text = name,
		class = 't-move-class t-move-class--' .. name:lower()
	} )
end

--- Element badges are looked up from a fixed table of the nine elements.
--- EvoCreo gives its typeless moves an element of NONE, which has no entry
--- there, so those fall back to a plain badge instead of raising an error.
---
--- @param name string|nil
--- @return string
local function getElementBadge( name )
	if not name then
		return ''
	end

	local ok, html = pcall( element._main, name )

	if ok then
		return html
	end

	return badge.render( { text = titleCase( name ) } )
end

--- The row of badges heading a game's stats.
---
--- Move type, category and element sit inside the game's own section rather
--- than in the infobox header, because the games disagree about them: of the
--- 162 moves they share, 57 change move type between EvoCreo and EvoCreo 2 and
--- 27 change category.
---
--- @param move table
--- @param args table
--- @return string
local function getBadgesHtml( move, args )
	local badges = {
		getMoveTypeBadge( args.type or move.type ),
		getClassBadge( args.class or move.class ),
		getElementBadge( args.element or move.element )
	}

	return tostring( mw.html.create( 'div' )
		:addClass( 't-move-infobox-badges' )
		:wikitext( table.concat( badges ) )
	)
end

--- Normalise one game's record of a move into the fields this infobox renders.
---
--- The two games spell the same fields differently, and EvoCreo stores accuracy
--- as a fraction rather than a percentage. EvoCreo also leaves an attribute out
--- of a move's record rather than writing its default value, so every field
--- taken from it needs that default supplied here.
---
--- @param data table
--- @param gameId string
--- @return table|nil
local function getMove( data, gameId )
	if gameId == 'evocreo2' then
		local ec2 = data.evocreo2

		if not ec2 then
			return nil
		end

		return {
			name = ec2.name,
			class = ec2.moveClass.name,
			type = ec2.moveType.name,
			element = ec2.element.name,
			damage = ec2.damage,
			accuracy = ec2.accuracy,
			recharge = ec2.recharge,
			-- Only EvoCreo 2 records what a move actually does. The EvoCreo
			-- data has no effects at all, so its section leaves this nil and
			-- renders no Effects block.
			effectMaps = ec2.moveEffectMaps,
			icon = getThumbnail( ec2.name )
		}
	end

	local ec1 = data.evocreo

	if not ec1 then
		return nil
	end

	return {
		name = data.name,
		class = ec1.type,
		-- Bite and Scratch carry no skill type of their own.
		type = ec1.skilltype or 'NORMAL',
		element = ec1.element,
		damage = ec1.basedamage,
		accuracy = math.floor( tonumber( ec1.accuracy or 1 ) * 100 + 0.5 ),
		-- Confused Strike and Rest carry no recharge, meaning none is needed.
		recharge = ec1.recharge or 0,
		icon = getThumbnail( data.name, 'evocreo' )
	}
end

--- Every game this move appears in, in the order the games are defined.
---
--- @param data table
--- @return table[]
local function getMoves( data )
	local moves = {}

	for _, game in ipairs( gameData.getGames() ) do
		local move = getMove( data, game.id )

		if move then
			table.insert( moves, { game = game, move = move } )
		end
	end

	return moves
end

--- @param move table
--- @param gameId string
--- @param args table
--- @return table[]
local function getStatsItems( move, gameId, args )
	local statsItems = {
		{
			label = 'Power',
			content = args.power or tostring( move.damage )
		},
		{
			label = 'Accuracy',
			content = args.accuracy or tostring( move.accuracy )
		},
		{
			-- EvoCreo moves take a number of turns to recharge; they do not
			-- consume energy the way EvoCreo 2 moves do.
			label = gameId == 'evocreo' and 'Recharge' or 'Energy',
			content = energy( args.energy or move.recharge )
		}
	}

	-- Appended rather than built in, because a move with no effects should show
	-- no Effects block at all, and the EvoCreo section never has any.
	for _, card in ipairs( effect.getInfoboxCards( move.effectMaps, 'move' ) ) do
		table.insert( statsItems, card )
	end

	return statsItems
end

--- One game's account of the move: how it is classified, then its numbers.
---
--- @param entry table
--- @param args table
--- @return table
local function getGameSection( entry, args )
	return {
		label = entry.game.name,
		columns = 3,
		content = getBadgesHtml( entry.move, args ),
		items = getStatsItems( entry.move, entry.game.id, args )
	}
end

--- Every field below the header can differ between the games, so each game
--- gets its own tab. A move that only ever appeared in one game gets a plain
--- section instead, which needs no heading to say which game it belongs to.
---
--- @param moves table[]
--- @param args table
--- @return table
local function getGamesSection( moves, args )
	if #moves == 1 then
		local section = getGameSection( moves[1], args )
		section.label = nil

		return section
	end

	local sections = {}

	for _, entry in ipairs( moves ) do
		table.insert( sections, getGameSection( entry, args ) )
	end

	return {
		sections = sections
	}
end

--- Build the infobox data needed from the raw data
---
--- @param moves table[]
--- @param args table
--- @return table
local function getInfoboxData( moves, args )
	local primary = moves[1].move

	return {
		title = primary.name,
		image = {
			src = args.image or primary.icon,
			size = ICON_SIZE,
			class = 'cp-image-pixelated t-move-infobox-image'
		},
		sections = {
			getGamesSection( moves, args )
		}
	}
end

--- @param context ComponentContext
--- @return string
function p.render( context )
	local moves = getMoves( context.data )

	if #moves == 0 then
		return tostring(
			mw.html.create( 'div' )
				:addClass( 'error' )
				:wikitext( 'Move data is currently unavailable.' )
		)
	end

	return mw.getCurrentFrame():extensionTag {
		name = 'templatestyles', args = { src = 'Module:Move/Components/Infobox/styles.css' }
	} .. infoboxLua.render( getInfoboxData( moves, context.args or {} ) )
end

return p