Module:LegendaryPokemon: Difference between revisions

From Pokemon Revolution Online Wiki
Jump to navigation Jump to search
Cae (talk | contribs)
created module
 
Cae (talk | contribs)
No edit summary
Line 106: Line 106:
local row = pokemonByName()[name]
local row = pokemonByName()[name]
if not row then
if not row then
return '<div style="display: inline-block; vertical-align: top; width: 240px; margin: 4px; ' ..
return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: 240px; ' ..
'color: red;">Data load error: No Pokémon data found for ' .. name .. '</div>', nil
'margin: 4px; color: red;">Data load error: No Pokémon data found for ' .. name .. '</div>', nil
end
end


Line 122: Line 122:


local buf = {}
local buf = {}
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; width: 240px; margin: 4px; ' ..
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; ' ..
'background-color: #' .. c.base .. '; border: 3px solid #' .. c.dark ..
'width: 240px; 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;">'
'; border-radius: 20px; padding: 6px; text-align: center; color: #' .. c.dark .. '; font-weight: bold;">'


Line 150: Line 150:
end
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)
local function requirementsBox(dark, requirements)
return '<div style="background-color: #FFFFFF; color: #000000; border-radius: 15px; font-weight: normal; ' ..
return '<div style="width: 100%; box-sizing: border-box; background-color: #FFFFFF; color: #000000; ' ..
'text-align: left; border: 1px solid #' .. dark .. '; padding: 8px; margin-top: 4px;">' ..
'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>'
(requirements or '') .. '</div>'
end
end
Line 187: Line 192:
end
end
local c = typeColor(frame, row.type1)
local c = typeColor(frame, row.type1)
return '<div style="display: inline-block; vertical-align: top; background-color: #' .. c.base ..
-- Explicit width (matching the 240px member card plus this wrapper's
'; border: 1px solid #' .. c.dark .. '; border-radius: 22px; padding: 4px; margin: 4px;">' ..
-- 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.
return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: 250px; ' ..
'background-color: #' .. c.base .. '; border: 1px solid #' .. c.dark ..
'; border-radius: 22px; padding: 4px; margin: 4px;">' ..
cardHtml .. requirementsBox(c.dark, requirements) .. '</div>'
cardHtml .. requirementsBox(c.dark, requirements) .. '</div>'
end
end
Line 202: Line 211:
cardsHtml[#cardsHtml + 1] = cardHtml
cardsHtml[#cardsHtml + 1] = cardHtml
end
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


local buf = {}
local buf = {}
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; background-color: #' .. rc.base ..
buf[#buf + 1] = '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: ' ..
'; border: 3px solid #' .. rc.dark .. '; border-radius: 25px; padding: 6px; margin: 4px; ' ..
groupWidth .. 'px; max-width: 100%; background-color: #' .. rc.base .. '; border: 3px solid #' .. rc.dark ..
'text-align: center; max-width: 100%;">'
'; border-radius: 25px; padding: 6px; margin: 4px; text-align: center;">'


buf[#buf + 1] = '<div style="background-color: #' .. rc.light .. '; border-radius: 20px; padding: 8px; ' ..
buf[#buf + 1] = '<div style="width: 100%; box-sizing: border-box; background-color: #' .. rc.light ..
'color: #' .. rc.dark .. '; font-weight: bold;"><span style="font-size: 18px;">' .. name ..
'; border-radius: 20px; padding: 8px; color: #' .. rc.dark .. '; font-weight: bold;">' ..
'</span><br><span style="font-size: 12px;">[[:Category:Generation ' .. genRoman ..
'<span style="font-size: 18px;">' .. name .. '</span><br><span style="font-size: 12px;">' ..
' Pokémon|<span style="color: #' .. rc.dark .. ';">Generation ' .. genRoman .. '</span>]]</span></div>'
'[[: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;">' ..
buf[#buf + 1] = '<div style="display: flex; flex-wrap: wrap; justify-content: center; margin-top: 6px;">' ..
table.concat(cardsHtml) .. '</div>'
table.concat(cardsHtml) .. '</div>'


buf[#buf + 1] = '<div style="background-color: #' .. rc.light .. '; border-radius: 20px; padding: 6px; ' ..
buf[#buf + 1] = '<div style="width: 100%; box-sizing: border-box; background-color: #' .. rc.light ..
'margin-top: 6px; color: #' .. rc.dark .. '; font-weight: bold; text-align: left;">' ..
'; border-radius: 20px; padding: 6px; margin-top: 6px; color: #' .. rc.dark ..
'<div style="text-align: center;">Availability</div>' .. requirementsBox(rc.dark, requirements) .. '</div>'
'; font-weight: bold; text-align: left;"><div style="text-align: center;">Availability</div>' ..
requirementsBox(rc.dark, requirements) .. '</div>'


buf[#buf + 1] = '</div>'
buf[#buf + 1] = '</div>'

Revision as of 17:42, 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: 40%; text-align: left; padding: 2px 4px; font-size: 11px; color: #' .. dark ..
		';">' .. label .. '</td><td style="padding: 2px 4px;">' ..
		'<div style="background: #FFFFFF; border: 1px solid #' .. dark .. '; border-radius: 8px; overflow: hidden;">' ..
		'<div style="width: ' .. pct .. '%; background: hsl(' .. hue ..
		', 65%, 45%); color: #FFFFFF; font-size: 10px; font-weight: normal; padding: 1px 4px; ' ..
		'white-space: nowrap;">' .. value .. '</div></div></td></tr>'
end

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%; 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%; font-weight: bold; font-size: 12px;"><tr>' ..
		'<td style="background-color: #' .. c1.dark .. '; border-radius: 15px 1px 1px 15px; padding: 2px;">[[' ..
		t1 .. '_(type)|<span style="color: #FFFFFF;">' .. t1 .. '</span>]]</td>' ..
		'<td style="background-color: #' .. c2.dark .. '; border-radius: 1px 15px 15px 1px; padding: 2px;">[[' ..
		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: 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 '') .. '&nbsp;<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.
		return '<div style="display: inline-block; vertical-align: top; box-sizing: border-box; width: 250px; ' ..
			'background-color: #' .. c.base .. '; border: 1px solid #' .. c.dark ..
			'; border-radius: 22px; 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

	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: #' .. rc.base .. '; border: 3px solid #' .. rc.dark ..
		'; border-radius: 25px; padding: 6px; margin: 4px; text-align: center;">'

	buf[#buf + 1] = '<div style="width: 100%; box-sizing: border-box; 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="width: 100%; box-sizing: border-box; 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