Module:LegendaryPokemon: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
No edit summary |
||
| Line 78: | Line 78: | ||
local pct = math.max(0, math.min(100, (value / STAT_MAX) * 100)) | local pct = math.max(0, math.min(100, (value / STAT_MAX) * 100)) | ||
local hue = math.floor((math.max(0, math.min(STAT_MAX, value)) / STAT_MAX) * 130) -- red (low) -> teal (high) | local hue = math.floor((math.max(0, math.min(STAT_MAX, value)) / STAT_MAX) * 130) -- red (low) -> teal (high) | ||
return '<tr><td style="width: | return '<tr><td style="width: 38%; text-align: left; padding: 1px 4px; font-size: 10px; color: #' .. dark .. | ||
';">' .. label .. '</td><td style="padding: | ';">' .. label .. '</td><td style="padding: 1px 4px;">' .. | ||
'<div style="background: #FFFFFF; border: 1px solid #' .. dark .. '; border-radius: | '<div style="background: #FFFFFF; border: 1px solid #' .. dark .. '; border-radius: 7px; overflow: hidden;">' .. | ||
'<div style="width: ' .. pct .. '%; background: hsl(' .. hue .. | '<div style="width: ' .. pct .. '%; background: hsl(' .. hue .. | ||
', 65%, 45%); color: #FFFFFF; font-size: | ', 65%, 45%); color: #FFFFFF; font-size: 9px; font-weight: normal; padding: 1px 4px; ' .. | ||
'white-space: nowrap;">' .. value .. '</div></div></td></tr>' | 'white-space: nowrap;">' .. value .. '</div></div></td></tr>' | ||
end | 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. | |||
local function typeBadges(frame, row) | local function typeBadges(frame, row) | ||
local t1 = nonEmpty(row.type1) | local t1 = nonEmpty(row.type1) | ||
| Line 91: | Line 95: | ||
local c1 = typeColor(frame, t1) | local c1 = typeColor(frame, t1) | ||
if not t2 then | if not t2 then | ||
return '<table align="center" style="width: | return '<table align="center" style="width: 85%; font-weight: bold; font-size: 11px;"><tr>' .. | ||
'<td style="background-color: #' .. c1.dark .. '; border-radius: | '<td style="background-color: #' .. c1.dark .. '; border-radius: 12px; padding: 3px;">[[' .. t1 .. | ||
'_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td></tr></table>' | '_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td></tr></table>' | ||
end | end | ||
local c2 = typeColor(frame, t2) | local c2 = typeColor(frame, t2) | ||
return '<table align="center" style="width: | return '<table align="center" style="width: 85%; table-layout: fixed; font-weight: bold; font-size: 11px;">' .. | ||
'<td style="background-color: #' .. c1.dark .. '; border-radius: | '<tr><td style="width: 50%; box-sizing: border-box; background-color: #' .. c1.dark .. | ||
'; border-radius: 12px 1px 1px 12px; padding: 3px; overflow-wrap: break-word;">[[' .. | |||
t1 .. '_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td>' .. | t1 .. '_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td>' .. | ||
'<td style="background-color: #' .. c2.dark .. '; border-radius: 1px | '<td style="width: 50%; box-sizing: border-box; background-color: #' .. c2.dark .. | ||
'; border-radius: 1px 12px 12px 1px; padding: 3px; overflow-wrap: break-word;">[[' .. | |||
t2 .. '_(type)|<span style="color: #FFFFFF;">' .. t2 .. '</span>]]</td></tr></table>' | t2 .. '_(type)|<span style="color: #FFFFFF;">' .. t2 .. '</span>]]</td></tr></table>' | ||
end | end | ||
| Line 123: | Line 129: | ||
local buf = {} | local buf = {} | ||
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; ' .. | buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; ' .. | ||
'width: 240px; margin: 4px; background-color: #' .. c.base .. '; border: | 'width: 240px; margin: 4px; background-color: #' .. c.base .. '; border: 2px solid #' .. c.dark .. | ||
'; border-radius: | '; border-radius: 18px; 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; ' .. | buf[#buf + 1] = '<div style="background-color: #' .. c.light .. '; border-radius: 15px; padding: 5px; ' .. | ||
| Line 195: | Line 201: | ||
-- own padding/border) instead of shrink-to-fit, so the card's size | -- own padding/border) instead of shrink-to-fit, so the card's size | ||
-- is driven by its fixed layout, not by how long Requirements is. | -- is driven by its fixed layout, not by how long Requirements is. | ||
-- No background fill here -- the member card underneath is already | |||
-- fully colored, so this wrapper stays a plain thin frame instead of | |||
-- doubling the same color behind it. | |||
return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: 250px; ' .. | return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: 250px; ' .. | ||
' | 'border: 1px solid #' .. c.dark .. '; border-radius: 20px; padding: 4px; margin: 4px;">' .. | ||
cardHtml .. requirementsBox(c.dark, requirements) .. '</div>' | cardHtml .. requirementsBox(c.dark, requirements) .. '</div>' | ||
end | end | ||
| Line 218: | Line 226: | ||
local groupWidth = (#members * 248) + 20 | local groupWidth = (#members * 248) + 20 | ||
-- The band itself stays a light neutral tint with just a thin colored | |||
-- border -- the member cards underneath are already fully colored in | |||
-- their own type, so filling the band behind them with a second solid | |||
-- region color doubled up the saturation instead of adding information. | |||
-- The region color still reads clearly via the title pill and border. | |||
local buf = {} | local buf = {} | ||
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: ' .. | buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: ' .. | ||
groupWidth .. 'px; max-width: 100%; background-color: # | groupWidth .. 'px; max-width: 100%; background-color: #F7F7F7; border: 2px solid #' .. rc.dark .. | ||
'; border-radius: | '; border-radius: 22px; padding: 6px; margin: 4px; text-align: center;">' | ||
buf[#buf + 1] = '<div style="width: 100%; box-sizing: border-box; background-color: #' .. rc.light .. | buf[#buf + 1] = '<div style="width: 100%; box-sizing: border-box; background-color: #' .. rc.light .. | ||
'; border-radius: | '; border-radius: 18px; padding: 8px; color: #' .. rc.dark .. '; font-weight: bold;">' .. | ||
'<span style="font-size: 18px;">' .. name .. '</span><br><span style="font-size: 12px;">' .. | '<span style="font-size: 18px;">' .. name .. '</span><br><span style="font-size: 12px;">' .. | ||
'[[:Category:Generation ' .. genRoman .. ' Pokémon|<span style="color: #' .. rc.dark .. | '[[:Category:Generation ' .. genRoman .. ' Pokémon|<span style="color: #' .. rc.dark .. | ||
| Line 233: | Line 246: | ||
buf[#buf + 1] = '<div style="width: 100%; box-sizing: border-box; background-color: #' .. rc.light .. | buf[#buf + 1] = '<div style="width: 100%; box-sizing: border-box; background-color: #' .. rc.light .. | ||
'; border-radius: | '; border-radius: 18px; padding: 6px; margin-top: 6px; color: #' .. rc.dark .. | ||
'; font-weight: bold; text-align: left;"><div style="text-align: center;">Availability</div>' .. | '; font-weight: bold; text-align: left;"><div style="text-align: center;">Availability</div>' .. | ||
requirementsBox(rc.dark, requirements) .. '</div>' | requirementsBox(rc.dark, requirements) .. '</div>' | ||
Revision as of 17:59, 4 August 2026
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
local function statBar(frame, label, value, dark)
value = tonumber(value) or 0
local pct = math.max(0, math.min(100, (value / STAT_MAX) * 100))
local hue = math.floor((math.max(0, math.min(STAT_MAX, value)) / STAT_MAX) * 130) -- red (low) -> teal (high)
return '<tr><td style="width: 38%; text-align: left; padding: 1px 4px; font-size: 10px; color: #' .. dark ..
';">' .. label .. '</td><td style="padding: 1px 4px;">' ..
'<div style="background: #FFFFFF; border: 1px solid #' .. dark .. '; border-radius: 7px; overflow: hidden;">' ..
'<div style="width: ' .. pct .. '%; background: hsl(' .. hue ..
', 65%, 45%); color: #FFFFFF; font-size: 9px; font-weight: normal; padding: 1px 4px; ' ..
'white-space: nowrap;">' .. value .. '</div></div></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.
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: 85%; font-weight: bold; font-size: 11px;"><tr>' ..
'<td style="background-color: #' .. c1.dark .. '; border-radius: 12px; padding: 3px;">[[' .. t1 ..
'_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td></tr></table>'
end
local c2 = typeColor(frame, t2)
return '<table align="center" style="width: 85%; table-layout: fixed; font-weight: bold; font-size: 11px;">' ..
'<tr><td style="width: 50%; box-sizing: border-box; background-color: #' .. c1.dark ..
'; border-radius: 12px 1px 1px 12px; padding: 3px; 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 12px 12px 1px; padding: 3px; 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="display: inline-block; vertical-align: top; box-sizing: border-box; width: 240px; ' ..
'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
local buf = {}
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; ' ..
'width: 240px; margin: 4px; background-color: #' .. c.base .. '; border: 2px solid #' .. c.dark ..
'; border-radius: 18px; 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>'
buf[#buf + 1] = '<div style="background-color: #FFFFFF; border-radius: 15px; height: 150px; display: flex; ' ..
'align-items: center; justify-content: center; margin-bottom: 6px;">[[File:' .. (row.number or '') ..
name .. '.png|150px]]</div>'
buf[#buf + 1] = typeBadges(frame, row)
buf[#buf + 1] = '<table style="width: 100%; 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) ..
'<tr><td style="text-align: left; padding: 3px 4px; font-size: 11px;">Total</td>' ..
'<td style="padding: 3px 4px; font-size: 11px;">' .. bst .. '</td></tr></table>'
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)
-- Explicit width (matching the 240px member card plus this wrapper's
-- own padding/border) instead of shrink-to-fit, so the card's size
-- is driven by its fixed layout, not by how long Requirements is.
-- No background fill here -- the member card underneath is already
-- fully colored, so this wrapper stays a plain thin frame instead of
-- doubling the same color behind it.
return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: 250px; ' ..
'border: 1px solid #' .. c.dark .. '; border-radius: 20px; padding: 4px; margin: 4px;">' ..
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
-- Explicit width sized to the member-card row (240px card + 8px margin
-- each, plus this wrapper's own padding/border) instead of shrink-to-fit
-- -- same reasoning as the single-card case, otherwise a long
-- Requirements sentence inflates the whole band far past its cards.
local groupWidth = (#members * 248) + 20
-- The band itself stays a light neutral tint with just a thin colored
-- border -- the member cards underneath are already fully colored in
-- their own type, so filling the band behind them with a second solid
-- region color doubled up the saturation instead of adding information.
-- The region color still reads clearly via the title pill and border.
local buf = {}
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: ' ..
groupWidth .. 'px; max-width: 100%; background-color: #F7F7F7; border: 2px solid #' .. rc.dark ..
'; border-radius: 22px; padding: 6px; margin: 4px; text-align: center;">'
buf[#buf + 1] = '<div style="width: 100%; box-sizing: border-box; background-color: #' .. rc.light ..
'; border-radius: 18px; 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="width: 100%; box-sizing: border-box; background-color: #' .. rc.light ..
'; border-radius: 18px; 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