Module:Learnsets: Difference between revisions

From Pokemon Revolution Online Wiki
Jump to navigation Jump to search
Cae (talk | contribs)
No edit summary
Cae (talk | contribs)
No edit summary
Line 1: Line 1:
local p = {}
local p = {}


local DATA_SOURCE = 'data'
local function getArgs(frame)
local DATA_FILE = 'PokemonRawList.csv'
    local args = {}
local DATA_FORMAT = 'CSV with header'
    local parent = frame:getParent()
   
    if parent then
        for k, v in pairs(parent.args) do
            if v ~= '' then
                args[k] = v
            end
        end
    end
   
    for k, v in pairs(frame.args) do
        if v ~= '' then
            args[k] = v
        end
    end
   
    return args
end


local COLUMNS = {
local moveDataCache = nil
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 function blankToNil(v)
    if v == nil or v == '' then
        return nil
    end
    return v
end


local STATS = {
local function loadMoveData()
{ key = 'hp', label = 'HP', template = 'HP' },
    if moveDataCache then
{ key = 'atk', label = 'Atk', template = 'Attack' },
        return moveDataCache
{ key = 'def', label = 'Def', template = 'Defense' },
    end
{ key = 'spatk', label = 'Sp.Atk', template = 'Special Attack' },
   
{ key = 'spdef', label = 'Sp.Def', template = 'Special Defense' },
    local csvData = mw.ext.externalData.getExternalData({
{ key = 'spd', label = 'Spd', template = 'Speed' },
        url = 'https://wiki.pokemonrevolution.net/index.php?title=Special:GetData/RawMovesList',
}
        format = 'CSV with header',
        data = {
            Name = 'Name',
            Type = 'Type',
            Category = 'Category',
            Power = 'Power',
            Accuracy = 'Accuracy',
            PP = 'PP',
            Broken = 'Broken',
            Typeless = 'Typeless',
            Priority = 'Priority',
            TM = 'TM',
            ['Move Tutor'] = 'Move Tutor',
            Egg = 'Egg',
        },
    })


local EV_KEYS = { 'evhp', 'evatk', 'evdef', 'evspa', 'evspd', 'evsp' }
    moveDataCache = {}
    if csvData then
        for _, row in ipairs(csvData) do
            moveDataCache[row.Name] = {
                type = blankToNil(row.Type) or 'Normal',
                category = blankToNil(row.Category) or 'Status',
                power = blankToNil(row.Power),
                accuracy = blankToNil(row.Accuracy),
                pp = blankToNil(row.PP),
                broken = row.Broken or 'N',
                typeless = row.Typeless or 'N',
                priority = blankToNil(row.Priority) or '0',
                tm = blankToNil(row.TM),
                move_tutor = blankToNil(row['Move Tutor']),
                egg = blankToNil(row.Egg)
            }
        end
    end


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


local function nonEmpty(s)
local function loadLearnset(pokemonName)
if s == nil then return nil end
    local jsonPath = '$[' .. mw.text.jsonEncode(pokemonName) .. ']'
s = mw.text.trim(tostring(s))
    local ok, result, errors = pcall(function()
if s == '' then return nil end
        return mw.ext.externalData.getFileData({
return s
            source = 'data',
            ['file name'] = 'Learnsets.json',
            format = 'JSON',
            ['use jsonpath'] = true,
            data = { poke = jsonPath },
        })
    end)
    if not ok then
        return nil, 'Lua error: ' .. tostring(result)
    end
    if errors and #errors > 0 then
        return nil, 'ExternalData error: ' .. table.concat(errors, ' | ')
    end
    local raw = result and result.poke
    if type(raw) ~= 'table' then
        return nil, 'No learnset data found for ' .. pokemonName
    end
    return raw, nil
end
end


local function templateArgs(frame)
local learnsetCache = {}
local parent = frame:getParent()
local learnsetErrorCache = {}
return (parent and parent.args) or frame.args
 
local function getCachedLearnset(pokemonName)
    if learnsetCache[pokemonName] == nil and learnsetErrorCache[pokemonName] == nil then
        learnsetCache[pokemonName], learnsetErrorCache[pokemonName] = loadLearnset(pokemonName)
    end
    return learnsetCache[pokemonName], learnsetErrorCache[pokemonName]
end
end


local function padNumber(n)
local pokemonTypeCache = nil
n = tonumber(n) or 0
 
return string.format('%03d', n)
local function loadPokemonTypes()
    if pokemonTypeCache then return pokemonTypeCache end
 
    local csvData = mw.ext.externalData.getExternalData({
        url    = 'https://wiki.pokemonrevolution.net/index.php?title=Special:GetData/PokemonRawList',
        format = 'CSV with header',
        data = {
            Name = 'Name',
            Type1 = 'Type1',
            Type2 = 'Type2',
        },
    })
 
    pokemonTypeCache = {}
    if csvData then
        for _, row in ipairs(csvData) do
            pokemonTypeCache[row.Name] = {
                type1 = row.Type1 or '',
                type2 = row.Type2 or '',
            }
        end
    end
    return pokemonTypeCache
end
end


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


local function byName(name)
local categoryColorCache = {}
return fetchRow('Name', name)
local function categoryColor(frame, category)
    category = (category and category ~= '') and category or 'Status'
    if not categoryColorCache[category] then
        categoryColorCache[category] = {
            base = frame:expandTemplate{ title = category .. ' color' },
            dark = frame:expandTemplate{ title = category .. ' color dark' },
        }
    end
    return categoryColorCache[category]
end
end


local function byNumber(number)
local function isSTAB(moveType, primaryType, secondaryType)
return fetchRow('Pokedex Number', number)
    return moveType == primaryType or moveType == secondaryType
end
end


local function typeColors(frame, pokeType)
local function formatItemNumber(label, num)
return {
    num = tonumber(num)
light = frame:expandTemplate{ title = pokeType .. ' color light' },
    if not num then
base = frame:expandTemplate{ title = pokeType .. ' color' },
        return ''
dark = frame:expandTemplate{ title = pokeType .. ' color dark' },
    end
}
    return label .. string.format('%02d', num)
end
end


local function statColors(frame, statTemplate)
function p.renderLevelUp(frame)
return {
    local args = getArgs(frame)
base = frame:expandTemplate{ title = statTemplate .. ' color' },
    local pokemonName = args[1] or args.pokemon
dark = frame:expandTemplate{ title = statTemplate .. ' color dark' },
    local primaryType = args.type1 or ""
}
    local secondaryType = args.type2 or ""
   
    local pokemon, lsErr = getCachedLearnset(pokemonName)
    local moveData = loadMoveData()
 
    if not pokemon then
        return "<span style='color:red'>Data load error: " .. (lsErr or 'unknown') .. "</span>"
    end
 
    local moves = pokemon.level_up or {}
    local colors = typeColor(frame, primaryType)
 
    local html = mw.html.create('div')
        :css('width', '77%')
        :css('margin', 'auto')
    local table = html:tag('table')
        :addClass('sortable')
        :css('width', '100%')
        :css('text-align', 'center')
        :css('border-radius', '15px 15px 0 0')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-bottom', 'none')
        :css('background-color', '#' .. colors.base)
        :css('padding', '7px')
 
        local header = table:tag('tr')
        :css('background-color', '#' .. colors.light)
        :css('color', '#' .. colors.dark)
    header:tag('th'):wikitext('Level'):css('width', '10%'):css('border-radius', '15px 1px 1px 1px')
    header:tag('th'):wikitext('Move'):css('width', '15%')
    header:tag('th'):wikitext('Type'):css('width', '17%')
    header:tag('th'):wikitext('Category'):css('width', '16%')
    header:tag('th'):wikitext('Base Power'):css('width', '17%')
    header:tag('th'):wikitext('Accuracy'):css('width', '15%')
    header:tag('th'):wikitext('PP'):css('width', '10%'):css('border-radius', '1px 15px 1px 1px')
 
        for _, move in ipairs(moves) do
        local moveName = move.move
        local level = move.level
        local moveInfo = moveData[moveName] or {}
 
        local row = table:tag('tr'):css('background-color', '#FFFFFF')
        row:tag('td'):wikitext(tostring(level))
 
        local moveLink = '[[' .. moveName .. ']]'
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
            moveLink = "'''" .. moveLink .. "'''"
        end
        if moveInfo.broken == "Y" then
            moveLink = "''" .. moveLink .. "''"
        end
        row:tag('td'):wikitext(moveLink)
 
        local moveTypeColors = typeColor(frame, moveInfo.type)
        row:tag('td')
            :css('background-color', '#' .. moveTypeColors.base)
            :css('border', '2px solid #' .. moveTypeColors.dark)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
 
        local moveCatColors = categoryColor(frame, moveInfo.category)
        row:tag('td')
            :css('background-color', '#' .. moveCatColors.base)
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')
 
        row:tag('td'):wikitext(moveInfo.power or '—')
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end
 
    html:tag('div')
        :css('text-align', 'left')
        :css('background-color', '#' .. colors.light)
        :css('padding', '10px')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-top', 'none')
        :css('border-radius', '0 0 15px 15px')
        :wikitext([=[
* '''Emboldened''' moves denote this Pokémon avails from a STAB bonus for it.
* ''Italicized'' moves are [[Broken_Moves_%26_Abilities|broken]]; see their individual pages for more information.
* All moves subsumed on this list are relearnable via the [[Move Relearner]]s if lost.
]=])
   
    return tostring(html) .. '<br clear="all">'
end
end


local function navTableHtml(frame, data, colors, pageName)
function p.renderTMsHMs(frame)
local number = tonumber(data.number)
    local args = getArgs(frame)
    local pokemonName = args[1] or args.pokemon
    local primaryType = args.type1 or ""
    local secondaryType = args.type2 or ""
   
    local pokemon = getCachedLearnset(pokemonName)
    local moveData = loadMoveData()
 
    if not pokemon then return '' end
 
    local tms = pokemon.tms or {}
    local hms = pokemon.hms or {}
    local colors = typeColor(frame, primaryType)
 
    local html = mw.html.create('div')
        :css('width', '77%')
        :css('margin', 'auto')
    local table = html:tag('table')
        :addClass('sortable')
        :css('width', '100%')
        :css('text-align', 'center')
        :css('border-radius', '25px 25px 0 0')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-bottom', 'none')
        :css('background-color', '#' .. colors.base)
        :css('padding', '7px')
 
    local header = table:tag('tr')
        :css('background-color', '#' .. colors.light)
        :css('color', '#' .. colors.dark)
    header:tag('th'):attr('colspan', '2'):wikitext('TM'):css('width', '10%'):css('border-top-left-radius', '25px')
    header:tag('th'):wikitext('Move'):css('width', '15%')
    header:tag('th'):wikitext('Type'):css('width', '17%')
    header:tag('th'):wikitext('Category'):css('width', '16%')
    header:tag('th'):wikitext('Base Power'):css('width', '17%')
    header:tag('th'):wikitext('Accuracy'):css('width', '15%')
    header:tag('th'):wikitext('PP'):css('width', '10%'):css('border-top-right-radius', '25px')
 
    for _, moveName in ipairs(tms) do
        local moveInfo = moveData[moveName] or {}
        local row = table:tag('tr'):css('background-color', '#FFFFFF')
 
        row:tag('td'):attr('colspan', '2'):wikitext(formatItemNumber('TM', moveInfo.tm))


local prevCell = ''
        local moveLink = '[[' .. moveName .. ']]'
if number and number > 1 then
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
local prev = byNumber(number - 1)
            moveLink = "'''" .. moveLink .. "'''"
if prev then
        end
prevCell = '&nbsp;[[File:' .. (number - 1) .. 'Icon.png|' .. prev.name .. '|link=' .. prev.name ..
        if moveInfo.broken == "Y" then
']]&nbsp;&nbsp;&nbsp;#' .. padNumber(prev.number)
            moveLink = "''" .. moveLink .. "''"
end
        end
end
        row:tag('td'):wikitext(moveLink)


local nextCell = ''
        local moveTypeColors = typeColor(frame, moveInfo.type)
if number then
        row:tag('td')
local nxt = byNumber(number + 1)
            :css('background-color', '#' .. moveTypeColors.base)
if nxt then
            :css('border', '2px solid #' .. moveTypeColors.dark)
nextCell = '#' .. padNumber(nxt.number) .. '&nbsp;&nbsp;&nbsp;[[File:' .. (number + 1) ..
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
'Icon.png|' .. nxt.name .. '|link=' .. nxt.name .. ']]'
end
end


return '<table style="margin-left: auto; margin-right: auto; margin-bottom: 15px; height: 60px; ' ..
        local moveCatColors = categoryColor(frame, moveInfo.category)
'border: 4px #' .. colors.dark .. ' solid; font-weight: bold; width: 65%; color: #' .. colors.dark ..
        row:tag('td')
'; border-radius: 20px; padding: 3px; background: #' .. colors.base .. ';">' ..
            :css('background-color', '#' .. moveCatColors.base)
'<tr>' ..
            :css('border', '2px solid #' .. moveCatColors.dark)
'<td style="text-align: left; width: 36%; background: #' .. colors.light ..
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')
'; border-radius: 15px 1px 1px 15px; border: 1px solid #' .. colors.dark .. ';">' .. prevCell .. '</td>' ..
 
'<td style="text-align: center; width: 28%; background: #' .. colors.light ..
        row:tag('td'):wikitext(moveInfo.power or '—')
'; border: 1px solid #' .. colors.dark .. ';">#' .. padNumber(number) .. '&nbsp;&nbsp;-&nbsp;&nbsp;' ..
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
pageName .. '</td>' ..
        row:tag('td'):wikitext(moveInfo.pp or '—')
'<td style="text-align: right; width: 36%; padding-right: 10px; background: #' .. colors.light ..
    end
'; border: 1px solid #' .. colors.dark .. '; border-radius: 1px 15px 15px 1px;">' .. nextCell .. '</td>' ..
 
'</tr></table>'
    for _, moveName in ipairs(hms) do
        local moveInfo = moveData[moveName] or {}
        local row = table:tag('tr'):css('background-color', '#FFFFCC')
 
        row:tag('td'):attr('colspan', '2'):wikitext(formatItemNumber('HM', moveInfo.tm)):css('font-weight', 'bold')
 
        local moveLink = '[[' .. moveName .. ']]'
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
            moveLink = "'''" .. moveLink .. "'''"
        end
        if moveInfo.broken == "Y" then
            moveLink = "''" .. moveLink .. "''"
        end
        row:tag('td'):wikitext(moveLink)
 
        local moveTypeColors = typeColor(frame, moveInfo.type)
        row:tag('td')
            :css('background-color', '#' .. moveTypeColors.base)
            :css('border', '2px solid #' .. moveTypeColors.dark)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
 
        local moveCatColors = categoryColor(frame, moveInfo.category)
        row:tag('td')
            :css('background-color', '#' .. moveCatColors.base)
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')
 
        row:tag('td'):wikitext(moveInfo.power or '—')
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end
 
    html:tag('div')
        :css('text-align', 'left')
        :css('background-color', '#' .. colors.light)
        :css('padding', '10px')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-top', 'none')
        :css('border-radius', '0 0 25px 25px')
        :wikitext([=[
* '''Emboldened''' moves denote that this Pokémon avails from a STAB bonus for it.
* ''Italicized'' moves are [[Broken_Moves_%26_Abilities|broken]]; see their individual pages for more information.
* Refer to [[TMs and HMs]] for information on each move's obtainability.
]=])
   
    return tostring(html) .. '<br clear="all">'
end
end


function p.navTable(frame)
function p.renderMoveTutors(frame)
local args = templateArgs(frame)
    local args = getArgs(frame)
local pageName = mw.title.getCurrentTitle().text
    local pokemonName = args[1] or args.pokemon
local name = nonEmpty(args.Name) or pageName
    local primaryType = args.type1 or ""
    local secondaryType = args.type2 or ""
   
    local pokemon = getCachedLearnset(pokemonName)
    local moveData = loadMoveData()
 
    if not pokemon then return '' end
 
    local moves = pokemon.move_tutors or {}
   
    if #moves == 0 then
        return ""
    end
   
    local colors = typeColor(frame, primaryType)
 
    local html = mw.html.create('div')
        :css('width', '77%')
        :css('margin', 'auto')
    local table = html:tag('table')
        :addClass('sortable')
        :css('width', '100%')
        :css('text-align', 'center')
        :css('border-radius', '25px 25px 0 0')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-bottom', 'none')
        :css('background-color', '#' .. colors.base)
        :css('padding', '7px')
 
    local header = table:tag('tr')
        :css('background-color', '#' .. colors.light)
        :css('color', '#' .. colors.dark)
    header:tag('th'):wikitext('Move'):css('width', '20%'):css('border-top-left-radius', '25px')
    header:tag('th'):wikitext('Type'):css('width', '17%')
    header:tag('th'):wikitext('Category'):css('width', '16%')
    header:tag('th'):wikitext('Base Power'):css('width', '17%')
    header:tag('th'):wikitext('Accuracy'):css('width', '15%')
    header:tag('th'):wikitext('PP'):css('width', '15%'):css('border-top-right-radius', '25px')
 
    for _, moveName in ipairs(moves) do
        local moveInfo = moveData[moveName] or {}
        local row = table:tag('tr'):css('background-color', '#FFFFFF')
 
        local moveLink = '[[' .. moveName .. ']]'
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
            moveLink = "'''" .. moveLink .. "'''"
        end
        if moveInfo.broken == "Y" then
            moveLink = "''" .. moveLink .. "''"
        end
        row:tag('td'):wikitext(moveLink)
 
        local moveTypeColors = typeColor(frame, moveInfo.type)
        row:tag('td')
            :css('background-color', '#' .. moveTypeColors.base)
            :css('border', '2px solid #' .. moveTypeColors.dark)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
 
        local moveCatColors = categoryColor(frame, moveInfo.category)
        row:tag('td')
            :css('background-color', '#' .. moveCatColors.base)
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')
 
        row:tag('td'):wikitext(moveInfo.power or '—')
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end


local data = byName(name)
    html:tag('div')
if not data then
        :css('text-align', 'left')
return '<strong class="error">PokeNavTable: no data found for "' .. name .. '"</strong>'
        :css('background-color', '#' .. colors.light)
end
        :css('padding', '10px')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-top', 'none')
        :css('border-radius', '0 0 25px 25px')
        :wikitext([=[
* '''Emboldened''' moves denote that this Pokémon avails from a STAB bonus for it.
* ''Italicized'' moves are [[Broken_Moves_%26_Abilities|broken]]; see their individual pages for more information.
* Refer to the [[Move Tutors]] article for tutorship information for all moves hereon.
]=])


return navTableHtml(frame, data, typeColors(frame, data.type1), pageName)
    return tostring(html) .. '<br clear="all">'
end
end


local function infoCell(colors, labelHtml, valueHtml)
function p.renderEggMoves(frame)
return '<td style="border: 3px solid #' .. colors.dark .. '; background-color: #' .. colors.light ..
    local args = getArgs(frame)
'; border-radius: 15px; padding: 5px;">' ..
    local pokemonName = args[1] or args.pokemon
'<p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark .. ';">' .. labelHtml .. '</p>' ..
    local primaryType = args.type1 or ""
'<span style="border-radius: 20px; padding: 6px; border: 4px solid #' .. colors.dark ..
    local secondaryType = args.type2 or ""
'; background-color: #FFFFFF;">&nbsp;&nbsp;' .. valueHtml .. '&nbsp;&nbsp;</span></td>'
   
end
    local pokemon = getCachedLearnset(pokemonName)
    local moveData = loadMoveData()
 
    if not pokemon then return '' end


local function abilityCell(colors, label, ability, widthPercent)
    local moves = pokemon.egg_moves or {}
if not ability then return '' end
   
local style = 'border-radius: 20px; padding: 6px; border: 4px solid #' .. colors.dark ..
    if #moves == 0 then
'; background-color: #FFFFFF;'
        return ""
if widthPercent then
    end
style = 'width: ' .. widthPercent .. '%; ' .. style
   
end
    local colors = typeColor(frame, primaryType)
return '<td style="' .. style .. '"><b>' .. label .. '</b><br>[[' .. ability .. '_(ability)|' .. ability ..
']]</td>'
end


local function abilitiesRow(colors, data)
    local html = mw.html.create('div')
local entries = {
        :css('width', '77%')
{ 'Ability 1', nonEmpty(data.ability1) },
        :css('margin', 'auto')
{ 'Ability 2', nonEmpty(data.ability2) },
    local table = html:tag('table')
{ 'Hidden Ability', nonEmpty(data.ha) },
        :addClass('sortable')
}
        :css('width', '100%')
local present = {}
        :css('text-align', 'center')
for _, entry in ipairs(entries) do
        :css('border-radius', '25px 25px 0 0')
if entry[2] then present[#present + 1] = entry end
        :css('border', '3px solid #' .. colors.dark)
end
        :css('border-bottom', 'none')
local widthPercent = (#present == 2) and 50 or nil
        :css('background-color', '#' .. colors.base)
local cells = {}
        :css('padding', '7px')
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 header = table:tag('tr')
local colors = statColors(frame, stat.template)
        :css('background-color', '#' .. colors.light)
return '<td style="border: 4px solid #' .. colors.dark .. '; background-color: #' .. colors.base ..
        :css('color', '#' .. colors.dark)
'; border-radius: 15px; padding: 5px; font-size: 11px;">\'\'\'' .. stat.label .. '\'\'\'<br>' ..
    header:tag('th'):wikitext('Move'):css('width', '20%'):css('border-top-left-radius', '25px')
value .. '</td>'
    header:tag('th'):wikitext('Type'):css('width', '17%')
end
    header:tag('th'):wikitext('Category'):css('width', '16%')
    header:tag('th'):wikitext('Base Power'):css('width', '17%')
    header:tag('th'):wikitext('Accuracy'):css('width', '15%')
    header:tag('th'):wikitext('PP'):css('width', '15%'):css('border-top-right-radius', '25px')


local function genderRatioHtml(male)
    for _, moveName in ipairs(moves) do
if male == 'Genderless' then
        local moveInfo = moveData[moveName] or {}
return '<span style="color:#000000; background-color: #FFFFFF; border-radius: 15px; ' ..
        local row = table:tag('tr'):css('background-color', '#FFFFFF')
'border: 4px solid #000000; padding: 5px;">&nbsp;&nbsp;Genderless&nbsp;&nbsp;</span>'
end
local m = tonumber(male) or 100
if m >= 100 then
return '<span style="color:#0000FF; border-radius: 15px; background-color: #FFFFFF; padding: 5px; ' ..
'border: 4px solid #0000FF;">&nbsp;&nbsp;100%&nbsp;&nbsp;</span>'
elseif m <= 0 then
return '<span style="color:#FF6060; background-color: #FFFFFF; border-radius: 15px; ' ..
'border: 4px solid #FF6060; padding: 5px;">&nbsp;&nbsp;100%&nbsp;&nbsp;</span>'
else
return '<span style="color:#0000FF; border-radius: 15px; background-color: #FFFFFF; padding: 5px; ' ..
'border: 4px solid #0000FF;">&nbsp;&nbsp;' .. m .. '%&nbsp;&nbsp;</span>' ..
'<span style="color:#FF6060; background-color: #FFFFFF; border-radius: 15px; ' ..
'border: 4px solid #FF6060; padding: 5px;">&nbsp;&nbsp;' .. (100 - m) .. '%&nbsp;&nbsp;</span>'
end
end


local function categories(data, gen, bstCategory, args)
        local moveLink = '[[' .. moveName .. ']]'
local cats = {
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
'[[Category:Pokémon species]]',
            moveLink = "'''" .. moveLink .. "'''"
'[[Category:Generation ' .. gen .. ' Pokémon]]',
        end
'[[Category:' .. data.type1 .. '-type Pokémon]]',
        if moveInfo.broken == "Y" then
}
            moveLink = "''" .. moveLink .. "''"
if nonEmpty(data.type2) then
        end
cats[#cats + 1] = '[[Category:' .. data.type2 .. '-type Pokémon]]'
        row:tag('td'):wikitext(moveLink)
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


function p.infobox(frame)
        local moveTypeColors = typeColor(frame, moveInfo.type)
local args = templateArgs(frame)
        row:tag('td')
local pageName = mw.title.getCurrentTitle().text
            :css('background-color', '#' .. moveTypeColors.base)
local name = nonEmpty(args.Name) or pageName
            :css('border', '2px solid #' .. moveTypeColors.dark)
local displayName = nonEmpty(args.Name) or pageName
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
local width = nonEmpty(args.Width) or '45'


local data = byName(name)
        local moveCatColors = categoryColor(frame, moveInfo.category)
if not data then
        row:tag('td')
return '<strong class="error">PokemonInfo: no data found for "' .. name .. '"</strong>'
            :css('background-color', '#' .. moveCatColors.base)
end
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')


local colors = typeColors(frame, data.type1)
        row:tag('td'):wikitext(moveInfo.power or '—')
local gen = GEN_NUMERALS[tonumber(data.generation)]
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end


-- Types pill(s).
    html:tag('div')
local typesHtml
        :css('text-align', 'left')
if nonEmpty(data.type2) then
        :css('background-color', '#' .. colors.light)
local type2Colors = typeColors(frame, data.type2)
        :css('padding', '10px')
typesHtml =
        :css('border', '3px solid #' .. colors.dark)
'<td style="width: 50%; background-color: #' .. colors.dark .. '; border-radius: 25px 1px 1px 25px;">' ..
        :css('border-top', 'none')
'[[' .. data.type1 .. '_(type)|<span style="color: #FFFFFF;">' .. data.type1 .. '</span>]]</td>' ..
        :css('border-radius', '0 0 25px 25px')
'<td style="width: 50%; background-color: #' .. type2Colors.dark .. '; border-radius: 1px 25px 25px 1px;">' ..
        :wikitext([=[
'[[' .. data.type2 .. '_(type)|<span style="color: #FFFFFF;">' .. data.type2 .. '</span>]]</td>'
* '''Emboldened''' moves denote that this Pokémon avails from a STAB bonus for it.
else
* ''Italicized'' moves are [[Broken_Moves_%26_Abilities|broken]]; see their individual pages for more information.
typesHtml =
* For more information on egg-move-tutors, refer to: [[Egg Moves]].
'<td style="width:100%; background-color: #' .. colors.dark .. '; border-radius: 25px;">' ..
]=])
'[[' .. data.type1 .. '_(type)|<span style="color: #FFFFFF;">' .. data.type1 .. '</span>]]</td>'
   
end
    return tostring(html) .. '<br clear="all">'
end


-- Base stats
-- Resolves type1/type2 for a given species name, preferring an explicit
local statCells, bst = {}, 0
-- override, falling back to the PokemonRawList lookup.
for _, stat in ipairs(STATS) do
local function resolveType(pokemonName, type1Override, type2Override)
local value = tonumber(data[stat.key]) or 0
    if type1Override ~= '' then
bst = bst + value
        return type1Override, type2Override
statCells[#statCells + 1] = statValueCell(frame, stat, value)
    end
end
    local typeData = loadPokemonTypes()
local bstCategory = math.floor(bst / 100 + 0.5) * 100
    local pt = typeData[pokemonName]
    if pt then
        return pt.type1 or '', pt.type2 or ''
    end
    return '', ''
end


-- EV yield.
local FORM_SUFFIXES = { 'Alolan', 'Galarian', 'Hisuian', 'Paldean' }
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.
function p.main(frame)
local catchRaw = tonumber(data.catch) or 0
    local args        = getArgs(frame)
local catchPercent = string.format('%.2f', catchRaw / 255 / 3 * 100)
    local pokemonName = args[1] or args.pokemon or ''


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


-- Header: dex number + icon, name.
    local primaryType, secondaryType = resolveType(pokemonName, args.type1 or '', args.type2 or '')
buf[#buf + 1] = '<tr><td style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; border-radius: 15px;"><span style="margin-bottom: 7px; font-weight: bold; color:#' ..
colors.dark .. ';">#' .. padNumber(data.number) .. '</span>&nbsp;&nbsp;&nbsp;[[File:' .. data.number ..
'Icon.png]]</td><td style="border: 3px 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.
    local function callRender(fn, name, type1, type2)
buf[#buf + 1] = '<tr><td colspan="3" style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
        return fn(frame:newChild({
colors.light .. '; border-radius: 15px; padding: 5px;"><div style="border-radius: 15px; ' ..
            title = frame:getTitle(),
'text-align: center; padding: 5px; background-color: #FFFFFF;">[[File:' .. data.number .. data.name ..
            args  = { name, type1 = type1, type2 = type2 },
'.png|150px]]</div></td></tr>'
        }))
    end


-- Types + Generation.
    local poke, lsErr = getCachedLearnset(pokemonName)
buf[#buf + 1] = '<tr><td style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
    if not poke then
colors.light .. '; padding: 11px; border-radius: 15px;"><p style="margin-bottom: 7px; font-weight: bold; ' ..
        return "<span style='color:red'>Data load error: " .. (lsErr or 'unknown') .. "</span>"
'color: #' .. colors.dark .. ';">Types</p><table align="center" style="width: 75%; font-weight: bold; ' ..
    end
'padding: 3px;"><tr>' .. typesHtml .. '</tr></table></td>' ..
'<td style="border: 3px 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: 20px; padding: 6px; border: 4px solid #' .. colors.dark ..
'; background-color: #FFFFFF;">&nbsp;&nbsp;[[:Category:Generation ' .. gen .. ' Pokémon|<span style="color:#' ..
colors.dark .. ';">' .. gen .. '</span>]]&nbsp;&nbsp;</span></td></tr>'


-- Abilities.
    local formName, formPoke, formType1, formType2
buf[#buf + 1] = '<tr><td colspan="6" style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
    for _, suffix in ipairs(FORM_SUFFIXES) do
colors.light .. '; padding: 11px; border-radius: 15px; width: 100%;"><table width="100%">' ..
        local candidateName = pokemonName .. '-' .. suffix
'<tr><th colspan="6"><p style="margin-bottom: 7px; font-weight: bold; color:#' .. colors.dark ..
        local candidatePoke = getCachedLearnset(candidateName)
';">[[Abilities|<span style="color:#' .. colors.dark .. ';">Abilities</span>]]</p></th></tr>' ..
        if candidatePoke then
'<tr>' .. abilitiesRow(colors, data) .. '</tr></table></td></tr>'
            formName = suffix
            formPoke = candidatePoke
            formType1, formType2 = resolveType(candidateName, '', '')
            break
        end
    end
    local formFullName = formPoke and (pokemonName .. '-' .. formName) or nil


-- Rarity + Catch rate.
    local sectionDefs = {
buf[#buf + 1] = '<tr>' ..
        { heading = 'Level-up', render = p.renderLevelUp,
infoCell(colors, '[[List of Pokémon by Rarity Tier|<span style="color:#' .. colors.dark ..
          hasData = function(pk) return #(pk.level_up or {}) > 0 end },
';">Rarity Tier</span>]]', '[[:Category:' .. data.rarity .. ' Pokémon|<span style="color:#' ..
        { heading = '[[TMs and HMs]]', render = p.renderTMsHMs,
colors.dark .. ';">' .. data.rarity .. '</span>]]') ..
          hasData = function(pk) return #(pk.tms or {}) > 0 or #(pk.hms or {}) > 0 end },
infoCell(colors, '[[Catch Rates|<span style="color:#' .. colors.dark .. ';">Catch rate</span>]]',
        { heading = '[[Move Tutors]]', render = p.renderMoveTutors,
data.catch .. '/255 (' .. catchPercent .. '%)') ..
          hasData = function(pk) return #(pk.move_tutors or {}) > 0 end },
'</tr>'
        { heading = '[[Egg Moves]]', render = p.renderEggMoves,
          hasData = function(pk) return #(pk.egg_moves or {}) > 0 end },
    }


-- Base stats.
    local sections = {}
buf[#buf + 1] = '<tr><td colspan="6" style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
colors.light .. '; border-radius: 15px; padding: 5px; width: 100%"><table width="100%">' ..
'<tr><th colspan="6"><p style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
';">[[Base stats|<span style="margin-bottom: 7px; font-weight: bold; color: #' .. colors.dark ..
';">Base stats</span>]]</p></th></tr><tr>' .. table.concat(statCells) .. '</tr>' ..
'<tr><td colspan="6"><div style="margin: auto; background-color:#FFFFFF; border: 3px solid #' ..
colors.dark .. '; padding: 6px; border-radius: 25px; margin-bottom: 5px; width: 50%;">' ..
'Base-stat total: ' .. bst .. '</div></td></tr></table></td></tr>'


-- EV yield.
    for _, def in ipairs(sectionDefs) do
buf[#buf + 1] = '<tr><td colspan="6" style="border: 3px solid #' .. colors.dark .. '; background-color: #' ..
        local normalHas = def.hasData(poke)
colors.light .. '; border-radius: 15px; padding: 5px; width: 100%"><table width="100%">' ..
        local formHas = formPoke and def.hasData(formPoke)
'<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: 3px solid #' ..
colors.dark .. '; padding: 6px; border-radius: 25px; margin-bottom: 5px; width: 50%;">' ..
'EV-yield total: ' .. evTotal .. '</div></td></tr></table></td></tr>'


-- Base experience + Gender ratio.
        if normalHas or formHas then
buf[#buf + 1] = '<tr>' ..
            table.insert(sections, "===" .. def.heading .. "===")
infoCell(colors, '[[Experience System#Base Experience|<span style="color:#' .. colors.dark ..
';">Base Experience</span>]]', data.exp) ..
'<td style="border: 3px 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>&nbsp;&nbsp;' .. genderRatioHtml(data.male) .. '&nbsp;&nbsp;</td>' ..
'</tr>'


-- Height + Weight.
            if formPoke then
buf[#buf + 1] = '<tr>' ..
                local normalBox = callRender(def.render, pokemonName, primaryType, secondaryType)
infoCell(colors, 'Height', data.height .. 'm') ..
                local formBox = callRender(def.render, formFullName, formType1, formType2)
infoCell(colors, 'Weight', data.weight .. 'kg') ..
                table.insert(sections,
'</tr>'
                    "{{TabbedInfoBoxes|Width=85|Class=|TabHeaders={{Tabs:MultiformTab|Form=Normal}}{{Tabs:MultiformTab|Form=" .. formName .. "}}|InfoBoxes=\n"
                    .. "{{Tabs:MultiformTableAdditional|Table=" .. normalBox .. "}}"
                    .. "{{Tabs:MultiformTableAdditional|Form=" .. formName .. "|Table=" .. formBox .. "}}\n}}")
            else
                table.insert(sections, callRender(def.render, pokemonName, primaryType, secondaryType))
            end
        end
    end


buf[#buf + 1] = '</table>'
    if #sections == 0 then
buf[#buf + 1] = categories(data, gen, bstCategory, args)
        return ''
    end


return table.concat(buf)
    return "==Moves==\n" .. table.concat(sections, '\n')
end
end


return p
return p

Revision as of 19:25, 9 July 2026

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

local p = {}

local function getArgs(frame)
    local args = {}
    local parent = frame:getParent()
    
    if parent then
        for k, v in pairs(parent.args) do
            if v ~= '' then
                args[k] = v
            end
        end
    end
    
    for k, v in pairs(frame.args) do
        if v ~= '' then
            args[k] = v
        end
    end
    
    return args
end

local moveDataCache = nil

local function blankToNil(v)
    if v == nil or v == '' then
        return nil
    end
    return v
end

local function loadMoveData()
    if moveDataCache then
        return moveDataCache
    end
    
    local csvData = mw.ext.externalData.getExternalData({
        url = 'https://wiki.pokemonrevolution.net/index.php?title=Special:GetData/RawMovesList',
        format = 'CSV with header',
        data = {
            Name = 'Name',
            Type = 'Type',
            Category = 'Category',
            Power = 'Power',
            Accuracy = 'Accuracy',
            PP = 'PP',
            Broken = 'Broken',
            Typeless = 'Typeless',
            Priority = 'Priority',
            TM = 'TM',
            ['Move Tutor'] = 'Move Tutor',
            Egg = 'Egg',
        },
    })

    moveDataCache = {}
    if csvData then
        for _, row in ipairs(csvData) do
            moveDataCache[row.Name] = {
                type = blankToNil(row.Type) or 'Normal',
                category = blankToNil(row.Category) or 'Status',
                power = blankToNil(row.Power),
                accuracy = blankToNil(row.Accuracy),
                pp = blankToNil(row.PP),
                broken = row.Broken or 'N',
                typeless = row.Typeless or 'N',
                priority = blankToNil(row.Priority) or '0',
                tm = blankToNil(row.TM),
                move_tutor = blankToNil(row['Move Tutor']),
                egg = blankToNil(row.Egg)
            }
        end
    end

    return moveDataCache
end

local function loadLearnset(pokemonName)
    local jsonPath = '$[' .. mw.text.jsonEncode(pokemonName) .. ']'
    local ok, result, errors = pcall(function()
        return mw.ext.externalData.getFileData({
            source = 'data',
            ['file name'] = 'Learnsets.json',
            format = 'JSON',
            ['use jsonpath'] = true,
            data = { poke = jsonPath },
        })
    end)
    if not ok then
        return nil, 'Lua error: ' .. tostring(result)
    end
    if errors and #errors > 0 then
        return nil, 'ExternalData error: ' .. table.concat(errors, ' | ')
    end
    local raw = result and result.poke
    if type(raw) ~= 'table' then
        return nil, 'No learnset data found for ' .. pokemonName
    end
    return raw, nil
end

local learnsetCache = {}
local learnsetErrorCache = {}

local function getCachedLearnset(pokemonName)
    if learnsetCache[pokemonName] == nil and learnsetErrorCache[pokemonName] == nil then
        learnsetCache[pokemonName], learnsetErrorCache[pokemonName] = loadLearnset(pokemonName)
    end
    return learnsetCache[pokemonName], learnsetErrorCache[pokemonName]
end

local pokemonTypeCache = nil

local function loadPokemonTypes()
    if pokemonTypeCache then return pokemonTypeCache end

    local csvData = mw.ext.externalData.getExternalData({
        url    = 'https://wiki.pokemonrevolution.net/index.php?title=Special:GetData/PokemonRawList',
        format = 'CSV with header',
        data = {
            Name = 'Name',
            Type1 = 'Type1',
            Type2 = 'Type2',
        },
    })

    pokemonTypeCache = {}
    if csvData then
        for _, row in ipairs(csvData) do
            pokemonTypeCache[row.Name] = {
                type1 = row.Type1 or '',
                type2 = row.Type2 or '',
            }
        end
    end
    return pokemonTypeCache
end

local typeColorCache = {}
local function typeColor(frame, pokeType)
    pokeType = (pokeType and pokeType ~= '') and 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 categoryColorCache = {}
local function categoryColor(frame, category)
    category = (category and category ~= '') and category or 'Status'
    if not categoryColorCache[category] then
        categoryColorCache[category] = {
            base = frame:expandTemplate{ title = category .. ' color' },
            dark = frame:expandTemplate{ title = category .. ' color dark' },
        }
    end
    return categoryColorCache[category]
end

local function isSTAB(moveType, primaryType, secondaryType)
    return moveType == primaryType or moveType == secondaryType
end

local function formatItemNumber(label, num)
    num = tonumber(num)
    if not num then
        return '—'
    end
    return label .. string.format('%02d', num)
end

function p.renderLevelUp(frame)
    local args = getArgs(frame)
    local pokemonName = args[1] or args.pokemon
    local primaryType = args.type1 or ""
    local secondaryType = args.type2 or ""
    
    local pokemon, lsErr = getCachedLearnset(pokemonName)
    local moveData = loadMoveData()

    if not pokemon then
        return "<span style='color:red'>Data load error: " .. (lsErr or 'unknown') .. "</span>"
    end

    local moves = pokemon.level_up or {}
    local colors = typeColor(frame, primaryType)

    local html = mw.html.create('div')
        :css('width', '77%')
        :css('margin', 'auto')
    local table = html:tag('table')
        :addClass('sortable')
        :css('width', '100%')
        :css('text-align', 'center')
        :css('border-radius', '15px 15px 0 0')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-bottom', 'none')
        :css('background-color', '#' .. colors.base)
        :css('padding', '7px')

        local header = table:tag('tr')
        :css('background-color', '#' .. colors.light)
        :css('color', '#' .. colors.dark)
    header:tag('th'):wikitext('Level'):css('width', '10%'):css('border-radius', '15px 1px 1px 1px')
    header:tag('th'):wikitext('Move'):css('width', '15%')
    header:tag('th'):wikitext('Type'):css('width', '17%')
    header:tag('th'):wikitext('Category'):css('width', '16%')
    header:tag('th'):wikitext('Base Power'):css('width', '17%')
    header:tag('th'):wikitext('Accuracy'):css('width', '15%')
    header:tag('th'):wikitext('PP'):css('width', '10%'):css('border-radius', '1px 15px 1px 1px')

        for _, move in ipairs(moves) do
        local moveName = move.move
        local level = move.level
        local moveInfo = moveData[moveName] or {}

        local row = table:tag('tr'):css('background-color', '#FFFFFF')
        row:tag('td'):wikitext(tostring(level))

        local moveLink = '[[' .. moveName .. ']]'
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
            moveLink = "'''" .. moveLink .. "'''"
        end
        if moveInfo.broken == "Y" then
            moveLink = "''" .. moveLink .. "''"
        end
        row:tag('td'):wikitext(moveLink)

        local moveTypeColors = typeColor(frame, moveInfo.type)
        row:tag('td')
            :css('background-color', '#' .. moveTypeColors.base)
            :css('border', '2px solid #' .. moveTypeColors.dark)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')

        local moveCatColors = categoryColor(frame, moveInfo.category)
        row:tag('td')
            :css('background-color', '#' .. moveCatColors.base)
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')

        row:tag('td'):wikitext(moveInfo.power or '—')
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end

    html:tag('div')
        :css('text-align', 'left')
        :css('background-color', '#' .. colors.light)
        :css('padding', '10px')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-top', 'none')
        :css('border-radius', '0 0 15px 15px')
        :wikitext([=[
* '''Emboldened''' moves denote this Pokémon avails from a STAB bonus for it.
* ''Italicized'' moves are [[Broken_Moves_%26_Abilities|broken]]; see their individual pages for more information.
* All moves subsumed on this list are relearnable via the [[Move Relearner]]s if lost.
]=])
    
    return tostring(html) .. '<br clear="all">'
end

function p.renderTMsHMs(frame)
    local args = getArgs(frame)
    local pokemonName = args[1] or args.pokemon
    local primaryType = args.type1 or ""
    local secondaryType = args.type2 or ""
    
    local pokemon = getCachedLearnset(pokemonName)
    local moveData = loadMoveData()

    if not pokemon then return '' end

    local tms = pokemon.tms or {}
    local hms = pokemon.hms or {}
    local colors = typeColor(frame, primaryType)

    local html = mw.html.create('div')
        :css('width', '77%')
        :css('margin', 'auto')
    local table = html:tag('table')
        :addClass('sortable')
        :css('width', '100%')
        :css('text-align', 'center')
        :css('border-radius', '25px 25px 0 0')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-bottom', 'none')
        :css('background-color', '#' .. colors.base)
        :css('padding', '7px')

    local header = table:tag('tr')
        :css('background-color', '#' .. colors.light)
        :css('color', '#' .. colors.dark)
    header:tag('th'):attr('colspan', '2'):wikitext('TM'):css('width', '10%'):css('border-top-left-radius', '25px')
    header:tag('th'):wikitext('Move'):css('width', '15%')
    header:tag('th'):wikitext('Type'):css('width', '17%')
    header:tag('th'):wikitext('Category'):css('width', '16%')
    header:tag('th'):wikitext('Base Power'):css('width', '17%')
    header:tag('th'):wikitext('Accuracy'):css('width', '15%')
    header:tag('th'):wikitext('PP'):css('width', '10%'):css('border-top-right-radius', '25px')

    for _, moveName in ipairs(tms) do
        local moveInfo = moveData[moveName] or {}
        local row = table:tag('tr'):css('background-color', '#FFFFFF')

        row:tag('td'):attr('colspan', '2'):wikitext(formatItemNumber('TM', moveInfo.tm))

        local moveLink = '[[' .. moveName .. ']]'
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
            moveLink = "'''" .. moveLink .. "'''"
        end
        if moveInfo.broken == "Y" then
            moveLink = "''" .. moveLink .. "''"
        end
        row:tag('td'):wikitext(moveLink)

        local moveTypeColors = typeColor(frame, moveInfo.type)
        row:tag('td')
            :css('background-color', '#' .. moveTypeColors.base)
            :css('border', '2px solid #' .. moveTypeColors.dark)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')

        local moveCatColors = categoryColor(frame, moveInfo.category)
        row:tag('td')
            :css('background-color', '#' .. moveCatColors.base)
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')

        row:tag('td'):wikitext(moveInfo.power or '—')
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end

    for _, moveName in ipairs(hms) do
        local moveInfo = moveData[moveName] or {}
        local row = table:tag('tr'):css('background-color', '#FFFFCC')

        row:tag('td'):attr('colspan', '2'):wikitext(formatItemNumber('HM', moveInfo.tm)):css('font-weight', 'bold')

        local moveLink = '[[' .. moveName .. ']]'
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
            moveLink = "'''" .. moveLink .. "'''"
        end
        if moveInfo.broken == "Y" then
            moveLink = "''" .. moveLink .. "''"
        end
        row:tag('td'):wikitext(moveLink)

        local moveTypeColors = typeColor(frame, moveInfo.type)
        row:tag('td')
            :css('background-color', '#' .. moveTypeColors.base)
            :css('border', '2px solid #' .. moveTypeColors.dark)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')

        local moveCatColors = categoryColor(frame, moveInfo.category)
        row:tag('td')
            :css('background-color', '#' .. moveCatColors.base)
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')

        row:tag('td'):wikitext(moveInfo.power or '—')
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end

    html:tag('div')
        :css('text-align', 'left')
        :css('background-color', '#' .. colors.light)
        :css('padding', '10px')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-top', 'none')
        :css('border-radius', '0 0 25px 25px')
        :wikitext([=[
* '''Emboldened''' moves denote that this Pokémon avails from a STAB bonus for it.
* ''Italicized'' moves are [[Broken_Moves_%26_Abilities|broken]]; see their individual pages for more information.
* Refer to [[TMs and HMs]] for information on each move's obtainability.
]=])
    
    return tostring(html) .. '<br clear="all">'
end

function p.renderMoveTutors(frame)
    local args = getArgs(frame)
    local pokemonName = args[1] or args.pokemon
    local primaryType = args.type1 or ""
    local secondaryType = args.type2 or ""
    
    local pokemon = getCachedLearnset(pokemonName)
    local moveData = loadMoveData()

    if not pokemon then return '' end

    local moves = pokemon.move_tutors or {}
    
    if #moves == 0 then
        return ""
    end
    
    local colors = typeColor(frame, primaryType)

    local html = mw.html.create('div')
        :css('width', '77%')
        :css('margin', 'auto')
    local table = html:tag('table')
        :addClass('sortable')
        :css('width', '100%')
        :css('text-align', 'center')
        :css('border-radius', '25px 25px 0 0')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-bottom', 'none')
        :css('background-color', '#' .. colors.base)
        :css('padding', '7px')

    local header = table:tag('tr')
        :css('background-color', '#' .. colors.light)
        :css('color', '#' .. colors.dark)
    header:tag('th'):wikitext('Move'):css('width', '20%'):css('border-top-left-radius', '25px')
    header:tag('th'):wikitext('Type'):css('width', '17%')
    header:tag('th'):wikitext('Category'):css('width', '16%')
    header:tag('th'):wikitext('Base Power'):css('width', '17%')
    header:tag('th'):wikitext('Accuracy'):css('width', '15%')
    header:tag('th'):wikitext('PP'):css('width', '15%'):css('border-top-right-radius', '25px')

    for _, moveName in ipairs(moves) do
        local moveInfo = moveData[moveName] or {}
        local row = table:tag('tr'):css('background-color', '#FFFFFF')

        local moveLink = '[[' .. moveName .. ']]'
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
            moveLink = "'''" .. moveLink .. "'''"
        end
        if moveInfo.broken == "Y" then
            moveLink = "''" .. moveLink .. "''"
        end
        row:tag('td'):wikitext(moveLink)

        local moveTypeColors = typeColor(frame, moveInfo.type)
        row:tag('td')
            :css('background-color', '#' .. moveTypeColors.base)
            :css('border', '2px solid #' .. moveTypeColors.dark)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')

        local moveCatColors = categoryColor(frame, moveInfo.category)
        row:tag('td')
            :css('background-color', '#' .. moveCatColors.base)
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')

        row:tag('td'):wikitext(moveInfo.power or '—')
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end

    html:tag('div')
        :css('text-align', 'left')
        :css('background-color', '#' .. colors.light)
        :css('padding', '10px')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-top', 'none')
        :css('border-radius', '0 0 25px 25px')
        :wikitext([=[
* '''Emboldened''' moves denote that this Pokémon avails from a STAB bonus for it.
* ''Italicized'' moves are [[Broken_Moves_%26_Abilities|broken]]; see their individual pages for more information.
* Refer to the [[Move Tutors]] article for tutorship information for all moves hereon.
]=])

    return tostring(html) .. '<br clear="all">'
end

function p.renderEggMoves(frame)
    local args = getArgs(frame)
    local pokemonName = args[1] or args.pokemon
    local primaryType = args.type1 or ""
    local secondaryType = args.type2 or ""
    
    local pokemon = getCachedLearnset(pokemonName)
    local moveData = loadMoveData()

    if not pokemon then return '' end

    local moves = pokemon.egg_moves or {}
    
    if #moves == 0 then
        return ""
    end
    
    local colors = typeColor(frame, primaryType)

    local html = mw.html.create('div')
        :css('width', '77%')
        :css('margin', 'auto')
    local table = html:tag('table')
        :addClass('sortable')
        :css('width', '100%')
        :css('text-align', 'center')
        :css('border-radius', '25px 25px 0 0')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-bottom', 'none')
        :css('background-color', '#' .. colors.base)
        :css('padding', '7px')

    local header = table:tag('tr')
        :css('background-color', '#' .. colors.light)
        :css('color', '#' .. colors.dark)
    header:tag('th'):wikitext('Move'):css('width', '20%'):css('border-top-left-radius', '25px')
    header:tag('th'):wikitext('Type'):css('width', '17%')
    header:tag('th'):wikitext('Category'):css('width', '16%')
    header:tag('th'):wikitext('Base Power'):css('width', '17%')
    header:tag('th'):wikitext('Accuracy'):css('width', '15%')
    header:tag('th'):wikitext('PP'):css('width', '15%'):css('border-top-right-radius', '25px')

    for _, moveName in ipairs(moves) do
        local moveInfo = moveData[moveName] or {}
        local row = table:tag('tr'):css('background-color', '#FFFFFF')

        local moveLink = '[[' .. moveName .. ']]'
        if isSTAB(moveInfo.type, primaryType, secondaryType) and moveInfo.category ~= "Status" then
            moveLink = "'''" .. moveLink .. "'''"
        end
        if moveInfo.broken == "Y" then
            moveLink = "''" .. moveLink .. "''"
        end
        row:tag('td'):wikitext(moveLink)

        local moveTypeColors = typeColor(frame, moveInfo.type)
        row:tag('td')
            :css('background-color', '#' .. moveTypeColors.base)
            :css('border', '2px solid #' .. moveTypeColors.dark)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')

        local moveCatColors = categoryColor(frame, moveInfo.category)
        row:tag('td')
            :css('background-color', '#' .. moveCatColors.base)
            :css('border', '2px solid #' .. moveCatColors.dark)
            :wikitext('[[' .. (moveInfo.category or 'Status') .. ' moves|<span style="color:#FFFFFF;">' .. (moveInfo.category or 'Status') .. '</span>]]')

        row:tag('td'):wikitext(moveInfo.power or '—')
        row:tag('td'):wikitext(moveInfo.accuracy or '—')
        row:tag('td'):wikitext(moveInfo.pp or '—')
    end

    html:tag('div')
        :css('text-align', 'left')
        :css('background-color', '#' .. colors.light)
        :css('padding', '10px')
        :css('border', '3px solid #' .. colors.dark)
        :css('border-top', 'none')
        :css('border-radius', '0 0 25px 25px')
        :wikitext([=[
* '''Emboldened''' moves denote that this Pokémon avails from a STAB bonus for it.
* ''Italicized'' moves are [[Broken_Moves_%26_Abilities|broken]]; see their individual pages for more information.
* For more information on egg-move-tutors, refer to: [[Egg Moves]].
]=])
    
    return tostring(html) .. '<br clear="all">'
end

-- Resolves type1/type2 for a given species name, preferring an explicit
-- override, falling back to the PokemonRawList lookup.
local function resolveType(pokemonName, type1Override, type2Override)
    if type1Override ~= '' then
        return type1Override, type2Override
    end
    local typeData = loadPokemonTypes()
    local pt = typeData[pokemonName]
    if pt then
        return pt.type1 or '', pt.type2 or ''
    end
    return '', ''
end

local FORM_SUFFIXES = { 'Alolan', 'Galarian', 'Hisuian', 'Paldean' }

function p.main(frame)
    local args        = getArgs(frame)
    local pokemonName = args[1] or args.pokemon or ''

    if pokemonName == '' then
        return "<span style='color:red'>Usage: {{Learnsets|PokemonName}}</span>"
    end

    local primaryType, secondaryType = resolveType(pokemonName, args.type1 or '', args.type2 or '')

    local function callRender(fn, name, type1, type2)
        return fn(frame:newChild({
            title = frame:getTitle(),
            args  = { name, type1 = type1, type2 = type2 },
        }))
    end

    local poke, lsErr = getCachedLearnset(pokemonName)
    if not poke then
        return "<span style='color:red'>Data load error: " .. (lsErr or 'unknown') .. "</span>"
    end

    local formName, formPoke, formType1, formType2
    for _, suffix in ipairs(FORM_SUFFIXES) do
        local candidateName = pokemonName .. '-' .. suffix
        local candidatePoke = getCachedLearnset(candidateName)
        if candidatePoke then
            formName = suffix
            formPoke = candidatePoke
            formType1, formType2 = resolveType(candidateName, '', '')
            break
        end
    end
    local formFullName = formPoke and (pokemonName .. '-' .. formName) or nil

    local sectionDefs = {
        { heading = 'Level-up', render = p.renderLevelUp,
          hasData = function(pk) return #(pk.level_up or {}) > 0 end },
        { heading = '[[TMs and HMs]]', render = p.renderTMsHMs,
          hasData = function(pk) return #(pk.tms or {}) > 0 or #(pk.hms or {}) > 0 end },
        { heading = '[[Move Tutors]]', render = p.renderMoveTutors,
          hasData = function(pk) return #(pk.move_tutors or {}) > 0 end },
        { heading = '[[Egg Moves]]', render = p.renderEggMoves,
          hasData = function(pk) return #(pk.egg_moves or {}) > 0 end },
    }

    local sections = {}

    for _, def in ipairs(sectionDefs) do
        local normalHas = def.hasData(poke)
        local formHas = formPoke and def.hasData(formPoke)

        if normalHas or formHas then
            table.insert(sections, "===" .. def.heading .. "===")

            if formPoke then
                local normalBox = callRender(def.render, pokemonName, primaryType, secondaryType)
                local formBox = callRender(def.render, formFullName, formType1, formType2)
                table.insert(sections,
                    "{{TabbedInfoBoxes|Width=85|Class=|TabHeaders={{Tabs:MultiformTab|Form=Normal}}{{Tabs:MultiformTab|Form=" .. formName .. "}}|InfoBoxes=\n"
                    .. "{{Tabs:MultiformTableAdditional|Table=" .. normalBox .. "}}"
                    .. "{{Tabs:MultiformTableAdditional|Form=" .. formName .. "|Table=" .. formBox .. "}}\n}}")
            else
                table.insert(sections, callRender(def.render, pokemonName, primaryType, secondaryType))
            end
        end
    end

    if #sections == 0 then
        return ''
    end

    return "==Moves==\n" .. table.concat(sections, '\n')
end

return p