Module:MoveLearnability

From Pokemon Revolution Online Wiki
Revision as of 13:46, 18 July 2026 by Cae (talk | contribs) (created page)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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

local p = {}

local PokemonForms = require('Module:PokemonForms')
local FORM_SUFFIXES = PokemonForms.FORM_SUFFIXES
local RAW_LIST_ALIASES = PokemonForms.RAW_LIST_ALIASES
local MOB_PREFIXES = PokemonForms.MOB_PREFIXES

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 function blankToNil(v)
    if v == nil or v == '' then
        return nil
    end
    return v
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',
        },
    })

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

    return moveDataCache
end

local pokemonDataCache = nil

local function loadPokemonData()
    if pokemonDataCache then
        return pokemonDataCache
    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',
            number = 'Pokedex Number',
            Type1 = 'Type1',
            Type2 = 'Type2',
        },
    })

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

local function loadMoveLearnset(moveName)
    local jsonPath = '$[' .. mw.text.jsonEncode(moveName) .. ']'
    local ok, result, errors = pcall(function()
        return mw.ext.externalData.getFileData({
            source = 'data',
            ['file name'] = 'Move_Learnsets.json',
            format = 'JSON',
            ['use jsonpath'] = true,
            data = { move = 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.move
    if type(raw) ~= 'table' then
        return nil, 'No learnability data found for ' .. moveName
    end
    return raw, nil
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 function hyphenSpaceVariant(s)
    if mw.ustring.find(s, '-', 1, true) then
        return (mw.ustring.gsub(s, '%-', ' '))
    end
    return (mw.ustring.gsub(s, ' ', '-'))
end

-- Learnset-style name -> exact PokemonRawList name, or nil.
local function resolveDirect(name, data)
    local aliased = RAW_LIST_ALIASES[name]
    if aliased and data[aliased] then
        return aliased
    end
    if data[name] then
        return name
    end
    local variant = hyphenSpaceVariant(name)
    if data[variant] then
        return variant
    end

    for _, form in ipairs(FORM_SUFFIXES) do
        local base
        if form.learnsetStyle == 'prefix-space' then
            local prefix = form.suffix .. ' '
            if mw.ustring.sub(name, 1, mw.ustring.len(prefix)) == prefix then
                base = mw.ustring.sub(name, mw.ustring.len(prefix) + 1)
            end
        else
            local tail = '-' .. form.suffix
            if mw.ustring.sub(name, -mw.ustring.len(tail)) == tail then
                base = mw.ustring.sub(name, 1, -mw.ustring.len(tail) - 1)
            end
        end

        if base then
            local rawName
            if form.rawList == 'suffix' then
                rawName = name
            elseif form.rawList == 'prefix-hyphen' then
                rawName = form.label .. '-' .. base
            elseif form.rawList == 'suffix-label' then
                rawName = base .. '-' .. form.label
            elseif form.rawList == 'suffix-space' then
                rawName = base .. ' ' .. form.label
            else
                rawName = form.label .. ' ' .. base
            end
            if data[rawName] then
                return rawName
            end
            local v2 = hyphenSpaceVariant(rawName)
            if data[v2] then
                return v2
            end
        end
    end

    return nil
end

-- Full resolution: direct match first; then special-mob prefix strip
-- ("Crystal Onix" -> Onix, keeping its own name and page link); then
-- progressive suffix strip for forms missing from the raw list
-- ("Arceus-Fire" -> Arceus, piped link to the base page).
local function resolvePokemon(name)
    local data = loadPokemonData()

    local rawName = resolveDirect(name, data)
    if rawName then
        return { raw = data[rawName], rawName = rawName, display = rawName, link = rawName, direct = true }
    end

    for _, prefix in ipairs(MOB_PREFIXES) do
        local head = prefix .. ' '
        if mw.ustring.sub(name, 1, mw.ustring.len(head)) == head then
            rawName = resolveDirect(mw.ustring.sub(name, mw.ustring.len(head) + 1), data)
            if rawName then
                return { raw = data[rawName], rawName = rawName, display = name, link = name, direct = false, mob = true }
            end
        end
    end

    local base = name
    while mw.ustring.find(base, '-', 1, true) do
        base = mw.ustring.gsub(base, '%-[^%-]*$', '')
        rawName = resolveDirect(base, data)
        if rawName then
            return { raw = data[rawName], rawName = rawName, display = name, link = rawName, direct = false }
        end
    end

    return { raw = nil, rawName = nil, display = name, link = name, direct = false }
end

-- Resolve, dedupe, and dex-sort one section's entries.
-- entries: list of { name = learnsetName, level = number|nil }.
local function prepareRows(entries)
    local rows = {}
    local byKey = {}

    for _, e in ipairs(entries) do
        local row = resolvePokemon(e.name)
        row.level = e.level

        local key = (row.rawName or row.display) .. '\0' .. tostring(e.level or '')
        local existing = byKey[key]
        if not existing then
            byKey[key] = row
            table.insert(rows, row)
        elseif row.direct and not existing.direct then
            for k, v in pairs(row) do
                existing[k] = v
            end
            existing.collapsed = nil
        elseif not row.direct and not existing.direct and not existing.mob then
            -- several raw-list-less forms of the same base; show the base once
            existing.collapsed = true
        end
    end

    table.sort(rows, function(a, b)
        local an = a.raw and tonumber(mw.ustring.match(a.raw.number or '', '^%d+')) or math.huge
        local bn = b.raw and tonumber(mw.ustring.match(b.raw.number or '', '^%d+')) or math.huge
        if an ~= bn then
            return an < bn
        end
        local as = a.raw and a.raw.number or ''
        local bs = b.raw and b.raw.number or ''
        if as ~= bs then
            return as < bs
        end
        return (a.display or '') < (b.display or '')
    end)

    return rows
end

local function padNumber(num)
    if mw.ustring.len(num) < 3 then
        return string.rep('0', 3 - mw.ustring.len(num)) .. num
    end
    return num
end

local function addTypeCell(frame, tr, pokeType)
    local td = tr:tag('td')
    if pokeType and pokeType ~= '' then
        local c = typeColor(frame, pokeType)
        td:css('background-color', '#' .. c.base)
            :css('border', '2px solid #' .. c.dark)
            :wikitext('[[' .. pokeType .. '_(type)|<span style="color:#FFFFFF;">' .. pokeType .. '</span>]]')
    else
        td:wikitext('—')
    end
end

local function renderTable(frame, entries, moveType, isDamaging, colors, withLevel)
    local rows = prepareRows(entries)

    local tbl = mw.html.create('table')
        :attr('cellspacing', '3')
        :css('width', withLevel and '62%' or '60%')
        :css('background-color', '#' .. colors.base)
        :css('border', '4px solid #' .. colors.dark)
        :css('padding', '4px')
        :css('border-radius', '25px')
        :css('text-align', 'center')

    local header = tbl:tag('tr')
        :css('color', '#' .. colors.dark)
        :css('background-color', '#' .. colors.light)
    local function th(text)
        return header:tag('th'):wikitext(text):css('border', '1px solid #' .. colors.dark)
    end
    th('No.'):attr('colspan', '2'):css('width', '17%'):css('border-top-left-radius', '25px')
    th('Pokémon'):css('width', '27%')
    local t1 = th('Type 1'):css('width', '20%')
    local t2 = th('Type 2'):css('width', '20%')
    if withLevel then
        th('Level'):css('width', '13%'):css('border-top-right-radius', '25px')
    else
        t2:css('border-top-right-radius', '25px')
    end

    for _, row in ipairs(rows) do
        local tr = tbl:tag('tr'):css('background-color', '#FFFFFF')

        local num = row.raw and row.raw.number
        tr:tag('td'):wikitext(num and padNumber(num) or '—')
        tr:tag('td'):wikitext(num and ('[[File:' .. num .. 'Icon.png]]') or '')

        local display = row.collapsed and row.rawName or row.display
        local target = row.collapsed and row.rawName or row.link
        local link
        if target == display then
            link = '[[' .. display .. ']]'
        else
            link = '[[' .. target .. '|' .. display .. ']]'
        end
        if isDamaging and row.raw and (row.raw.type1 == moveType or row.raw.type2 == moveType) then
            link = "'''" .. link .. "'''"
        end
        tr:tag('td'):wikitext(link)

        addTypeCell(frame, tr, row.raw and row.raw.type1)
        addTypeCell(frame, tr, row.raw and row.raw.type2)

        if withLevel then
            tr:tag('td'):wikitext(row.level and tostring(row.level) or '—')
        end
    end

    local footer = tbl:tag('tr')
    footer:tag('td')
        :attr('colspan', withLevel and '6' or '5')
        :css('text-align', 'left')
        :css('border', '1px solid #' .. colors.dark)
        :css('border-radius', '1px 1px 25px 25px')
        :css('background-color', '#' .. colors.light)
        :css('color', '#' .. colors.dark)
        :css('padding-left', '10px')
        :wikitext('<p style="margin-top: 8px;">\'\'\'Emboldened\'\'\' Pokémon avail from a STAB bonus for this move.</p>')

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

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

    if moveName == '' then
        local title = mw.title.getCurrentTitle()
        moveName = title and title.text or ''
    end
    if moveName == '' then
        return "<span style='color:red'>Usage: {{MoveLearnability|MoveName}}</span>"
    end

    local entry, err = loadMoveLearnset(moveName)
    if not entry then
        return "<span style='color:red'>Data load error: " .. (err or 'unknown') .. "</span>"
    end

    local moveInfo = loadMoveData()[moveName] or {}
    local moveType = moveInfo.type or 'Normal'
    local isDamaging = (moveInfo.category or 'Status') ~= 'Status'
    local colors = typeColor(frame, moveType)

    local sectionDefs = {
        { heading = 'Level-Up/Instinctual', key = 'level_up', withLevel = true },
        { heading = '[[TMs]]', key = 'tms' },
        { heading = '[[Move tutor]]', key = 'move_tutors' },
        { heading = '[[Egg-Inheritable Moves|Egg Moves]]', key = 'egg_moves' },
    }

    local sections = {}
    for _, def in ipairs(sectionDefs) do
        local raw = entry[def.key] or {}
        if #raw > 0 then
            local entries = {}
            for _, e in ipairs(raw) do
                if type(e) == 'table' then
                    table.insert(entries, { name = e.pokemon, level = e.level })
                else
                    table.insert(entries, { name = e })
                end
            end
            table.insert(sections, '===' .. def.heading .. '===')
            table.insert(sections, renderTable(frame, entries, moveType, isDamaging, colors, def.withLevel))
        end
    end

    if #sections == 0 then
        return ''
    end

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

return p