Module:Pokemon: Difference between revisions

From Pokemon Revolution Online Wiki
Jump to navigation Jump to search
Cae (talk | contribs)
No edit summary
Cae (talk | contribs)
No edit summary
Line 232: Line 232:


local function infoCell(colors, labelHtml, valueHtml)
local function infoCell(colors, labelHtml, valueHtml)
return '<td style="border: 3px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
return '<td style="border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
'; border-radius: 15px; padding: 5px;">' ..
'; border-radius: 15px; padding: 5px;">' ..
'<p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark .. ';">' .. labelHtml .. '</p>' ..
'<p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark .. ';">' .. labelHtml .. '</p>' ..
'<span style="border-radius: 20px; padding: 6px; border: 4px solid #' .. colors.dark ..
'<span style="border-radius: 12px; padding: 4px 10px; border: 1px solid #' .. colors.dark ..
'; background-color: #FFFFFF;">&nbsp;&nbsp;' .. valueHtml .. '&nbsp;&nbsp;</span></td>'
'; background-color: #FFFFFF;">&nbsp;&nbsp;' .. valueHtml .. '&nbsp;&nbsp;</span></td>'
end
end
Line 241: Line 241:
local function abilityCell(colors, label, ability, widthPercent)
local function abilityCell(colors, label, ability, widthPercent)
if not ability then return '' end
if not ability then return '' end
local style = 'border-radius: 20px; padding: 6px; border: 4px solid #' .. colors.dark ..
local style = 'border-radius: 12px; padding: 4px 10px; border: 1px solid #' .. colors.dark ..
'; background-color: #FFFFFF;'
'; background-color: #FFFFFF;'
if widthPercent then
if widthPercent then
Line 270: Line 270:
local function statValueCell(frame, stat, value)
local function statValueCell(frame, stat, value)
local colors = statColors(frame, stat.template)
local colors = statColors(frame, stat.template)
return '<td style="border: 4px solid #' .. colors.dark .. '; background-color: #' .. colors.base ..
return '<td style="border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.base ..
'; border-radius: 15px; padding: 5px; font-size: 11px;">\'\'\'' .. stat.label .. '\'\'\'<br>' ..
'; border-radius: 12px; padding: 4px; font-size: 11px;">\'\'\'' .. stat.label .. '\'\'\'<br>' ..
value .. '</td>'
value .. '</td>'
end
end
-- One horizontal stat bar, scaled against the stat ceiling of 255.
local function statBarRow(frame, stat, value)
local colors = statColors(frame, stat.template)
local pct = math.floor(value / 255 * 100 + 0.5)
if pct < 2 then pct = 2 end
if pct > 100 then pct = 100 end
return '<tr>' ..
'<td style="text-align: right; padding: 2px 8px 2px 4px; font-weight: bold; font-size: 12px; ' ..
'color: #' .. colors.dark .. '; white-space: nowrap;">' .. stat.label .. '</td>' ..
'<td style="width: 100%; padding: 2px 0;"><div style="background-color: #FFFFFF; border: 1px solid #' ..
colors.dark .. '; border-radius: 8px; height: 13px; overflow: hidden;">' ..
'<div style="width: ' .. pct .. '%; height: 100%; background-color: #' .. colors.base .. ';"></div>' ..
'</div></td>' ..
'<td style="text-align: left; padding: 2px 4px 2px 8px; font-weight: bold; font-size: 12px; ' ..
'width: 34px;">' .. value .. '</td></tr>'
end
local MALE_COLOR = '3B6BF5'
local FEMALE_COLOR = 'F5629C'


local function genderRatioHtml(male)
local function genderRatioHtml(male)
if male == 'Genderless' then
if male == 'Genderless' then
return '<span style="color:#000000; background-color: #FFFFFF; border-radius: 15px; ' ..
return '<span style="display: inline-block; color: #000000; background-color: #FFFFFF; ' ..
'border: 4px solid #000000; padding: 5px;">&nbsp;&nbsp;Genderless&nbsp;&nbsp;</span>'
'border-radius: 12px; border: 1px solid #000000; padding: 3px 12px; font-weight: bold;">' ..
'Genderless</span>'
end
end
-- ExternalData drops cells whose value is '0' (PHP empty('0') is true),
-- ExternalData drops cells whose value is '0' (PHP empty('0') is true),
Line 286: Line 307:
m = nonEmpty(male) and 100 or 0
m = nonEmpty(male) and 100 or 0
end
end
if m >= 100 then
if m < 0 then m = 0 end
return '<span style="color:#0000FF; border-radius: 15px; background-color: #FFFFFF; padding: 5px; ' ..
if m > 100 then m = 100 end
'border: 4px solid #0000FF;">&nbsp;&nbsp;100%&nbsp;&nbsp;</span>'
local f = 100 - m
elseif m <= 0 then
 
return '<span style="color:#FF6060; background-color: #FFFFFF; border-radius: 15px; ' ..
local segments = ''
'border: 4px solid #FF6060; padding: 5px;">&nbsp;&nbsp;100%&nbsp;&nbsp;</span>'
if m > 0 then
else
segments = segments .. '<div style="width: ' .. m .. '%; background-color: #' .. MALE_COLOR .. ';"></div>'
return '<span style="color:#0000FF; border-radius: 15px; background-color: #FFFFFF; padding: 5px; ' ..
end
'border: 4px solid #0000FF;">&nbsp;&nbsp;' .. m .. '%&nbsp;&nbsp;</span>' ..
if f > 0 then
'<span style="color:#FF6060; background-color: #FFFFFF; border-radius: 15px; ' ..
segments = segments .. '<div style="width: ' .. f .. '%; background-color: #' .. FEMALE_COLOR .. ';"></div>'
'border: 4px solid #FF6060; padding: 5px;">&nbsp;&nbsp;' .. (100 - m) .. '%&nbsp;&nbsp;</span>'
end
 
local labels = {}
if m > 0 then
labels[#labels + 1] = '<span style="color: #' .. MALE_COLOR .. '; font-weight: bold;">' .. m .. '% </span>'
end
if f > 0 then
labels[#labels + 1] = '<span style="color: #' .. FEMALE_COLOR .. '; font-weight: bold;">' .. f .. '% </span>'
end
end
return '<div style="width: 85%; margin: 4px auto 0;">' ..
'<div style="display: flex; height: 12px; border-radius: 8px; overflow: hidden; ' ..
'border: 1px solid #555555; background-color: #FFFFFF;">' .. segments .. '</div>' ..
'<div style="margin-top: 4px; font-size: 12px;">' .. table.concat(labels, ' &nbsp;·&nbsp; ') ..
'</div></div>'
end
end


local function categories(data, gen, bstCategory, args)
local function categories(data, gen, bstCategory, args)
local cats = {
local cats = { '[[Category:Pokémon species]]' }
'[[Category:Pokémon species]]',
if gen then
'[[Category:Generation ' .. gen .. ' Pokémon]]',
cats[#cats + 1] = '[[Category:Generation ' .. gen .. ' Pokémon]]'
'[[Category:' .. data.type1 .. '-type Pokémon]]',
end
}
cats[#cats + 1] = '[[Category:' .. data.type1 .. '-type Pokémon]]'
if nonEmpty(data.type2) then
if nonEmpty(data.type2) then
cats[#cats + 1] = '[[Category:' .. data.type2 .. '-type Pokémon]]'
cats[#cats + 1] = '[[Category:' .. data.type2 .. '-type Pokémon]]'
Line 322: Line 356:
local function infoboxHtml(frame, args, data, displayName, width, pageName, includeNav)
local function infoboxHtml(frame, args, data, displayName, width, pageName, includeNav)
local colors = typeColors(frame, data.type1)
local colors = typeColors(frame, data.type1)
local gen = GEN_NUMERALS[tonumber(data.generation)]
-- Regional forms carry values like '1A'/'5A'; use the leading digits.
local gen = GEN_NUMERALS[tonumber(mw.ustring.match(tostring(data.generation or ''), '^%d+') or '')]


-- Types pill(s).
-- Types pill(s).
Line 340: Line 375:


-- Base stats
-- Base stats
local statCells, bst = {}, 0
local statRows, bst = {}, 0
for _, stat in ipairs(STATS) do
for _, stat in ipairs(STATS) do
local value = tonumber(data[stat.key]) or 0
local value = tonumber(data[stat.key]) or 0
bst = bst + value
bst = bst + value
statCells[#statCells + 1] = statValueCell(frame, stat, value)
statRows[#statRows + 1] = statBarRow(frame, stat, value)
end
end
local bstCategory = math.floor(bst / 100 + 0.5) * 100
local bstCategory = math.floor(bst / 100 + 0.5) * 100
Line 369: Line 404:


-- Header: dex number + icon, name.
-- Header: dex number + icon, name.
buf[#buf + 1] = '<tr><td style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
buf[#buf + 1] = '<tr><td style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; border-radius: 15px;"><span style="margin-bottom: 7px; font-weight: bold; color:#' ..
colors.light .. '; border-radius: 15px;"><span style="margin-bottom: 7px; font-weight: bold; color:#' ..
colors.dark .. ';">#' .. padNumber(data.number) .. '</span>&nbsp;&nbsp;&nbsp;[[File:' .. data.number ..
colors.dark .. ';">#' .. padNumber(data.number) .. '</span>&nbsp;&nbsp;&nbsp;[[File:' .. data.number ..
'Icon.png]]</td><td style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
'Icon.png]]</td><td style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; border-radius: 15px;"><span style="margin-bottom: 7px; font-weight: bold; color:#' ..
colors.light .. '; border-radius: 15px;"><span style="margin-bottom: 7px; font-weight: bold; color:#' ..
colors.dark .. ';">' .. displayName .. '</span></td></tr>'
colors.dark .. ';">' .. displayName .. '</span></td></tr>'


-- Artwork.
-- Artwork.
buf[#buf + 1] = '<tr><td colspan="3" style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
buf[#buf + 1] = '<tr><td colspan="3" style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; border-radius: 15px; padding: 5px;"><div style="border-radius: 15px; ' ..
colors.light .. '; border-radius: 15px; padding: 5px;"><div style="border-radius: 15px; ' ..
'text-align: center; padding: 5px; background-color: #FFFFFF;">[[File:' .. data.number .. data.name ..
'text-align: center; padding: 5px; background-color: #FFFFFF;">[[File:' .. data.number .. data.name ..
Line 383: Line 418:


-- Types + Generation.
-- Types + Generation.
buf[#buf + 1] = '<tr><td style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
local genHtml = '—'
if gen then
genHtml = '[[:Category:Generation ' .. gen .. ' Pokémon|<span style="color:#' ..
colors.dark .. ';">' .. gen .. '</span>]]'
end
buf[#buf + 1] = '<tr><td style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; padding: 11px; border-radius: 15px;"><p style="margin-bottom: 7px; font-weight: bold; ' ..
colors.light .. '; padding: 11px; border-radius: 15px;"><p style="margin-bottom: 7px; font-weight: bold; ' ..
'color: #' .. colors.dark .. ';">Types</p><table align="center" style="width: 75%; font-weight: bold; ' ..
'color: #' .. colors.dark .. ';">Types</p><table align="center" style="width: 75%; font-weight: bold; ' ..
'padding: 3px;"><tr>' .. typesHtml .. '</tr></table></td>' ..
'padding: 3px;"><tr>' .. typesHtml .. '</tr></table></td>' ..
'<td style="border: 3px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
'<td style="border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
'; border-radius: 15px;"><p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
'; border-radius: 15px;"><p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
';">Generation</p><span style="border-radius: 20px; padding: 6px; border: 4px solid #' .. colors.dark ..
';">Generation</p><span style="border-radius: 12px; padding: 4px 10px; border: 1px solid #' .. colors.dark ..
'; background-color: #FFFFFF;">&nbsp;&nbsp;[[:Category:Generation ' .. gen .. ' Pokémon|<span style="color:#' ..
'; background-color: #FFFFFF;">&nbsp;&nbsp;' .. genHtml .. '&nbsp;&nbsp;</span></td></tr>'
colors.dark .. ';">' .. gen .. '</span>]]&nbsp;&nbsp;</span></td></tr>'


-- Abilities.
-- Abilities.
buf[#buf + 1] = '<tr><td colspan="6" style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
buf[#buf + 1] = '<tr><td colspan="6" style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; padding: 11px; border-radius: 15px; width: 100%;"><table width="100%">' ..
colors.light .. '; padding: 11px; border-radius: 15px; width: 100%;"><table width="100%">' ..
'<tr><th colspan="6"><p style="margin-bottom: 7px; font-weight: bold; color:#' .. colors.dark ..
'<tr><th colspan="6"><p style="margin-bottom: 7px; font-weight: bold; color:#' .. colors.dark ..
Line 410: Line 449:


-- Base stats.
-- Base stats.
buf[#buf + 1] = '<tr><td colspan="6" style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
buf[#buf + 1] = '<tr><td colspan="6" style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; border-radius: 15px; padding: 5px; width: 100%"><table width="100%">' ..
colors.light .. '; border-radius: 15px; padding: 8px; width: 100%">' ..
'<tr><th colspan="6"><p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
'<table width="100%" style="border-collapse: collapse;">' ..
';">[[Base stats|<span style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
'<tr><th colspan="3"><p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
';">Base stats</span>]]</p></th></tr><tr>' .. table.concat(statCells) .. '</tr>' ..
';">[[Base stats|<span style="color: #' .. colors.dark .. ';">Base stats</span>]]</p></th></tr>' ..
'<tr><td colspan="6"><div style="margin: auto; background-color:#FFFFFF; border: 3px solid #' ..
table.concat(statRows) ..
colors.dark .. '; padding: 6px; border-radius: 25px; margin-bottom: 5px; width: 50%;">' ..
'<tr><td colspan="3" style="text-align: center; padding-top: 6px;">' ..
'Base-stat total: ' .. bst .. '</div></td></tr></table></td></tr>'
'<div style="display: inline-block; background-color: #FFFFFF; border: 1px solid #' .. colors.dark ..
'; padding: 4px 14px; border-radius: 12px;">Base-stat total: ' .. bst .. '</div></td></tr>' ..
'</table></td></tr>'


-- EV yield.
-- EV yield.
buf[#buf + 1] = '<tr><td colspan="6" style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
buf[#buf + 1] = '<tr><td colspan="6" style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; border-radius: 15px; padding: 5px; width: 100%"><table width="100%">' ..
colors.light .. '; border-radius: 15px; padding: 5px; width: 100%"><table width="100%">' ..
'<tr><th colspan="6">[[EVs|<span style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
'<tr><th colspan="6">[[EVs|<span style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
';">EV Yield</span>]]</th></tr><tr>' .. table.concat(evCells) .. '</tr>' ..
';">EV Yield</span>]]</th></tr><tr>' .. table.concat(evCells) .. '</tr>' ..
'<tr><td colspan="6"><div style="margin: auto; background-color:#FFFFFF; border: 3px solid #' ..
'<tr><td colspan="6"><div style="margin: auto; background-color:#FFFFFF; border: 1px solid #' ..
colors.dark .. '; padding: 6px; border-radius: 25px; margin-bottom: 5px; width: 50%;">' ..
colors.dark .. '; padding: 4px 14px; border-radius: 12px; margin-bottom: 5px; width: 50%;">' ..
'EV-yield total: ' .. evTotal .. '</div></td></tr></table></td></tr>'
'EV-yield total: ' .. evTotal .. '</div></td></tr></table></td></tr>'


Line 432: Line 473:
infoCell(colors, '[[Experience System#Base Experience|<span style="color:#' .. colors.dark ..
infoCell(colors, '[[Experience System#Base Experience|<span style="color:#' .. colors.dark ..
';">Base Experience</span>]]', data.exp) ..
';">Base Experience</span>]]', data.exp) ..
'<td style="border: 3px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
'<td style="border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
'; border-radius: 15px; padding: 5px;"><p style="margin-bottom: 7px; font-weight: bold; color: #' ..
'; border-radius: 15px; padding: 5px;"><p style="margin-bottom: 7px; font-weight: bold; color: #' ..
colors.dark .. ';">Gender Ratio</p>&nbsp;&nbsp;' .. genderRatioHtml(data.male) .. '&nbsp;&nbsp;</td>' ..
colors.dark .. ';">Gender Ratio</p>' .. genderRatioHtml(data.male) .. '</td>' ..
'</tr>'
'</tr>'



Revision as of 11:06, 19 July 2026

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

local p = {}

local DATA_SOURCE = 'data'
local DATA_FILE = 'PokemonRawList.csv'
local DATA_FORMAT = 'CSV with header'

local COLUMNS = {
	number = 'Pokedex Number',
	name = 'Name',
	type1 = 'Type1',
	type2 = 'Type2',
	ability1 = 'Ability 1',
	ability2 = 'Ability 2',
	ha = 'Hidden Ability',
	generation = 'Generation',
	hp = 'HP',
	atk = 'Attack',
	def = 'Defense',
	spatk = 'Special Attack',
	spdef = 'Special Defense',
	spd = 'Speed',
	catch = 'Catch Rate',
	evhp = 'EVHP',
	evatk = 'EVATK',
	evdef = 'EVDEF',
	evspa = 'EVSPA',
	evspd = 'EVSPD',
	evsp = 'EVSP',
	male = 'Male Ratio',
	height = 'Height',
	weight = 'Weight',
	npc = 'NPC',
	exp = 'Base Experience',
	rarity = 'Tier',
}

local GEN_NUMERALS = { 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX' }

local STATS = {
	{ key = 'hp', label = 'HP', template = 'HP' },
	{ key = 'atk', label = 'Atk', template = 'Attack' },
	{ key = 'def', label = 'Def', template = 'Defense' },
	{ key = 'spatk', label = 'Sp.Atk', template = 'Special Attack' },
	{ key = 'spdef', label = 'Sp.Def', template = 'Special Defense' },
	{ key = 'spd', label = 'Spd', template = 'Speed' },
}

local EV_KEYS = { 'evhp', 'evatk', 'evdef', 'evspa', 'evspd', 'evsp' }

local DATA_SPEC
do
	local parts = {}
	for key, header in pairs(COLUMNS) do
		parts[#parts + 1] = key .. '=' .. header
	end
	DATA_SPEC = table.concat(parts, ',')
end

local function nonEmpty(s)
	if s == nil then return nil end
	s = mw.text.trim(tostring(s))
	if s == '' then return nil end
	return s
end

local function templateArgs(frame)
	local parent = frame:getParent()
	return (parent and parent.args) or frame.args
end

local function padNumber(n)
	n = tonumber(n) or 0
	return string.format('%03d', n)
end

local function fetchRow(filterColumn, filterValue)
	local ok, values, errors = pcall(function()
		return mw.ext.externalData.getFileData{
			source = DATA_SOURCE,
			['file name'] = DATA_FILE,
			format = DATA_FORMAT,
			filters = filterColumn .. '=' .. filterValue,
			data = DATA_SPEC,
		}
	end)
	if not ok then
		error('Module:Pokemon: External Data call failed (' .. tostring(values) ..
			'). Is Legacy mode enabled (mw.ext.externalData.getFileData)?')
	end
	if not values or values.name == nil then
		return nil
	end
	return values
end

local function byName(name)
	return fetchRow('Name', name)
end

local function byNumber(number)
	return fetchRow('Pokedex Number', number)
end

local PokemonForms = require('Module:PokemonForms')
local FORM_SUFFIXES = PokemonForms.FORM_SUFFIXES
local STRIP_SUFFIXES = PokemonForms.STRIP_SUFFIXES

local allRowsCache = nil

local function loadAllRows()
	if allRowsCache then
		return allRowsCache
	end
	local csvData = mw.ext.externalData.getExternalData{
		url = 'https://wiki.pokemonrevolution.net/index.php?title=Special:GetData/PokemonRawList',
		format = DATA_FORMAT,
		data = COLUMNS,
	}
	allRowsCache = {}
	if csvData then
		for _, row in ipairs(csvData) do
			if nonEmpty(row.name) then
				allRowsCache[row.name] = row
			end
		end
	end
	return allRowsCache
end

local function hyphenSpaceVariant(s)
	if mw.ustring.find(s, '-', 1, true) then
		return (mw.ustring.gsub(s, '%-', ' '))
	end
	return (mw.ustring.gsub(s, ' ', '-'))
end

-- Page/raw-list name -> base name the form candidates are derived from
-- ("Alolan Rattata" -> "Rattata", "Eiscue-Ice Face" -> "Eiscue").
local function formStem(name)
	for _, form in ipairs(FORM_SUFFIXES) do
		local head = form.label .. ' '
		if mw.ustring.sub(name, 1, mw.ustring.len(head)) == head then
			return mw.ustring.sub(name, mw.ustring.len(head) + 1)
		end
	end
	for _, suffix in ipairs(STRIP_SUFFIXES) do
		for _, sep in ipairs({ '-', ' ' }) do
			local tail = sep .. suffix
			if mw.ustring.sub(name, -mw.ustring.len(tail)) == tail then
				return mw.ustring.sub(name, 1, -mw.ustring.len(tail) - 1)
			end
		end
	end
	return name
end

local function rawListFormName(form, stem)
	if form.rawList == 'suffix' then
		return stem .. '-' .. form.suffix
	elseif form.rawList == 'prefix-hyphen' then
		return form.label .. '-' .. stem
	elseif form.rawList == 'suffix-label' then
		return stem .. '-' .. form.label
	elseif form.rawList == 'suffix-space' then
		return stem .. ' ' .. form.label
	end
	return form.label .. ' ' .. stem
end

local function typeColors(frame, pokeType)
	return {
		light = frame:expandTemplate{ title = pokeType .. ' color light' },
		base = frame:expandTemplate{ title = pokeType .. ' color' },
		dark = frame:expandTemplate{ title = pokeType .. ' color dark' },
	}
end

local function statColors(frame, statTemplate)
	return {
		base = frame:expandTemplate{ title = statTemplate .. ' color' },
		dark = frame:expandTemplate{ title = statTemplate .. ' color dark' },
	}
end

local function navTableHtml(frame, data, colors, pageName)
	local number = tonumber(data.number)

	local prevCell = ''
	if number and number > 1 then
		local prev = byNumber(number - 1)
		if prev then
			prevCell = '&nbsp;[[File:' .. (number - 1) .. 'Icon.png|' .. prev.name .. '|link=' .. prev.name ..
				']]&nbsp;&nbsp;&nbsp;#' .. padNumber(prev.number)
		end
	end

	local nextCell = ''
	if number then
		local nxt = byNumber(number + 1)
		if nxt then
			nextCell = '#' .. padNumber(nxt.number) .. '&nbsp;&nbsp;&nbsp;[[File:' .. (number + 1) ..
				'Icon.png|' .. nxt.name .. '|link=' .. nxt.name .. ']]'
		end
	end

	return '<table style="margin-left: auto; margin-right: auto; margin-bottom: 15px; height: 60px; ' ..
		'border: 4px #' .. colors.dark .. ' solid; font-weight: bold; width: 65%; color: #' .. colors.dark ..
		'; border-radius: 20px; padding: 3px; background: #' .. colors.base .. ';">' ..
		'<tr>' ..
		'<td style="text-align: left; width: 36%; background: #' .. colors.light ..
		'; border-radius: 15px 1px 1px 15px; border: 1px solid #' .. colors.dark .. ';">' .. prevCell .. '</td>' ..
		'<td style="text-align: center; width: 28%; background: #' .. colors.light ..
		'; border: 1px solid #' .. colors.dark .. ';">#' .. padNumber(number) .. '&nbsp;&nbsp;-&nbsp;&nbsp;' ..
		pageName .. '</td>' ..
		'<td style="text-align: right; width: 36%; padding-right: 10px; background: #' .. colors.light ..
		'; border: 1px solid #' .. colors.dark .. '; border-radius: 1px 15px 15px 1px;">' .. nextCell .. '</td>' ..
		'</tr></table>'
end

function p.navTable(frame)
	local args = templateArgs(frame)
	local pageName = mw.title.getCurrentTitle().text
	local name = nonEmpty(args.Name) or pageName

	local data = byName(name)
	if not data then
		return '<strong class="error">PokeNavTable: no data found for "' .. name .. '"</strong>'
	end

	return navTableHtml(frame, data, typeColors(frame, data.type1), pageName)
end

local function infoCell(colors, labelHtml, valueHtml)
	return '<td style="border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
		'; border-radius: 15px; padding: 5px;">' ..
		'<p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark .. ';">' .. labelHtml .. '</p>' ..
		'<span style="border-radius: 12px; padding: 4px 10px; border: 1px solid #' .. colors.dark ..
		'; background-color: #FFFFFF;">&nbsp;&nbsp;' .. valueHtml .. '&nbsp;&nbsp;</span></td>'
end

local function abilityCell(colors, label, ability, widthPercent)
	if not ability then return '' end
	local style = 'border-radius: 12px; padding: 4px 10px; border: 1px solid #' .. colors.dark ..
		'; background-color: #FFFFFF;'
	if widthPercent then
		style = 'width: ' .. widthPercent .. '%; ' .. style
	end
	return '<td style="' .. style .. '"><b>' .. label .. '</b><br>[[' .. ability .. '_(ability)|' .. ability ..
		']]</td>'
end

local function abilitiesRow(colors, data)
	local entries = {
		{ 'Ability 1', nonEmpty(data.ability1) },
		{ 'Ability 2', nonEmpty(data.ability2) },
		{ 'Hidden Ability', nonEmpty(data.ha) },
	}
	local present = {}
	for _, entry in ipairs(entries) do
		if entry[2] then present[#present + 1] = entry end
	end
	local widthPercent = (#present == 2) and 50 or nil
	local cells = {}
	for _, entry in ipairs(present) do
		cells[#cells + 1] = abilityCell(colors, entry[1], entry[2], widthPercent)
	end
	return table.concat(cells)
end

local function statValueCell(frame, stat, value)
	local colors = statColors(frame, stat.template)
	return '<td style="border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.base ..
		'; border-radius: 12px; padding: 4px; font-size: 11px;">\'\'\'' .. stat.label .. '\'\'\'<br>' ..
		value .. '</td>'
end

-- One horizontal stat bar, scaled against the stat ceiling of 255.
local function statBarRow(frame, stat, value)
	local colors = statColors(frame, stat.template)
	local pct = math.floor(value / 255 * 100 + 0.5)
	if pct < 2 then pct = 2 end
	if pct > 100 then pct = 100 end
	return '<tr>' ..
		'<td style="text-align: right; padding: 2px 8px 2px 4px; font-weight: bold; font-size: 12px; ' ..
		'color: #' .. colors.dark .. '; white-space: nowrap;">' .. stat.label .. '</td>' ..
		'<td style="width: 100%; padding: 2px 0;"><div style="background-color: #FFFFFF; border: 1px solid #' ..
		colors.dark .. '; border-radius: 8px; height: 13px; overflow: hidden;">' ..
		'<div style="width: ' .. pct .. '%; height: 100%; background-color: #' .. colors.base .. ';"></div>' ..
		'</div></td>' ..
		'<td style="text-align: left; padding: 2px 4px 2px 8px; font-weight: bold; font-size: 12px; ' ..
		'width: 34px;">' .. value .. '</td></tr>'
end

local MALE_COLOR = '3B6BF5'
local FEMALE_COLOR = 'F5629C'

local function genderRatioHtml(male)
	if male == 'Genderless' then
		return '<span style="display: inline-block; color: #000000; background-color: #FFFFFF; ' ..
			'border-radius: 12px; border: 1px solid #000000; padding: 3px 12px; font-weight: bold;">' ..
			'Genderless</span>'
	end
	-- ExternalData drops cells whose value is '0' (PHP empty('0') is true),
	-- so a nil/blank ratio on an otherwise-complete row means 0% male.
	local m = tonumber(male)
	if m == nil then
		m = nonEmpty(male) and 100 or 0
	end
	if m < 0 then m = 0 end
	if m > 100 then m = 100 end
	local f = 100 - m

	local segments = ''
	if m > 0 then
		segments = segments .. '<div style="width: ' .. m .. '%; background-color: #' .. MALE_COLOR .. ';"></div>'
	end
	if f > 0 then
		segments = segments .. '<div style="width: ' .. f .. '%; background-color: #' .. FEMALE_COLOR .. ';"></div>'
	end

	local labels = {}
	if m > 0 then
		labels[#labels + 1] = '<span style="color: #' .. MALE_COLOR .. '; font-weight: bold;">' .. m .. '% ♂</span>'
	end
	if f > 0 then
		labels[#labels + 1] = '<span style="color: #' .. FEMALE_COLOR .. '; font-weight: bold;">' .. f .. '% ♀</span>'
	end

	return '<div style="width: 85%; margin: 4px auto 0;">' ..
		'<div style="display: flex; height: 12px; border-radius: 8px; overflow: hidden; ' ..
		'border: 1px solid #555555; background-color: #FFFFFF;">' .. segments .. '</div>' ..
		'<div style="margin-top: 4px; font-size: 12px;">' .. table.concat(labels, ' &nbsp;·&nbsp; ') ..
		'</div></div>'
end

local function categories(data, gen, bstCategory, args)
	local cats = { '[[Category:Pokémon species]]' }
	if gen then
		cats[#cats + 1] = '[[Category:Generation ' .. gen .. ' Pokémon]]'
	end
	cats[#cats + 1] = '[[Category:' .. data.type1 .. '-type Pokémon]]'
	if nonEmpty(data.type2) then
		cats[#cats + 1] = '[[Category:' .. data.type2 .. '-type Pokémon]]'
	end
	cats[#cats + 1] = '[[Category:' .. data.rarity .. ' Pokémon]]'
	if nonEmpty(data.npc) then
		cats[#cats + 1] = '[[Category:NPC-obtainable Pokémon]]'
	end
	if not nonEmpty(args.EvolvesFrom) then
		cats[#cats + 1] = '[[Category:Base-stage Pokémon]]'
	end
	cats[#cats + 1] = '[[Category:' .. bstCategory .. '-plus-BST Pokémon]]'
	return table.concat(cats, ' ')
end

local function infoboxHtml(frame, args, data, displayName, width, pageName, includeNav)
	local colors = typeColors(frame, data.type1)
	-- Regional forms carry values like '1A'/'5A'; use the leading digits.
	local gen = GEN_NUMERALS[tonumber(mw.ustring.match(tostring(data.generation or ''), '^%d+') or '')]

	-- Types pill(s).
	local typesHtml
	if nonEmpty(data.type2) then
		local type2Colors = typeColors(frame, data.type2)
		typesHtml =
			'<td style="width: 50%; background-color: #' .. colors.dark .. '; border-radius: 25px 1px 1px 25px;">' ..
			'[[' .. data.type1 .. '_(type)|<span style="color: #FFFFFF;">' .. data.type1 .. '</span>]]</td>' ..
			'<td style="width: 50%; background-color: #' .. type2Colors.dark .. '; border-radius: 1px 25px 25px 1px;">' ..
			'[[' .. data.type2 .. '_(type)|<span style="color: #FFFFFF;">' .. data.type2 .. '</span>]]</td>'
	else
		typesHtml =
			'<td style="width:100%; background-color: #' .. colors.dark .. '; border-radius: 25px;">' ..
			'[[' .. data.type1 .. '_(type)|<span style="color: #FFFFFF;">' .. data.type1 .. '</span>]]</td>'
	end

	-- Base stats
	local statRows, bst = {}, 0
	for _, stat in ipairs(STATS) do
		local value = tonumber(data[stat.key]) or 0
		bst = bst + value
		statRows[#statRows + 1] = statBarRow(frame, stat, value)
	end
	local bstCategory = math.floor(bst / 100 + 0.5) * 100

	-- EV yield.
	local evCells, evTotal = {}, 0
	for i, stat in ipairs(STATS) do
		local ev = tonumber(data[EV_KEYS[i]]) or 0
		evTotal = evTotal + ev
		evCells[#evCells + 1] = statValueCell(frame, stat, ev)
	end

	-- Catch rate.
	local catchRaw = tonumber(data.catch) or 0
	local catchPercent = string.format('%.2f', catchRaw / 255 / 3 * 100)

	local buf = {}
	if includeNav then
		buf[#buf + 1] = navTableHtml(frame, data, colors, pageName)
	end
	buf[#buf + 1] = '<table align="right" class="informational-box" cellpadding="4" cellspacing="4" ' ..
		'style="width: ' .. width .. '%; border: 4px solid #' .. colors.dark .. '; background-color: #' ..
		colors.base .. '; padding: 4px; border-radius: 15px;">'

	-- Header: dex number + icon, name.
	buf[#buf + 1] = '<tr><td style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
		colors.light .. '; border-radius: 15px;"><span style="margin-bottom: 7px; font-weight: bold; color:#' ..
		colors.dark .. ';">#' .. padNumber(data.number) .. '</span>&nbsp;&nbsp;&nbsp;[[File:' .. data.number ..
		'Icon.png]]</td><td style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
		colors.light .. '; border-radius: 15px;"><span style="margin-bottom: 7px; font-weight: bold; color:#' ..
		colors.dark .. ';">' .. displayName .. '</span></td></tr>'

	-- Artwork.
	buf[#buf + 1] = '<tr><td colspan="3" style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
		colors.light .. '; border-radius: 15px; padding: 5px;"><div style="border-radius: 15px; ' ..
		'text-align: center; padding: 5px; background-color: #FFFFFF;">[[File:' .. data.number .. data.name ..
		'.png|150px]]</div></td></tr>'

	-- Types + Generation.
	local genHtml = '—'
	if gen then
		genHtml = '[[:Category:Generation ' .. gen .. ' Pokémon|<span style="color:#' ..
			colors.dark .. ';">' .. gen .. '</span>]]'
	end
	buf[#buf + 1] = '<tr><td style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
		colors.light .. '; padding: 11px; border-radius: 15px;"><p style="margin-bottom: 7px; font-weight: bold; ' ..
		'color: #' .. colors.dark .. ';">Types</p><table align="center" style="width: 75%; font-weight: bold; ' ..
		'padding: 3px;"><tr>' .. typesHtml .. '</tr></table></td>' ..
		'<td style="border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
		'; border-radius: 15px;"><p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
		';">Generation</p><span style="border-radius: 12px; padding: 4px 10px; border: 1px solid #' .. colors.dark ..
		'; background-color: #FFFFFF;">&nbsp;&nbsp;' .. genHtml .. '&nbsp;&nbsp;</span></td></tr>'

	-- Abilities.
	buf[#buf + 1] = '<tr><td colspan="6" style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
		colors.light .. '; padding: 11px; border-radius: 15px; width: 100%;"><table width="100%">' ..
		'<tr><th colspan="6"><p style="margin-bottom: 7px; font-weight: bold; color:#' .. colors.dark ..
		';">[[Abilities|<span style="color:#' .. colors.dark .. ';">Abilities</span>]]</p></th></tr>' ..
		'<tr>' .. abilitiesRow(colors, data) .. '</tr></table></td></tr>'

	-- Rarity + Catch rate.
	buf[#buf + 1] = '<tr>' ..
		infoCell(colors, '[[List of Pokémon by Rarity Tier|<span style="color:#' .. colors.dark ..
			';">Rarity Tier</span>]]', '[[:Category:' .. data.rarity .. ' Pokémon|<span style="color:#' ..
			colors.dark .. ';">' .. data.rarity .. '</span>]]') ..
		infoCell(colors, '[[Catch Rates|<span style="color:#' .. colors.dark .. ';">Catch rate</span>]]',
			data.catch .. '/255 (' .. catchPercent .. '%)') ..
		'</tr>'

	-- Base stats.
	buf[#buf + 1] = '<tr><td colspan="6" style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
		colors.light .. '; border-radius: 15px; padding: 8px; width: 100%">' ..
		'<table width="100%" style="border-collapse: collapse;">' ..
		'<tr><th colspan="3"><p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
		';">[[Base stats|<span style="color: #' .. colors.dark .. ';">Base stats</span>]]</p></th></tr>' ..
		table.concat(statRows) ..
		'<tr><td colspan="3" style="text-align: center; padding-top: 6px;">' ..
		'<div style="display: inline-block; background-color: #FFFFFF; border: 1px solid #' .. colors.dark ..
		'; padding: 4px 14px; border-radius: 12px;">Base-stat total: ' .. bst .. '</div></td></tr>' ..
		'</table></td></tr>'

	-- EV yield.
	buf[#buf + 1] = '<tr><td colspan="6" style="border: 1px solid #' .. colors.dark .. '; background-color: #' ..
		colors.light .. '; border-radius: 15px; padding: 5px; width: 100%"><table width="100%">' ..
		'<tr><th colspan="6">[[EVs|<span style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
		';">EV Yield</span>]]</th></tr><tr>' .. table.concat(evCells) .. '</tr>' ..
		'<tr><td colspan="6"><div style="margin: auto; background-color:#FFFFFF; border: 1px solid #' ..
		colors.dark .. '; padding: 4px 14px; border-radius: 12px; margin-bottom: 5px; width: 50%;">' ..
		'EV-yield total: ' .. evTotal .. '</div></td></tr></table></td></tr>'

	-- Base experience + Gender ratio.
	buf[#buf + 1] = '<tr>' ..
		infoCell(colors, '[[Experience System#Base Experience|<span style="color:#' .. colors.dark ..
			';">Base Experience</span>]]', data.exp) ..
		'<td style="border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
		'; border-radius: 15px; padding: 5px;"><p style="margin-bottom: 7px; font-weight: bold; color: #' ..
		colors.dark .. ';">Gender Ratio</p>' .. genderRatioHtml(data.male) .. '</td>' ..
		'</tr>'

	-- Height + Weight.
	buf[#buf + 1] = '<tr>' ..
		infoCell(colors, 'Height', data.height .. 'm') ..
		infoCell(colors, 'Weight', data.weight .. 'kg') ..
		'</tr>'

	buf[#buf + 1] = '</table>'
	buf[#buf + 1] = categories(data, gen, bstCategory, args)

	return table.concat(buf)
end

function p.infobox(frame)
	local args = templateArgs(frame)
	local pageName = mw.title.getCurrentTitle().text
	local name = nonEmpty(args.Name) or pageName
	local width = nonEmpty(args.Width) or '45'

	local data = byName(name)
	if not data then
		return '<strong class="error">PokemonInfo: no data found for "' .. name .. '"</strong>'
	end

	return infoboxHtml(frame, args, data, name, width, pageName, true)
end

-- Temporary diagnostic: {{#invoke:Pokemon|debugForms|Name=Meowth}}
function p.debugForms(frame)
	local args = templateArgs(frame)
	local name = nonEmpty(args.Name) or mw.title.getCurrentTitle().text
	local rows = loadAllRows()
	local count = 0
	for _ in pairs(rows) do count = count + 1 end
	local stem = formStem(name)
	local out = { 'name=' .. name, 'stem=' .. stem, 'rows loaded=' .. count }
	for _, form in ipairs(FORM_SUFFIXES) do
		local candidate = rawListFormName(form, stem)
		if rows[candidate] or rows[hyphenSpaceVariant(candidate)] then
			out[#out + 1] = 'form found: ' .. form.label .. ' -> ' .. candidate
		end
	end
	return table.concat(out, '; ')
end

-- Like infobox, but detects alternate forms via Module:PokemonForms and, when
-- any exist, renders one nav table plus a {{TabbedInfoBoxes}} block with one
-- infobox per form. Falls back to the plain single infobox otherwise.
function p.infoboxes(frame)
	local args = templateArgs(frame)
	local pageName = mw.title.getCurrentTitle().text
	local name = nonEmpty(args.Name) or pageName

	local rows = loadAllRows()
	local function rowFor(n)
		return rows[n] or rows[hyphenSpaceVariant(n)]
	end

	local stem = formStem(name)
	local baseData = rowFor(stem) or rowFor(name)
	if not baseData then
		return '<strong class="error">PokemonInfo: no data found for "' .. name .. '"</strong>'
	end
	local baseName = baseData.name

	local forms = {}
	local seen = { [baseName] = true }
	for _, form in ipairs(FORM_SUFFIXES) do
		local row = rowFor(rawListFormName(form, stem))
		if row and not seen[row.name] then
			seen[row.name] = true
			forms[#forms + 1] = { label = form.label, data = row, baseLabel = form.baseLabel }
		end
	end

	if #forms == 0 then
		local width = nonEmpty(args.Width) or '45'
		return infoboxHtml(frame, args, baseData, baseName, width, pageName, true)
	end

	local baseLabel = 'Normal'
	for _, f in ipairs(forms) do
		if f.baseLabel then
			baseLabel = f.baseLabel
			break
		end
	end

	local tabWidth = nonEmpty(args.Width) or '100'
	local tabHeaders = '{{Tabs:MultiformTab|Form=' .. baseLabel .. '}}'
	local infoBoxes = '{{Tabs:MultiformTable|Table=' ..
		infoboxHtml(frame, args, baseData, baseName, tabWidth, pageName, false) .. '}}'
	for _, f in ipairs(forms) do
		tabHeaders = tabHeaders .. '{{Tabs:MultiformTab|Form=' .. f.label .. '}}'
		infoBoxes = infoBoxes .. '{{Tabs:MultiformTable|Form=' .. f.label .. '|Table=' ..
			infoboxHtml(frame, args, f.data, f.data.name, tabWidth, pageName, false) .. '}}'
	end

	return navTableHtml(frame, baseData, typeColors(frame, baseData.type1), pageName) ..
		frame:preprocess('{{TabbedInfoBoxes|TabHeaders=' .. tabHeaders .. '|InfoBoxes=\n' ..
			infoBoxes .. '\n}}')
end

return p