Module:MoveLearnability
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