Module:Learnsets

From Pokemon Revolution Online Wiki
Revision as of 14:57, 9 July 2026 by Cae (talk | contribs)
Jump to navigation Jump to search

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 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',
        },
    })

    -- getExternalData returns an array of row-tables here (csvData[i].Name),
    -- not column-arrays (csvData.Name[i]).
    moveDataCache = {}
    if csvData then
        for _, row in ipairs(csvData) do
            moveDataCache[row.Name] = {
                type = row.Type or 'Normal',
                category = row.Category or 'Status',
                power = row.Power,
                accuracy = row.Accuracy or '—',
                pp = row.PP,
                broken = row.Broken or 'N',
                typeless = row.Typeless or 'N',
                priority = row.Priority or '0',
                tm = row.TM,
                move_tutor = row['Move Tutor'],
                egg = 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

-- Loads Type1/Type2 for every Pokemon from the PokemonRawList CSV.
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',
        },
    })

    -- Same row-table shape as loadMoveData: csvData[i].Name, not csvData.Name[i].
    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 typeColors = {
    Normal = { bg = "A8A878", border = "6D6D4E" },
    Fire = { bg = "F08030", border = "9C531F" },
    Water = { bg = "6890F0", border = "445E9C" },
    Electric = { bg = "F8D030", border = "A1871F" },
    Grass = { bg = "78C850", border = "4E8234" },
    Ice = { bg = "98D8D8", border = "638D8D" },
    Fighting = { bg = "C03028", border = "7D1F1A" },
    Poison = { bg = "A040A0", border = "682A68" },
    Ground = { bg = "E0C068", border = "927D44" },
    Flying = { bg = "A890F0", border = "6D5E9C" },
    Psychic = { bg = "F85888", border = "A13959" },
    Bug = { bg = "A8B820", border = "6D7815" },
    Rock = { bg = "B8A038", border = "786824" },
    Ghost = { bg = "705898", border = "493963" },
    Dragon = { bg = "7038F8", border = "4924A1" },
    Dark = { bg = "705848", border = "49392F" },
    Steel = { bg = "B8B8D0", border = "787887" },
    Fairy = { bg = "EE99AC", border = "9B6470" },
    Shadow = { bg = "5A5A5A", border = "2D2D2D" }
}

local categoryColors = {
    Physical = { bg = "C92112", border = "7D1F1A" },
    Special = { bg = "4F5870", border = "353B51" },
    Status = { bg = "8C888C", border = "5A5A5A" }
}

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

local function getBorderColor(primaryType)
    return typeColors[primaryType] and typeColors[primaryType].border or "000000"
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 html = mw.html.create('div')
    local table = html:tag('table')
        :css('width', '77%')
        :css('text-align', 'center')
        :css('margin', 'auto')
        :css('border-radius', '15px')
        :css('border', '3px solid #' .. getBorderColor(primaryType))
        :css('background-color', '#F5F5F5')
        :css('padding', '7px')
    
        local header = table:tag('tr')
        :css('background-color', '#E0E0E0')
    header:tag('th'):wikitext('Level'):css('width', '14%'):css('border-radius', '15px 1px 1px 1px')
    header:tag('th'):wikitext('Move'):css('width', '17%')
    header:tag('th'):wikitext('Type'):css('width', '22%')
    header:tag('th'):wikitext('Category'):css('width', '21%')
    header:tag('th'):wikitext('Base Power'):css('width', '26%')
    header:tag('th'):wikitext('Accuracy'):css('width', '13%')
    header:tag('th'):wikitext('PP'):css('width', '5%'):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 typeColor = typeColors[moveInfo.type] or typeColors.Normal
        row:tag('td')
            :css('background-color', '#' .. typeColor.bg)
            :css('border', '2px solid #' .. typeColor.border)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
        
        local catColor = categoryColors[moveInfo.category] or categoryColors.Status
        row:tag('td')
            :css('background-color', '#' .. catColor.bg)
            :css('border', '2px solid #' .. catColor.border)
            :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
    
    table:tag('tr'):tag('td'):attr('colspan', '7')
        :css('text-align', 'left')
        :css('background-color', '#E0E0E0')
        :css('padding', '10px')
        :css('border-radius', '1px 1px 25px 25px')
        :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 html = mw.html.create('div')
    local table = html:tag('table')
        :css('width', '77%')
        :css('text-align', 'center')
        :css('margin', 'auto')
        :css('border-radius', '25px')
        :css('border', '3px solid #' .. getBorderColor(primaryType))
        :css('background-color', '#F5F5F5')
        :css('padding', '7px')
    
    local header = table:tag('tr')
        :css('background-color', '#E0E0E0')
    header:tag('th'):attr('colspan', '2'):wikitext('TM'):css('width', '11%'):css('border-top-left-radius', '25px')
    header:tag('th'):wikitext('Move'):css('width', '18%')
    header:tag('th'):wikitext('Type'):css('width', '17%')
    header:tag('th'):wikitext('Category'):css('width', '19%')
    header:tag('th'):wikitext('Base Power'):css('width', '26%')
    header:tag('th'):wikitext('Accuracy'):css('width', '20%')
    header:tag('th'):wikitext('PP'):css('width', '7%'):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')
        
        local tmNum = moveInfo.tm or ''
        row:tag('td'):attr('colspan', '2'):wikitext(tmNum ~= '' and tmNum or '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 typeColor = typeColors[moveInfo.type] or typeColors.Normal
        row:tag('td')
            :css('background-color', '#' .. typeColor.bg)
            :css('border', '2px solid #' .. typeColor.border)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
        
        local catColor = categoryColors[moveInfo.category] or categoryColors.Status
        row:tag('td')
            :css('background-color', '#' .. catColor.bg)
            :css('border', '2px solid #' .. catColor.border)
            :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('HM'):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 typeColor = typeColors[moveInfo.type] or typeColors.Normal
        row:tag('td')
            :css('background-color', '#' .. typeColor.bg)
            :css('border', '2px solid #' .. typeColor.border)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
        
        local catColor = categoryColors[moveInfo.category] or categoryColors.Status
        row:tag('td')
            :css('background-color', '#' .. catColor.bg)
            :css('border', '2px solid #' .. catColor.border)
            :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
    
    table:tag('tr'):tag('td'):attr('colspan', '8')
        :css('text-align', 'left')
        :css('background-color', '#E0E0E0')
        :css('padding', '10px')
        :css('border-radius', '1px 1px 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 html = mw.html.create('div')
    local table = html:tag('table')
        :css('width', '77%')
        :css('text-align', 'center')
        :css('margin', 'auto')
        :css('border-radius', '25px')
        :css('border', '3px solid #' .. getBorderColor(primaryType))
        :css('background-color', '#F5F5F5')
        :css('padding', '7px')
    
    local header = table:tag('tr'):css('background-color', '#E0E0E0')
    header:tag('th'):wikitext('Move'):css('border-top-left-radius', '25px')
    header:tag('th'):wikitext('Type')
    header:tag('th'):wikitext('Category')
    header:tag('th'):wikitext('Base Power')
    header:tag('th'):wikitext('Accuracy')
    header:tag('th'):wikitext('PP'):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 typeColor = typeColors[moveInfo.type] or typeColors.Normal
        row:tag('td')
            :css('background-color', '#' .. typeColor.bg)
            :css('border', '2px solid #' .. typeColor.border)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
        
        local catColor = categoryColors[moveInfo.category] or categoryColors.Status
        row:tag('td')
            :css('background-color', '#' .. catColor.bg)
            :css('border', '2px solid #' .. catColor.border)
            :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
    
    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 html = mw.html.create('div')
    local table = html:tag('table')
        :css('width', '77%')
        :css('text-align', 'center')
        :css('margin', 'auto')
        :css('border-radius', '25px')
        :css('border', '3px solid #' .. getBorderColor(primaryType))
        :css('background-color', '#F5F5F5')
        :css('padding', '7px')
    
    local header = table:tag('tr'):css('background-color', '#E0E0E0')
    header:tag('th'):wikitext('Move'):css('width', '15%'):css('border-top-left-radius', '25px')
    header:tag('th'):wikitext('Type'):css('width', '13%')
    header:tag('th'):wikitext('Category'):css('width', '14%')
    header:tag('th'):wikitext('Base Power'):css('width', '15%')
    header:tag('th'):wikitext('Accuracy'):css('width', '17%')
    header:tag('th'):wikitext('PP'):css('width', '18%'):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 typeColor = typeColors[moveInfo.type] or typeColors.Normal
        row:tag('td')
            :css('background-color', '#' .. typeColor.bg)
            :css('border', '2px solid #' .. typeColor.border)
            :wikitext('[[' .. (moveInfo.type or 'Normal') .. '_(type)|<span style="color:#FFFFFF;">' .. (moveInfo.type or 'Normal') .. '</span>]]')
        
        local catColor = categoryColors[moveInfo.category] or categoryColors.Status
        row:tag('td')
            :css('background-color', '#' .. catColor.bg)
            :css('border', '2px solid #' .. catColor.border)
            :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
    
    table:tag('tr'):tag('td'):attr('colspan', '6')
        :css('text-align', 'left')
        :css('background-color', '#E0E0E0')
        :css('padding', '10px')
        :css('border-radius', '1px 1px 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

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   = args.type1 or ''
    local secondaryType = args.type2 or ''

    if primaryType == '' then
        local typeData = loadPokemonTypes()
        local pt = typeData[pokemonName]
        if pt then
            primaryType   = pt.type1 or ''
            secondaryType = pt.type2 or ''
        end
    end

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

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

    local out = {}

    if #(poke.level_up or {}) > 0 then
        table.insert(out, "== Level-Up Moves ==")
        table.insert(out, callRender(p.renderLevelUp))
    end

    if #(poke.tms or {}) > 0 or #(poke.hms or {}) > 0 then
        table.insert(out, "== TM/HM Moves ==")
        table.insert(out, callRender(p.renderTMsHMs))
    end

    if #(poke.move_tutors or {}) > 0 then
        table.insert(out, "== Move Tutor Moves ==")
        table.insert(out, callRender(p.renderMoveTutors))
    end

    if #(poke.egg_moves or {}) > 0 then
        table.insert(out, "== Egg Moves ==")
        table.insert(out, callRender(p.renderEggMoves))
    end

    return table.concat(out, '\n')
end

return p