Module:Card

Revision as of 22:24, 14 July 2026 by Valaerys (talk | contribs) (Created page with "local p = {} local lib = require('Module:Feature') local TemplateData = require('Module:TemplateData') local Icon = require('Module:Icon') local RARITY_STARS = { --['0'] = '', --['1'] = '✦', --['2'] = '✦✦',notext --['3'] = '✦✦✦', --['4'] = '✦✦✦✦', --['5'] = '✦✦✦✦✦' ['1'] = 'x12px|link=|alt=Rarity 1', ['2'] = 'x12px|link=|alt=Rarity 2', ['3'] = '[[File:Icon 3 Stars.png|x12px|link=...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
This Lua module is used primarily on pages in the Main namespace. It can be found on pages, or on about Expression error: Unexpected / operator.% of those pages.
Edits to this Lua module will be widely noticed. To avoid large-scale disruption and unnecessary server load, any changes to this Lua module should be tested in its sandbox or testcases subpages. The tested changes can then be added to this page in one single edit.
Please consider discussing any changes in the wiki's Discord.

This module implements {{Card}}.


local p = {}
local lib = require('Module:Feature')
local TemplateData = require('Module:TemplateData')
local Icon = require('Module:Icon')


local RARITY_STARS = {
	--['0'] = '',
	--['1'] = '✦',
	--['2'] = '✦✦',notext
	--['3'] = '✦✦✦',
	--['4'] = '✦✦✦✦',
	--['5'] = '✦✦✦✦✦'
	['1'] = '[[File:Icon 1 Stars.png|x12px|link=|alt=Rarity 1]]',
	['2'] = '[[File:Icon 2 Stars.png|x12px|link=|alt=Rarity 2]]',
	['3'] = '[[File:Icon 3 Stars.png|x12px|link=|alt=Rarity 3]]',
	['4'] = '[[File:Icon 4 Stars.png|x12px|link=|alt=Rarity 4]]',
	['5'] = '[[File:Icon 5 Stars.png|x12px|link=|alt=Rarity 5]]'
}

local SUPERIMPOSITION_TABLE = {
	['1'] = { num = '1', text = 'I' },
	['I'] = { num = '1', text = 'I' },
	['2'] = { num = '2', text = 'II' },
	['II'] = { num = '2', text = 'II' },
	['3'] = { num = '3', text = 'III' },
	['III'] = { num = '3', text = 'III' },
	['4'] = { num = '4', text = 'IV' },
	['IV'] = { num = '4', text = 'IV' },
	['5'] = { num = '5', text = 'V' },
	['V'] = { num = '5', text = 'V' },
}

local PREFIX_ICONS = {
	['Recipe: '] = {name='Recipe', type='Item'}
}

-- main template/module parameters
local CARD_ARGS = {
	{
		name='name', alias={1,'character'}, displayName=1,
		status='suggested',
		label='Name',
		description='The name of the image (without the prefix or extension).',
		default='Unknown',
		example={'Credit', 'Stellar Jade', 'Amber'},
	},
	{
		name='text', alias=2, displayName=2,
		label='Card Text',
		description='The text below the image.',
		default='—',
		displayDefault='"—" for most items; same as Image Name for characters',
		example={'100', 'Lv. 1', 'Set'},
	},
	{
		name='notext', type='1',
		label='No Text',
		trueDescription='remove text from bottom.',
	},
	{
		name='multiline_text', type='1',
		label='Allow Multiple Lines of Card Text',
		trueDescription='allow card text to wrap to a new line if necessary.',
	},
	{
		name='rarity', displayType='number',
		label='Card Rarity',
		description='The Rarity of the item. For light background, add 10. Use 0 for white version. Use multiple numbers for split rarities.',
		default='0',
		displayDefault='determined based on Image Name; "0" if unknown',
		example={'1', '2', '3', '4', '5'},
	},
	{
		name='type', placeholderFor='Module:Icon',
	},
	{
		name='suffix', placeholderFor='Module:Icon',
	},
	{
		name='extension', placeholderFor='Module:Icon',
	},
	{
		name='link', displayType='wiki-page-name',
		label='Card Link',
		description='The page to link to when image or caption text is clicked.',
		displayDefault='same as Image Name',
		defaultFrom='name',
		example={'Credit', 'Virtual Particle', 'Trailblazer'},
	},
	{
		name='link_suffix',
		label='Page Link Suffix',
		description='Text to append to the Page Link. (Usually used to link to page sections.)',
		default='',
		example='#Steps',
	},
	{
		name='nolink', type='1',
		label='No Link',
		trueDescription='remove default links from card image, icons, and caption.',
		displayDefault='"1" if Image Name is default ("Unknown")',
	},
	{
		name='icon', displayType='content',
		label='Icon',
		description='The icon at the top left of the card.',
		example={'{{Icon/Element|Fire|25}}', '[[File:Item Stellar Jade.png|25px]]'},
	},
	{
		name='icon_style', displayType='string',
		label='Icon Style',
		description='The custom style of the icon at the top left of the card. Omit the \'\' used for CSS.',
		example='background: red;',
	},
	{
		name='icon_right', displayType='content',
		label='Top-Right Icon',
		description='Icon on the top right of the card.',
		example='{{Icon/Element|Fire|25}}',
	},
	{
		name='icon_right_style', displayType='string',
		label='Top-Right Icon Style',
		description='The custom style of the icon at the top right of the card. Omit the \'\' used for CSS.',
		example='background: red;',
	},
	{
		name='show_caption', type='1',
		label='Show Caption',
		trueDescription='show caption text below the card.',
	},
	{
		name='caption', displayType='string',
		label='Caption Text',
		description='The linked caption text that shows below the card.',
		displayDefault='same as Name',
		defaultFrom='name',
		example={'Comfort Food', 'Rewards'},
	},
	{
		name='caption_width',
		label='Caption Width',
		description='Width of the captions. Set to \'auto\' to automatically increase the caption width to prevent it from wrapping in the middle of words.',
		displayDefault='same as card width (74px, or 40px for mini cards)',
		example={'200px', 'auto'},
	},
	{
		name='note', displayType='string',
		label='Caption Note',
		description='The unlinked caption text that shows below the linked caption text.',
		example='(Bonus reward)',
	},
	{
		name='superimposition', alias='si', displayType='number',
		label='Superimposition Level',
		description='The superimposition level of the light cone.', --Set to \'1a\' to denote a light cone that cannot be superimposed.
		example={'1', '2', '3', '4', '5'}, --, '1a'
	},
	{
		name='ascension', placeholderFor='Module:Icon',
	},
	{
		name='eidolon', alias='e', displayType='number',
		label='Eidolon Level',
		description='Eidolon Level of the Character.',
		example={'0', '1', '2', '3', '4', '5', '6'},
	},
	{
		name='weakness', alias='w', displayType='string',
		label='Enemy Weakness',
		description='List of elements the enemy is weak against',
		example={'Quantum; Wind'},
	},
	{
		name='aether', displayType='string',
		label='Aether Type',
		description='Enemy\'s Aether type when an Aether Spirit',
		example={'Humanoid', 'Mechanical', 'Aberrant'},
	},
	{
		name='tier', displayType='string',
		label='Enemy Tier',
		description='List of elements the enemy is weak against',
		example={'Normal', 'Elite', 'Boss', 'Echo of War'},
	},
	{
		name='stars', type='1',
		label='Show Item Stars',
		trueDescription='show the stars of the rarity of the item. (Does not work if rarity is "0" or split (e.g., "123" or "34").)',
	},
	{
		name='mini', type='1',
		label='Mini',
		trueDescription='show a smaller card. (Not all parameters apply to mini cards.)',
	},
	{
		name='noprefix', type='1',
		label='Ignore Prefix',
		trueDescription='Skips any processing of prefixes (such as Recipe) for special cases.'
	},
	{
		name='notice', displayType='string',
		label='Notice Label',
		description='Text to display in a small box in the style of the in-game "New" notifications.',
		example={'New','2.2','Soon'}
	},
	{
		name='minitext', type='1',
		label='Allow Mini Card Text',
		description='Flag to allow the text param for mini cards.'
	},
}
-- add parameters inherited from Icon for the main image, top-left icon, and equipped icon
Icon.addIconArgs(CARD_ARGS, '', '', { -- exclude args already present in CARD_ARGS or that don't apply to the main image
	name=false,
	link=false,
	size=false,
	alt=false,
})
Icon.addIconArgs(CARD_ARGS, 'icon_', 'Top-Left Icon ', { -- override to add `element` alias and document automatic icons
	name={
		alias='element',
		description='Name of the icon to display at the top left of the card (without the prefix or extension).',
		displayDefault='element icon for characters; empty otherwise',
		example={'Quantum'}
	},
})
Icon.addIconArgs(CARD_ARGS, 'equipped_', 'Equipped ', { -- overrides to add `equipped`/`e` aliases and change default size
	name={
		alias={'equipped', 'eq'},
		description='Name of the light cone or character to display at the top right of the card (or bottom left, if Eidolon Level is present; name does not include the prefix or extension).',
		example={'In the Name of the World', 'Trailblazer'}
	},
	size={default='30'},
})


function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		parentFirst = true,
		wrapper = { 'Template:Card' }
	})
	return p._main(args, frame)
end

-- apply defaults; load and apply data (e.g., rarity, character element)
function p._processArgs(args)
	local out = TemplateData.processArgs(args, CARD_ARGS)
	
	-- set defaults that depend on other arguments
	out.defaults.show_caption = lib.isNotEmpty(out.nonDefaults.caption)
	out.defaults.nolink = out.name == out.defaults.name and lib.isEmpty(out.nonDefaults.link)
	
	-- handle deprecated caption value
	if tostring(out.caption) == '1' then
		out.uses_deprecated_params = true
		out.caption = nil
		out.show_caption = true
	end
	
	-- handle nolink
	out.final_link = out.nolink and '' or (out.link .. out.link_suffix)
	
	-- build args for main card image
	local mainImageArgs = Icon.extractIconArgs(out)
	mainImageArgs.link = out.final_link
	mainImageArgs.size = 74
	
	if (not args.noprefix) then
		-- check for prefixes in `name` that correspond to top-left icons
		-- note: priority of icon specification: `icon` arg > `icon_name` arg > prefix of `name` > character element from data
		local baseName, prefix = Icon.stripPrefixes(out.name, PREFIX_ICONS)
		if prefix then
			-- configure icon if none is specified by `icon_name`,
			-- making sure not to change `icon_type` if `icon_name` is explicitly specified
			if out.icon_name == out.defaults.icon_name then
				local prefixIcon = PREFIX_ICONS[prefix]
				out.icon_name = prefixIcon.name
				out.icon_type = prefixIcon.type
			end
			mainImageArgs.name = baseName
		end
	end
	
	-- create card image and get type/data
	local card_type, data
	out.image, card_type, data = Icon.createIcon(mainImageArgs)
	
	-- set defaults based on data
	if data then
		if card_type == 'Character' or card_type == 'Currency Wars Character' then
			if out.icon_name == out.defaults.icon_name and data.element ~= 'Adaptive' and lib.isNotEmpty(data.element) then
				out.icon_name = data.element
				out.icon_type = 'Icon'
			end
			out.defaults.text = data.name or out.name
		end
		if card_type == 'Light Cone' then
			out.icon_name = data.path
			out.icon_type = 'Icon'
			out.icon_size = 15
			-- if args.mini then
				out.image.defaults.suffix = 'Icon'
			-- else
			-- 	out.image.size = '74x'
			-- end
		end
		if card_type == 'Enemy' then
			out.weakness = out.weakness or data.weakness
			out.tier = out.tier or data.tier
			out.final_link = (out.nolink and '') or data.link or out.final_link
		end
		if card_type == 'Aether Enemy' then
			out.aether = out.aether or data.aethertype
			out.final_link = (out.nolink and '') or data.link or out.final_link
		end
		if card_type == 'Dice Face' then
			out.notext = true
			out.final_link = '#' .. out.final_link
		end
		if data.title then
			out.defaults.caption = data.title
		end
		if data.upcoming then
			out.defaults.notice = data.upcoming
		end
	end
	
	
	-- add top-left icon (if not specified by 'icon' wikitext)
	if out.icon == out.defaults.icon then
		local iconImage = Icon.createIcon(out, 'icon_')
		
		iconImage.defaults.size = lib.ternary(out.mini, 16, 20)
		-- disable link if nolink is explicitly set, but icon_link is not
		if out.nonDefaults.nolink then
			iconImage.defaults.link = ''
		end
		out.icon = iconImage:buildString()
	end
	
	-- add bottom-left icon for enemy weakness
	if lib.isNotEmpty(out.weakness) then
		local elements = lib.split(out.weakness, out.weakness:find(',') and ',' or ';')
		out.icon_bottom_type = 'Icon'
		out.icon_bottom = ''
		for _, element in ipairs(elements) do
			out.icon_bottom_name = element
			local iconImage = Icon.createIcon(out, 'icon_bottom_')
			
			iconImage.defaults.size = lib.ternary(out.mini, 10, 20)
			iconImage.defaults.alt = 'Weak to ' .. element
			-- disable link if nolink is explicitly set, but icon_link is not
			if out.nonDefaults.nolink then
				iconImage.defaults.link = ''
			end
			out.icon_bottom = out.icon_bottom .. iconImage:buildString()
		end
	end
	
	-- add bottom-left icon for enemy aether type
	if lib.isNotEmpty(out.aether) then
		out.icon_bottom_type = 'Type'
		out.icon_bottom_name = out.aether
		
		local iconImage = Icon.createIcon(out, 'icon_bottom_')
		iconImage.defaults.size = lib.ternary(out.mini, 10, 20)
		iconImage.defaults.alt = 'Aether Enemy of the ' .. out.aether .. ' type'
		-- disable link if nolink is explicitly set, but icon_link is not
		if out.nonDefaults.nolink then iconImage.defaults.link = '' end
		
		out.icon_bottom = iconImage:buildString()
	end
	
	-- add bottom-left icon currency wars bond(s)
	if lib.isNotEmpty(out.bonds) then
		local bonds = lib.split(out.bonds, ';')
		out.icon_bottom_type = 'Bond'
		out.icon_bottom = ''
		out.icon_bottom_suffix = 'Icon'
		out.icon_bottom_class = 'diamond-icon diamond-icon-morepadding'
		for _, bond in ipairs(bonds) do
			out.icon_bottom_name = bond
			local iconImage = Icon.createIcon(out, 'icon_bottom_')
			
			iconImage.defaults.size = lib.ternary(out.mini, 7, 15)
			iconImage.defaults.alt = 'Bond: ' .. bond
			-- disable link if nolink is explicitly set, but icon_link is not
			if out.nonDefaults.nolink then
				iconImage.defaults.link = ''
			else
				iconImage.defaults.link = 'Currency Wars: Zero-Sum Game/Bonds#' .. bond
			end
			out.icon_bottom = out.icon_bottom .. iconImage:buildString()
		end
	end
	
    -- add enemy danger icon
	if card_type == 'Enemy' and lib.isNotEmpty(out.tier) and out.tier ~= 'Normal' then
        out.icon_right = '[[File:Icon ' .. out.tier .. ' Enemy.png|25x25px|link=|alt=' .. out.tier .. ' Enemy]]'
	end
	
	-- Currency Wars on/off-field 
	if card_type == 'Currency Wars Character' and lib.isNotEmpty(out.fieldposition) then
		local posIcons = {}
		for pos in lib.gsplit(out.fieldposition, ';', {removeEmpty=true}) do
        	table.insert(posIcons, '[[File:Icon ' .. pos .. ' White.png|20x20px|link=|alt=' .. pos .. '|class=diamond-icon]]')
		end
		out.icon_right = table.concat(posIcons, '')
	end
	
	-- add equipped icon
	if lib.isNotEmpty(out.equipped_name) then
		local equippedImage, equippedType = Icon.createIcon(out, 'equipped_')
		equippedImage.defaults.suffix = 'Icon'
		if equippedType == 'Character' then
			equippedImage.defaults.size = 30
			equippedImage.defaults.alt = 'Equipped on ' .. equippedImage.alt
		else
			equippedImage.defaults.alt = 'Equipped with ' .. equippedImage.alt
		end
		if out.nonDefaults.nolink then
			equippedImage.defaults.link = ''
		end
		out.equipped = equippedImage:buildString()
	end
	
	return out, data
end

local function setCaptionWidth(node, width)
	if width == 'auto' then
		node:addClass('auto-width')
	else
		node:css('width', width)
	end
end

-- Creates and returns a card element from *processed* card args.
function p._createCard(a)
	local node_container = mw.html.create('div'):addClass('card-container')
	
	if a.mini then
		node_container:addClass('mini-card')
	end
	
	-- create two wrapper divs for the main card body
	-- (required for CSS positioning and rounded corners, respectively)
	local node_card = node_container:tag('span')
		:addClass('card-wrapper')
		:tag('span')
			:addClass('card-body')
			:addClass('card-' .. a.rarity)
	
	if lib.isNotEmpty(a.icon) then
		node_card:tag('span')
			:addClass('card-icon')
			:cssText(a.icon_style)
			:wikitext(a.icon)
	end
	if lib.isNotEmpty(a.icon_bottom) then
		node_card:tag('span')
			:addClass('card-icon-bottom')
			:cssText(a.icon_bottom_style)
			:wikitext(a.icon_bottom)
	end
	
	local node_image = node_card:tag('span')
		:addClass('card-image-container')
	node_image:tag('span')
		:wikitext(a.image:buildString())
		
	if not a.mini then
		if a.stars then
			local starsImage = RARITY_STARS[a.rarity]
			if starsImage then
				node_card:tag('span')
					:addClass('card-stars')
					:addClass('star-' .. a.rarity)
					:tag('span')
						:wikitext(starsImage)
			end
		end
		if lib.isNotEmpty(a.icon_right) then
			node_card:tag('span')
				:addClass('card-icon-right')
				:cssText(a.icon_right_style)
				:wikitext(a.icon_right)
		end
		if lib.isNotEmpty(a.equipped) then
			node_card:tag('span')
				:addClass('card-equipped')
				:wikitext(a.equipped)
		end
		
		local si_cell = nil
		if lib.isNotEmpty(a.superimposition) then si_cell = SUPERIMPOSITION_TABLE[a.superimposition] end
		if si_cell ~= nil then
			node_card:tag('span')
				:addClass('card-superimposition')
				:addClass('superimposition-' .. si_cell.num)
				:wikitext(si_cell.text)
		end
		if lib.isNotEmpty(a.eidolon) then
			node_card:tag('span')
				:addClass('card-eidolon')
				:wikitext(' E', a.eidolon)
			node_card:addClass('card-with-eidolon')
		end
	
		if (lib.isNotEmpty(a.notice)) then
			node_card:tag('span')
				:addClass('card-notice-label')
				:wikitext(a.notice)
		end
	end
		
	if not a.notext and (not a.mini or a.minitext) then
		local node_text = node_card:tag('span')
			:addClass('card-text')
			:addClass('card-font')
			:wikitext(' ', a.text)
		if a.multiline_text then
			node_text:addClass('multi-line')
		end
	end
	
	if a.show_caption and lib.isNotEmpty(a.caption) then
		local node_caption = node_container:tag('span')
			:addClass('card-caption')
			:wikitext(' ', a.nolink and a.caption or ('[[' .. a.final_link .. '|' .. a.caption .. ']]'))
		setCaptionWidth(node_caption, a.caption_width)
	end
	
	if lib.isNotEmpty(a.note) then
		local node_note = node_container:tag('span')
			:addClass('card-caption')
			:wikitext(a.note)
		setCaptionWidth(node_note, a.caption_width)
	end
	
	if a.uses_deprecated_params then
		node_container:wikitext('[[Category:Pages Using Deprecated Template Parameters]]')
	end

	return node_container
end

-- Processes arguments and returns a corresponding Card element.
function p._main(args, frame)
	local a = p._processArgs(args)
	
	return p._createCard(a)
end

function p.templateData(frame)
	return TemplateData.templateData(CARD_ARGS, {
		description='This template is used to create an item display that is similar to the items in the Inventory in-game. Text can be shown below the item\'s image, background and stars can match the rarity, superimposition, icons, and set can be added.\n\nIf the image name matches an existing Item, then the rarity will automatically be set.\n\nIf the image name matches an existing Character of Light Cone, then the format will automatically change to match that type item.',
		format='inline'
	}, frame)
end

function p.syntax(frame)
	return TemplateData.syntax(CARD_ARGS, frame)
end

return p