Module:PokemonForms: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
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 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 PokemonForms = require('Module:PokemonForms') | |||
local FORM_SUFFIXES = PokemonForms.FORM_SUFFIXES | |||
local STRIP_SUFFIXES = PokemonForms.STRIP_SUFFIXES | |||
local MEGA_VARIANTS = PokemonForms.MEGA_VARIANTS or { '', ' X', ' Y' } | |||
-- The single data fetch: the whole raw list is loaded once per page render | |||
-- and every lookup (by name, by number, form probing) reads from it. | |||
local allRowsCache = nil | |||
local numberIndexCache = 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 = {} | |||
numberIndexCache = {} | |||
if csvData then | |||
for _, row in ipairs(csvData) do | |||
if nonEmpty(row.name) then | |||
allRowsCache[row.name] = row | |||
local num = nonEmpty(row.number) | |||
-- First occurrence wins so base forms beat Megas sharing a number. | |||
if num and numberIndexCache[num] == nil then | |||
numberIndexCache[num] = row | |||
end | |||
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 | |||
local function byName(name) | |||
local rows = loadAllRows() | |||
return rows[name] or rows[hyphenSpaceVariant(name)] | |||
end | |||
local function byNumber(number) | |||
loadAllRows() | |||
return numberIndexCache[tostring(number)] | |||
end | |||
local MEGA_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', | |||
evhp = 'EVHP', | |||
evatk = 'EVATK', | |||
evdef = 'EVDEF', | |||
evspa = 'EVSPA', | |||
evspd = 'EVSPD', | |||
evsp = 'EVSP', | |||
height = 'Height', | |||
weight = 'Weight', | |||
stone = 'Stone', | |||
} | } | ||
p. | local MEGA_SPEC | ||
do | |||
local parts = {} | |||
for key, header in pairs(MEGA_COLUMNS) do | |||
parts[#parts + 1] = key .. '=' .. header | |||
end | |||
MEGA_SPEC = table.concat(parts, ',') | |||
end | |||
local megaCache = {} | |||
local function megaByName(name) | |||
if megaCache[name] == nil then | |||
local ok, values = pcall(function() | |||
return mw.ext.externalData.getFileData{ | |||
source = 'data', | |||
['file name'] = 'RawMegaPokemonList.csv', | |||
format = DATA_FORMAT, | |||
filters = 'Name=' .. name, | |||
data = MEGA_SPEC, | |||
} | |||
end) | |||
if ok and values and values.name ~= nil then | |||
megaCache[name] = values | |||
else | |||
megaCache[name] = false | |||
end | |||
end | |||
return megaCache[name] or nil | |||
end | |||
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 statColorCache = {} | |||
local function statColors(frame, statTemplate) | |||
if not statColorCache[statTemplate] then | |||
statColorCache[statTemplate] = { | |||
base = frame:expandTemplate{ title = statTemplate .. ' color' }, | |||
dark = frame:expandTemplate{ title = statTemplate .. ' color dark' }, | |||
} | |||
end | |||
return statColorCache[statTemplate] | |||
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 = ' [[File:' .. prev.number .. 'Icon.png|' .. prev.name .. '|link=' .. prev.name .. | |||
']] #' .. padNumber(prev.number) .. ' [[' .. prev.name .. ']]' | |||
end | |||
end | |||
local nextCell = '' | |||
if number then | |||
local nxt = byNumber(number + 1) | |||
if nxt then | |||
nextCell = '[[' .. nxt.name .. ']] #' .. padNumber(nxt.number) .. ' [[File:' .. | |||
nxt.number .. '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) .. ' - ' .. | |||
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 colspan="3" style="width: 50%; 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;"> ' .. valueHtml .. ' </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 > 0) and string.format('%.2f', 100 / #present) 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="width: 16.66%; border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.base .. | |||
'; border-radius: 12px; padding: 4px; font-size: 11px;">\'\'\'' .. stat.label .. '\'\'\'<br>' .. | |||
value .. '</td>' | |||
end | |||
local function statBarRow(frame, stat, value) | |||
local labelColors = 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 | |||
local hue = math.floor(math.max(0, math.min(180, value * 180 / 255)) + 0.5) | |||
local fill = 'hsl(' .. hue .. ', 85%, 45%)' | |||
return '<tr>' .. | |||
'<td style="text-align: right; padding: 2px 8px 2px 4px; font-weight: bold; font-size: 12px; ' .. | |||
'color: #' .. labelColors.dark .. '; white-space: nowrap;">' .. stat.label .. '</td>' .. | |||
'<td style="width: 100%; padding: 2px 0;"><div style="background-color: #FFFFFF; border: 1px solid ' .. | |||
'#999999; border-radius: 8px; height: 13px; overflow: hidden;">' .. | |||
'<div style="width: ' .. pct .. '%; height: 100%; background-color: ' .. fill .. ';"></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 | |||
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, ' · ') .. | |||
'</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 colspan="3" style="width: 50%; 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> [[File:' .. data.number .. | |||
'Icon.png]]</td><td colspan="3" style="width: 50%; 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="6" 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 colspan="3" style="width: 50%; 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 colspan="3" style="width: 50%; 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;"> ' .. genHtml .. ' </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 colspan="3" style="width: 50%; 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 | |||
local function megaInfoboxHtml(frame, data, width) | |||
local colors = typeColors(frame, data.type1) | |||
local gen = GEN_NUMERALS[tonumber(mw.ustring.match(tostring(data.generation or ''), '^%d+') or '')] | |||
local displayName = 'Mega ' .. (data.name or '') | |||
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 | |||
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 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 | |||
local buf = {} | |||
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: name across the whole top line. | |||
buf[#buf + 1] = '<tr><td colspan="6" 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="6" 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 or '') .. (data.name or '') .. | |||
'-Mega.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 colspan="3" style="width: 50%; 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 colspan="3" style="width: 50%; 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;"> ' .. genHtml .. ' </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>' | |||
-- Mega Stone, linked to its quest page ("Venusaurite (quest)"). | |||
local stone = nonEmpty(data.stone) | |||
local stoneHtml = '—' | |||
if stone then | |||
stoneHtml = '[[' .. stone .. ' (quest)|<span style="color:#' .. colors.dark .. ';">' .. stone .. '</span>]]' | |||
end | |||
buf[#buf + 1] = '<tr><td colspan="6" 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 .. ';">Mega Stone</p>' .. | |||
'<span style="border-radius: 12px; padding: 4px 10px; border: 1px solid #' .. colors.dark .. | |||
'; background-color: #FFFFFF;"> ' .. stoneHtml .. ' </span></td></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>' | |||
-- Height + Weight. | |||
buf[#buf + 1] = '<tr>' .. | |||
infoCell(colors, 'Height', (data.height or '—') .. 'm') .. | |||
infoCell(colors, 'Weight', (data.weight or '—') .. 'kg') .. | |||
'</tr>' | |||
buf[#buf + 1] = '</table>' | |||
buf[#buf + 1] = '[[Category:Mega-Evolvable Pokémon]]' | |||
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 | |||
frame:callParserFunction('#vardefine', 'type', data.type1 or 'Normal') | |||
return infoboxHtml(frame, args, data, name, width, pageName, true) | |||
end | |||
function p.primaryType(frame) | |||
local args = templateArgs(frame) | |||
local name = nonEmpty(args.Name) or mw.title.getCurrentTitle().text | |||
local data = byName(name) or byName(formStem(name)) | |||
return (data and nonEmpty(data.type1)) or 'Normal' | |||
end | |||
local function requestedName(frame) | |||
local parent = frame:getParent() | |||
return nonEmpty(frame.args.Name) | |||
or (parent and nonEmpty(parent.args.Name)) | |||
or mw.title.getCurrentTitle().text | |||
end | |||
local participantsCache = {} | |||
local function computeParticipants(frame, name) | |||
if participantsCache[name] ~= nil then | |||
return participantsCache[name] or nil | |||
end | |||
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 | |||
participantsCache[name] = false | |||
return nil | |||
end | |||
local baseDexNumber = mw.ustring.match(tostring(baseData.number or ''), '^%d+') | |||
local parts = { { data = baseData, label = 'Normal' } } | |||
local seen = { [baseData.name] = true } | |||
for _, form in ipairs(FORM_SUFFIXES) do | |||
local row = rowFor(rawListFormName(form, stem)) | |||
-- A candidate must share the same base dex number to be a real form of | |||
-- this Pokémon. Without this check, stripping "Nidoran Male" down to | |||
-- the stem "Nidoran" (done because "Male" is a real form suffix for | |||
-- Indeedee/Meowstic-style species) lets "Nidoran" + " Female" happen to | |||
-- match the raw list's "Nidoran Female" row — a wholly different | |||
-- species (dex #29 vs Male's #32), not this one's gender form. | |||
local rowDexNumber = row and mw.ustring.match(tostring(row.number or ''), '^%d+') | |||
if row and not seen[row.name] and rowDexNumber == baseDexNumber then | |||
seen[row.name] = true | |||
parts[#parts + 1] = { data = row, label = form.label } | |||
if form.baseLabel then | |||
parts[1].label = form.baseLabel | |||
end | |||
end | |||
end | |||
for _, variant in ipairs(MEGA_VARIANTS) do | |||
local row = megaByName(stem .. variant) | |||
if row then | |||
parts[#parts + 1] = { data = row, label = 'Mega' .. variant, mega = true } | |||
end | |||
end | |||
participantsCache[name] = parts | |||
return parts | |||
end | |||
function p.formLabel(frame) | |||
local idx = tonumber(frame.args[1]) or 0 | |||
local parts = computeParticipants(frame, requestedName(frame)) | |||
if not parts then | |||
return '' | |||
end | |||
local part = parts[idx + 1] | |||
return part and part.label or '' | |||
end | |||
p. | function p.rawName(frame) | ||
local name = requestedName(frame) | |||
local parts = computeParticipants(frame, name) | |||
return (parts and parts[1].data.name) or name | |||
end | |||
p. | function p.formBox(frame) | ||
local idx = tonumber(frame.args[1]) or 0 | |||
local width = nonEmpty(frame.args.Width) or '100' | |||
local name = requestedName(frame) | |||
local parts = computeParticipants(frame, name) | |||
if not parts then | |||
return '<strong class="error">PokemonInfo: no data found for "' .. name .. '"</strong>' | |||
end | |||
local part = parts[idx + 1] | |||
if not part then | |||
return '' | |||
end | |||
if part.mega then | |||
return megaInfoboxHtml(frame, part.data, width) | |||
end | |||
local parent = frame:getParent() | |||
local args = (parent and parent.args) or frame.args | |||
return infoboxHtml(frame, args, part.data, part.data.name, width, mw.title.getCurrentTitle().text, false) | |||
end | end | ||
return p | return p | ||
Revision as of 16:06, 30 July 2026
Documentation for this module may be created at Module:PokemonForms/doc
local p = {}
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 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 PokemonForms = require('Module:PokemonForms')
local FORM_SUFFIXES = PokemonForms.FORM_SUFFIXES
local STRIP_SUFFIXES = PokemonForms.STRIP_SUFFIXES
local MEGA_VARIANTS = PokemonForms.MEGA_VARIANTS or { '', ' X', ' Y' }
-- The single data fetch: the whole raw list is loaded once per page render
-- and every lookup (by name, by number, form probing) reads from it.
local allRowsCache = nil
local numberIndexCache = 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 = {}
numberIndexCache = {}
if csvData then
for _, row in ipairs(csvData) do
if nonEmpty(row.name) then
allRowsCache[row.name] = row
local num = nonEmpty(row.number)
-- First occurrence wins so base forms beat Megas sharing a number.
if num and numberIndexCache[num] == nil then
numberIndexCache[num] = row
end
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
local function byName(name)
local rows = loadAllRows()
return rows[name] or rows[hyphenSpaceVariant(name)]
end
local function byNumber(number)
loadAllRows()
return numberIndexCache[tostring(number)]
end
local MEGA_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',
evhp = 'EVHP',
evatk = 'EVATK',
evdef = 'EVDEF',
evspa = 'EVSPA',
evspd = 'EVSPD',
evsp = 'EVSP',
height = 'Height',
weight = 'Weight',
stone = 'Stone',
}
local MEGA_SPEC
do
local parts = {}
for key, header in pairs(MEGA_COLUMNS) do
parts[#parts + 1] = key .. '=' .. header
end
MEGA_SPEC = table.concat(parts, ',')
end
local megaCache = {}
local function megaByName(name)
if megaCache[name] == nil then
local ok, values = pcall(function()
return mw.ext.externalData.getFileData{
source = 'data',
['file name'] = 'RawMegaPokemonList.csv',
format = DATA_FORMAT,
filters = 'Name=' .. name,
data = MEGA_SPEC,
}
end)
if ok and values and values.name ~= nil then
megaCache[name] = values
else
megaCache[name] = false
end
end
return megaCache[name] or nil
end
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 statColorCache = {}
local function statColors(frame, statTemplate)
if not statColorCache[statTemplate] then
statColorCache[statTemplate] = {
base = frame:expandTemplate{ title = statTemplate .. ' color' },
dark = frame:expandTemplate{ title = statTemplate .. ' color dark' },
}
end
return statColorCache[statTemplate]
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 = ' [[File:' .. prev.number .. 'Icon.png|' .. prev.name .. '|link=' .. prev.name ..
']] #' .. padNumber(prev.number) .. ' [[' .. prev.name .. ']]'
end
end
local nextCell = ''
if number then
local nxt = byNumber(number + 1)
if nxt then
nextCell = '[[' .. nxt.name .. ']] #' .. padNumber(nxt.number) .. ' [[File:' ..
nxt.number .. '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) .. ' - ' ..
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 colspan="3" style="width: 50%; 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;"> ' .. valueHtml .. ' </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 > 0) and string.format('%.2f', 100 / #present) 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="width: 16.66%; border: 1px solid #' .. colors.dark .. '; background-color: #' .. colors.base ..
'; border-radius: 12px; padding: 4px; font-size: 11px;">\'\'\'' .. stat.label .. '\'\'\'<br>' ..
value .. '</td>'
end
local function statBarRow(frame, stat, value)
local labelColors = 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
local hue = math.floor(math.max(0, math.min(180, value * 180 / 255)) + 0.5)
local fill = 'hsl(' .. hue .. ', 85%, 45%)'
return '<tr>' ..
'<td style="text-align: right; padding: 2px 8px 2px 4px; font-weight: bold; font-size: 12px; ' ..
'color: #' .. labelColors.dark .. '; white-space: nowrap;">' .. stat.label .. '</td>' ..
'<td style="width: 100%; padding: 2px 0;"><div style="background-color: #FFFFFF; border: 1px solid ' ..
'#999999; border-radius: 8px; height: 13px; overflow: hidden;">' ..
'<div style="width: ' .. pct .. '%; height: 100%; background-color: ' .. fill .. ';"></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
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, ' · ') ..
'</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 colspan="3" style="width: 50%; 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> [[File:' .. data.number ..
'Icon.png]]</td><td colspan="3" style="width: 50%; 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="6" 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 colspan="3" style="width: 50%; 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 colspan="3" style="width: 50%; 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;"> ' .. genHtml .. ' </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 colspan="3" style="width: 50%; 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
local function megaInfoboxHtml(frame, data, width)
local colors = typeColors(frame, data.type1)
local gen = GEN_NUMERALS[tonumber(mw.ustring.match(tostring(data.generation or ''), '^%d+') or '')]
local displayName = 'Mega ' .. (data.name or '')
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
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 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
local buf = {}
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: name across the whole top line.
buf[#buf + 1] = '<tr><td colspan="6" 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="6" 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 or '') .. (data.name or '') ..
'-Mega.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 colspan="3" style="width: 50%; 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 colspan="3" style="width: 50%; 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;"> ' .. genHtml .. ' </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>'
-- Mega Stone, linked to its quest page ("Venusaurite (quest)").
local stone = nonEmpty(data.stone)
local stoneHtml = '—'
if stone then
stoneHtml = '[[' .. stone .. ' (quest)|<span style="color:#' .. colors.dark .. ';">' .. stone .. '</span>]]'
end
buf[#buf + 1] = '<tr><td colspan="6" 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 .. ';">Mega Stone</p>' ..
'<span style="border-radius: 12px; padding: 4px 10px; border: 1px solid #' .. colors.dark ..
'; background-color: #FFFFFF;"> ' .. stoneHtml .. ' </span></td></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>'
-- Height + Weight.
buf[#buf + 1] = '<tr>' ..
infoCell(colors, 'Height', (data.height or '—') .. 'm') ..
infoCell(colors, 'Weight', (data.weight or '—') .. 'kg') ..
'</tr>'
buf[#buf + 1] = '</table>'
buf[#buf + 1] = '[[Category:Mega-Evolvable Pokémon]]'
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
frame:callParserFunction('#vardefine', 'type', data.type1 or 'Normal')
return infoboxHtml(frame, args, data, name, width, pageName, true)
end
function p.primaryType(frame)
local args = templateArgs(frame)
local name = nonEmpty(args.Name) or mw.title.getCurrentTitle().text
local data = byName(name) or byName(formStem(name))
return (data and nonEmpty(data.type1)) or 'Normal'
end
local function requestedName(frame)
local parent = frame:getParent()
return nonEmpty(frame.args.Name)
or (parent and nonEmpty(parent.args.Name))
or mw.title.getCurrentTitle().text
end
local participantsCache = {}
local function computeParticipants(frame, name)
if participantsCache[name] ~= nil then
return participantsCache[name] or nil
end
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
participantsCache[name] = false
return nil
end
local baseDexNumber = mw.ustring.match(tostring(baseData.number or ''), '^%d+')
local parts = { { data = baseData, label = 'Normal' } }
local seen = { [baseData.name] = true }
for _, form in ipairs(FORM_SUFFIXES) do
local row = rowFor(rawListFormName(form, stem))
-- A candidate must share the same base dex number to be a real form of
-- this Pokémon. Without this check, stripping "Nidoran Male" down to
-- the stem "Nidoran" (done because "Male" is a real form suffix for
-- Indeedee/Meowstic-style species) lets "Nidoran" + " Female" happen to
-- match the raw list's "Nidoran Female" row — a wholly different
-- species (dex #29 vs Male's #32), not this one's gender form.
local rowDexNumber = row and mw.ustring.match(tostring(row.number or ''), '^%d+')
if row and not seen[row.name] and rowDexNumber == baseDexNumber then
seen[row.name] = true
parts[#parts + 1] = { data = row, label = form.label }
if form.baseLabel then
parts[1].label = form.baseLabel
end
end
end
for _, variant in ipairs(MEGA_VARIANTS) do
local row = megaByName(stem .. variant)
if row then
parts[#parts + 1] = { data = row, label = 'Mega' .. variant, mega = true }
end
end
participantsCache[name] = parts
return parts
end
function p.formLabel(frame)
local idx = tonumber(frame.args[1]) or 0
local parts = computeParticipants(frame, requestedName(frame))
if not parts then
return ''
end
local part = parts[idx + 1]
return part and part.label or ''
end
function p.rawName(frame)
local name = requestedName(frame)
local parts = computeParticipants(frame, name)
return (parts and parts[1].data.name) or name
end
function p.formBox(frame)
local idx = tonumber(frame.args[1]) or 0
local width = nonEmpty(frame.args.Width) or '100'
local name = requestedName(frame)
local parts = computeParticipants(frame, name)
if not parts then
return '<strong class="error">PokemonInfo: no data found for "' .. name .. '"</strong>'
end
local part = parts[idx + 1]
if not part then
return ''
end
if part.mega then
return megaInfoboxHtml(frame, part.data, width)
end
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
return infoboxHtml(frame, args, part.data, part.data.name, width, mw.title.getCurrentTitle().text, false)
end
return p