Module:LegendaryPokemon
Documentation for this module may be created at Module:LegendaryPokemon/doc
local p = {}
local DATA_FORMAT = 'CSV with header'
local STAT_MAX = 255
local ROMAN = { 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX' }
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 pokemonRowsCache = nil
local function pokemonByName()
if pokemonRowsCache then return pokemonRowsCache end
local csvData = mw.ext.externalData.getExternalData{
url = 'https://wiki.pokemonrevolution.net/index.php?title=Special:GetData/PokemonRawList',
format = DATA_FORMAT,
data = {
number = 'Pokedex Number', name = 'Name', type1 = 'Type1', type2 = 'Type2', generation = 'Generation',
hp = 'HP', atk = 'Attack', def = 'Defense', spatk = 'Special Attack', spdef = 'Special Defense',
spd = 'Speed',
},
}
pokemonRowsCache = {}
if csvData then
for _, row in ipairs(csvData) do
if nonEmpty(row.name) then
pokemonRowsCache[row.name] = row
end
end
end
return pokemonRowsCache
end
local typeColorCache = {}
local function typeColor(frame, pokeType)
pokeType = nonEmpty(pokeType) or 'Normal'
if not typeColorCache[pokeType] then
typeColorCache[pokeType] = {
light = frame:expandTemplate{ title = pokeType .. ' color light' },
base = frame:expandTemplate{ title = pokeType .. ' color' },
dark = frame:expandTemplate{ title = pokeType .. ' color dark' },
}
end
return typeColorCache[pokeType]
end
local regionNameCache = {}
local function regionForGen(frame, genRoman)
if not regionNameCache[genRoman] then
regionNameCache[genRoman] = frame:expandTemplate{ title = 'LegendaryRegionFromGen', args = { genRoman } }
end
return regionNameCache[genRoman]
end
local regionColorCache = {}
local function regionColor(frame, regionName)
if not regionColorCache[regionName] then
regionColorCache[regionName] = {
light = frame:expandTemplate{ title = regionName .. ' color light' },
base = frame:expandTemplate{ title = regionName .. ' color' },
dark = frame:expandTemplate{ title = regionName .. ' color dark' },
}
end
return regionColorCache[regionName]
end
local function genToRoman(genValue)
local digits = mw.ustring.match(tostring(genValue or ''), '^%d+')
local n = digits and tonumber(digits)
return (n and ROMAN[n]) or tostring(genValue or '')
end
-- Same 3-column shape as Module:Pokemon's statBarRow: a nowrap label that
-- only takes the width its own text needs, a bar column pinned to width:
-- 100% that soaks up all the leftover space, and a small fixed-width value
-- column after it -- rather than forcing the label into a fixed 40%, which
-- is what was crowding the bar and making the whole row look right-heavy.
local function statBar(frame, label, value, dark)
value = tonumber(value) or 0
local pct = math.floor(value / STAT_MAX * 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 / STAT_MAX)) + 0.5) -- red (low) -> teal (high)
local fill = 'hsl(' .. hue .. ', 85%, 45%)'
return '<tr>' ..
'<td style="text-align: right; padding: 2px 6px 2px 2px; font-weight: bold; font-size: 10px; ' ..
'color: #' .. dark .. '; white-space: nowrap;">' .. label .. '</td>' ..
'<td style="width: 100%; padding: 2px 0;"><div style="background-color: #FFFFFF; border: 1px solid ' ..
'#999999; border-radius: 8px; height: 12px; overflow: hidden;">' ..
'<div style="width: ' .. pct .. '%; height: 100%; background-color: ' .. fill .. ';"></div>' ..
'</div></td>' ..
'<td style="text-align: left; padding: 2px 2px 2px 6px; font-weight: bold; font-size: 10px; ' ..
'width: 28px;">' .. value .. '</td></tr>'
end
-- Cells are forced to an even 50/50 split (rather than sizing to content) so
-- a longer type name -- "Fighting" next to "Psychic", say -- can't crowd the
-- other cell into overflowing; text is still free to wrap onto a second
-- line at that width rather than being clipped. border: none resets both
-- tables below to kill a default wiki-skin table border that was otherwise
-- showing through as an unstyled brownish line around this part of the card.
local function typeBadges(frame, row)
local t1 = nonEmpty(row.type1)
local t2 = nonEmpty(row.type2)
local c1 = typeColor(frame, t1)
if not t2 then
return '<table align="center" style="width: 80%; border: none; border-collapse: collapse; ' ..
'font-weight: bold; font-size: 12px;"><tr>' ..
'<td style="background-color: #' .. c1.dark .. '; border-radius: 15px; padding: 2px;">[[' .. t1 ..
'_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td></tr></table>'
end
local c2 = typeColor(frame, t2)
return '<table align="center" style="width: 80%; table-layout: fixed; border: none; border-collapse: collapse; ' ..
'font-weight: bold; font-size: 12px;"><tr>' ..
'<td style="width: 50%; box-sizing: border-box; background-color: #' .. c1.dark ..
'; border-radius: 15px 1px 1px 15px; padding: 2px; overflow-wrap: break-word;">[[' ..
t1 .. '_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td>' ..
'<td style="width: 50%; box-sizing: border-box; background-color: #' .. c2.dark ..
'; border-radius: 1px 15px 15px 1px; padding: 2px; overflow-wrap: break-word;">[[' ..
t2 .. '_(type)|<span style="color: #FFFFFF;">' .. t2 .. '</span>]]</td></tr></table>'
end
local function memberCard(frame, name, showGeneration)
local row = pokemonByName()[name]
if not row then
return '<div style="flex: 1 1 220px; width: 100%; box-sizing: border-box; margin: 4px; color: red;">' ..
'Data load error: No Pokémon data found for ' .. name .. '</div>', nil
end
local c = typeColor(frame, row.type1)
local bst = (tonumber(row.hp) or 0) + (tonumber(row.atk) or 0) + (tonumber(row.def) or 0) +
(tonumber(row.spatk) or 0) + (tonumber(row.spdef) or 0) + (tonumber(row.spd) or 0)
local genLine = ''
if showGeneration then
local genRoman = genToRoman(row.generation)
genLine = '<br><span style="font-size: 11px;">[[:Category:Generation ' .. genRoman ..
' Pokémon|<span style="color: #' .. c.dark .. ';">Generation ' .. genRoman .. '</span>]]</span>'
end
-- flex: grows/shrinks within a group's flex row (220px basis); width:
-- 100% takes over instead when this card is the sole content of a
-- block-level parent (a single-Pokémon entry) -- flex properties are
-- simply inert outside of a flex container, so both rules can coexist.
local buf = {}
buf[#buf + 1] = '<div style="flex: 1 1 220px; width: 100%; min-width: 0; box-sizing: border-box; margin: 4px; ' ..
'background-color: #' .. c.base .. '; border: 3px solid #' .. c.dark ..
'; border-radius: 20px; padding: 6px; text-align: center; color: #' .. c.dark .. '; font-weight: bold;">'
buf[#buf + 1] = '<div style="background-color: #' .. c.light .. '; border-radius: 15px; padding: 5px; ' ..
'margin-bottom: 6px;">#' .. (row.number or '') .. ' <span style="font-size: 16px;">[[' .. name ..
'|<span style="color: #' .. c.dark .. ';">' .. name .. '</span>]]</span>' .. genLine .. '</div>'
-- 150x150px (not just 150px) fits the sprite *within* a 150x150 box on
-- whichever dimension is the constraint, instead of only fixing width
-- and leaving height to the image's own aspect ratio -- a tall/narrow
-- sprite was overflowing the fixed-height box under the old |150px form.
buf[#buf + 1] = '<div style="background-color: #FFFFFF; border-radius: 15px; height: 150px; overflow: hidden; ' ..
'display: flex; align-items: center; justify-content: center; margin-bottom: 6px;">[[File:' ..
(row.number or '') .. name .. '.png|150x150px]]</div>'
buf[#buf + 1] = typeBadges(frame, row)
buf[#buf + 1] = '<table style="width: 100%; border: none; border-collapse: collapse; margin-top: 6px;">' ..
statBar(frame, 'HP', row.hp, c.dark) ..
statBar(frame, 'Attack', row.atk, c.dark) ..
statBar(frame, 'Defense', row.def, c.dark) ..
statBar(frame, 'Sp. Atk', row.spatk, c.dark) ..
statBar(frame, 'Sp. Def', row.spdef, c.dark) ..
statBar(frame, 'Speed', row.spd, c.dark) ..
'</table>'
buf[#buf + 1] = '<div style="text-align: center; margin-top: 4px;">' ..
'<div style="display: inline-block; background-color: #FFFFFF; border: 1px solid #' .. c.dark ..
'; padding: 2px 10px; border-radius: 10px; font-size: 11px;">Total: ' .. bst .. '</div></div>'
buf[#buf + 1] = '</div>'
return table.concat(buf), row
end
-- width: 100% + border-box is the actual fix here: without a constrained
-- width, a long Requirements sentence has no wrap point to size against, so
-- the inline-block ancestor's shrink-to-fit layout stretches the *whole
-- card* to fit that one line instead of wrapping text inside a fixed box.
local function requirementsBox(dark, requirements)
return '<div style="width: 100%; box-sizing: border-box; background-color: #FFFFFF; color: #000000; ' ..
'border-radius: 15px; font-weight: normal; text-align: left; border: 1px solid #' .. dark ..
'; padding: 8px; margin-top: 4px; overflow-wrap: break-word;">' ..
(requirements or '') .. '</div>'
end
function p.card(frame)
local parent = frame:getParent()
local args = (parent and parent.args) or frame.args
local name = nonEmpty(args.Name)
if not name then
return "<span style='color:red'>Usage: {{LegendaryPokemon|Name=...|Requirements=...|" ..
"Pokemon=Name1\\Name2 (omit Pokemon= for a single-species entry)}}</span>"
end
local pokemonArg = nonEmpty(args.Pokemon)
local members = {}
if pokemonArg then
for _, part in ipairs(mw.text.split(pokemonArg, '\\', true)) do
local trimmed = mw.text.trim(part)
if trimmed ~= '' then
members[#members + 1] = trimmed
end
end
else
members = { name }
end
local requirements = args.Requirements or ''
if #members == 1 then
local cardHtml, row = memberCard(frame, members[1], true)
if not row then
return cardHtml
end
local c = typeColor(frame, row.type1)
-- 50% width, matching the original single-Pokémon template's own
-- width: 50% -- two of these sit side by side and together span
-- the full row. calc() leaves room for this wrapper's own margin
-- (and the small whitespace gap browsers render between adjacent
-- inline-block elements from separate template calls) so two
-- actually fit per row at true 50/50 instead of the second one
-- wrapping early. Still immune to the Requirements-text sizing bug:
-- width is a definite percentage of the page column, not
-- shrink-to-fit, so a long sentence just wraps instead of stretching it.
return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; ' ..
'width: calc(50% - 10px); background-color: #' .. c.base .. '; border: 1px solid #' .. c.dark ..
'; border-radius: 22px; padding: 4px; margin: 5px;">' ..
cardHtml .. requirementsBox(c.dark, requirements) .. '</div>'
end
local firstRow = pokemonByName()[members[1]]
local genRoman = nonEmpty(args.Gen) or (firstRow and genToRoman(firstRow.generation)) or '?'
local regionName = regionForGen(frame, genRoman)
local rc = regionColor(frame, regionName)
local cardsHtml = {}
for _, member in ipairs(members) do
local cardHtml = memberCard(frame, member, false)
cardsHtml[#cardsHtml + 1] = cardHtml
end
-- Full width, block-level -- matches the original group template's own
-- width: 100%, so a group always takes the whole page width and stacks
-- one per row, same as before. width: 100% is a definite size relative
-- to the page column rather than shrink-to-fit, so it's still immune to
-- the long-Requirements-text sizing bug; member cards flex to fill it.
local buf = {}
buf[#buf + 1] = '<div style="box-sizing: border-box; width: 100%; background-color: #' .. rc.base ..
'; border: 3px solid #' .. rc.dark .. '; border-radius: 25px; padding: 6px; margin: 8px 0; ' ..
'text-align: center;">'
buf[#buf + 1] = '<div style="background-color: #' .. rc.light .. '; border-radius: 20px; padding: 8px; ' ..
'color: #' .. rc.dark .. '; font-weight: bold;"><span style="font-size: 18px;">' .. name ..
'</span><br><span style="font-size: 12px;">[[:Category:Generation ' .. genRoman ..
' Pokémon|<span style="color: #' .. rc.dark .. ';">Generation ' .. genRoman .. '</span>]]</span></div>'
buf[#buf + 1] = '<div style="display: flex; flex-wrap: wrap; justify-content: center; margin-top: 6px;">' ..
table.concat(cardsHtml) .. '</div>'
buf[#buf + 1] = '<div style="background-color: #' .. rc.light .. '; border-radius: 20px; padding: 6px; ' ..
'margin-top: 6px; color: #' .. rc.dark .. '; font-weight: bold; text-align: left;">' ..
'<div style="text-align: center;">Availability</div>' .. requirementsBox(rc.dark, requirements) .. '</div>'
buf[#buf + 1] = '</div>'
return table.concat(buf)
end
return p