Module:Sandbox/C.Ezra.M/Pokémon

Documentation for this module may be created at Module:Sandbox/C.Ezra.M/Pokémon/doc

--- Individual modules of the Pokémon template.
--- Experimental template -- Work in progress!
--- Do not rely on this module in a production environment!

-- (c) 2024 C.Ezra.M <cezram AT proton DOT me>
-- This code is licensed under Creative Commons BY-NC-SA 2.5

local p = {}

-- library functions, will probably be moved somewhere else.
local function ifNilThen(x, y)
    if x == nil then
        return y
    else
        return x
    end
end

--- returns true if x is false, nil, 0, an empty string, or an empty table
local function isEmpty(x)
    if not x or x == "" or x == 0 then
        return true
    elseif type(x) == "table" and next(x) == nil then
        return true
    end
    return false
end

local function colorTemplate(f, typ)
    return f:expandTemplate { title = ifNilThen(typ, "") .. " color" }
end

local function colorLightTemplate(f, typ)
    return f:expandTemplate { title = ifNilThen(typ, "") .. " color light" }
end

-- template proper -- some functions are local to prevent them from being exported

local function convertPokemonName(name)
    local convert = {
        ["Nidoran♀️"] = "Nidoran",
        ["Nidoran♂️"] = "Nidoran",
        ["Mime Jr."] = "Mime Jr",
    }
    return ifNilThen(convert[name], name)
end

local function spriteGenderOrForm(name, form)
    local convert = {
        ["male"] = "-Male",
        ["male shiny"] = "-Male",
        ["female"] = "-Female",
        ["female shiny"] = "-Female",
    }
    return ifNilThen(convert[name], form)
end

local function isShiny(spriteGender)
    local convert = {
        ["male shiny"] = true,
        ["female shiny"] = true,
        ["shiny"] = true,
    }
    return ifNilThen(convert[spriteGender], false)
end

--- work in progress - currently only yields Sugimori artwork if the `img` argument is not supplied
local function getPokemonImage(f)
    local image = f.args.img
    if isEmpty(image) then
        image = table.concat({
            f.args.ndex,
            convertPokemonName(f.args.pokemon),
            ifNilThen(spriteGenderOrForm(f.args.spritegender, f.args.form), ""),
            ".png",
        })
    end
    return mw.ustring.format('<div class="image">[[File:%s|100x100px|alt=]]</div>', image)
    -- disable alt text, the sprite is pretty much used only for decoration,
    -- whatever would be said by the screen reader wouldn't really make sense
    -- (linking is still enabled)
end

--- work in progress
local function makeTag(f)
    local pokeLink = f.args.character
    local gender = ""
    local shinyStar = ""

    if isEmpty(pokeLink) then
        local nickname = f.args.nickname
        if isEmpty(nickname) then
            nickname = f.args.pokemon
        end
        pokeLink = f:expandTemplate { title = "p", args = { f.args.pokemon, nickname } }
    end

    if f.args.gender == "male" or f.args.gender == "♂️" then
        gender = f:expandTemplate { title = "male" }
    elseif f.args.gender == "female" or f.args.gender == "♀️" then
        gender = f:expandTemplate { title = "female" }
    elseif f.args.gender == "both" then
        gender = f:expandTemplate { title = "male" } .. "/" .. f:expandTemplate { title = "female" }
    end

    if isShiny(f.args.spritegender) then
        shinyStar = "[[File:ShinyVIIIStar.png|20px|link=Shiny Pokémon]]"
    end
    return mw.ustring.format('<div class="tag">%s%s Lv.%s%s</div>', pokeLink, gender, f.args.level, shinyStar)
end

local function typeColumn(f)
    local template = "[[File:%sIC SV.png|x20px|link=%s (type)|alt=%s]]"
    local t1 = f.args.type1
    local type1link = mw.ustring.format(template, t1, t1, t1)
    local type2link = ""
    if not isEmpty(f.args.type2) then
        local t2 = f.args.type2
        type2link = mw.ustring.format(template, t2, t2, t2)
    end
    return mw.ustring.format('<div class="type"><span class="field-name">Type:</span>%s%s</div>', type1link, type2link)
end

local function moveEntry(f, name, typ, cat)
    -- if move not given, represent its slot as — (em dash)
    if name == "" or name == nil then
        return '* <span class="move-name">—</span>'
    else
        local moveLink = f:expandTemplate { title = "m", args = { name } }
        -- mw.ustring.format is the same as string.format, but has Unicode support
        local typeImg = mw.ustring.format(
            '<span class="type-icon" style="background-color: #%s">[[File:%s icon.png|20px|link=%s (type)|alt=%s]]</span>',
            colorTemplate(f, typ), typ, typ, typ)
        if tonumber(f.args.gen) <= 3 then
            -- in Gen 3 and below, a move's category is determined from its type, so we can ignore the category icon
            return mw.ustring.format('* <span class="move-name">%s</span>%s', moveLink, typeImg)
        else
            -- using the same class for type and category icons
            local catImg = mw.ustring.format(
                '<span class="type-icon" style="background-color: #%s">[[File:%sIC SV.png|20px|link=%s move|alt=%s]]</span>',
                colorTemplate(f, cat), cat, cat, cat)
            return mw.ustring.format('* <span class="move-name">%s</span>%s%s', moveLink, typeImg, catImg)
        end
    end
end

local function secondaryInfoRow(name, data)
    return mw.ustring.format('! class="field-name" | %s\n| %s\n', name, data)
end

--- Returns a table of mechanics that existed in the given generation.
--- Currently only recognizes numbers, LG (Let's Go), and LA (Legends: Arceus)
local function mechanicsInGen(gen)
    gen = mw.ustring.lower(gen)
    return {
        -- ifNilThen is only used for comparisons, since Lua errors out when comparing any value with nil.
        -- Lua can check equality of nil with any non-nil value, but always returns false if so.
        gender = ifNilThen(tonumber(gen), 0) >= 2,
        held = ifNilThen(tonumber(gen), 0) >= 2 and gen ~= "lg" and gen ~= "la",
        ability = ifNilThen(tonumber(gen), 0) >= 3 and gen ~= "lg" and gen ~= "la",
        category = ifNilThen(tonumber(gen), 0) >= 4,
        spritegender = ifNilThen(tonumber(gen), 0) >= 4,
        megastone = gen == "lg", -- this is only used for LGPE - for other games with Mega Evolution, use the held item field.
        dynamax = tonumber(gen) == 8,
        tera = tonumber(gen) == 9,
    }
end

--- Checks if a mechanic exists in a given generation.
local function mechanicExistsInGen(gen, mechanic)
    return ifNilThen(mechanicsInGen(gen)[mechanic], false)
end

-- exported functions

function p.primaryInfo(f)
    return table.concat({ '<div class="primary">', getPokemonImage(f), makeTag(f), typeColumn(f), '</div>' })
end

--- work in progress - only the item, Ability, and Tera Type have been added
--- TODO: make a better generation checker (for game mechanics, sprites, etc.)
function p.secondaryInfo(f)
    local info = {}
    local gen = f.args.gen
    if not isEmpty(f.args.held) and mechanicExistsInGen(gen, "held") then
        info["held"] = f.args.held
    end
    if not isEmpty(f.args.ability) and mechanicExistsInGen(gen, "ability") then
        info["ability"] = f.args.ability
    end
    if not isEmpty(f.args.tera) and mechanicExistsInGen(gen, "tera") then
        info["tera"] = f.args.tera
    end
    local rows = {}
    if info["held"] then
        table.insert(
            rows,
            secondaryInfoRow(
                "Held item:",
                -- for now, it's safe to assume the image size is 24px, and no items need to link to a disambiguated page
                -- (with exceptions like the Poké Ball or Pearl items, but those almost never happen)
                mw.ustring.format("[[File:Bag %s Sprite.png|24px|alt=|link=%s]] [[%s]]", info["held"], info["held"], info["held"])
            )
        )
    end
    -- show if the held item mechanic exists but there is no held item
    if info["held"] == nil and mechanicExistsInGen(gen, "held") then
        table.insert(
            rows,
            secondaryInfoRow("Held item:", "None")
        )
    end
    if info["ability"] then
        table.insert(
            rows,
            secondaryInfoRow(
                "Ability:",
                f:expandTemplate { title = "a", args = { info["ability"] } }
            )
        )
    end
    if info["ability"] == nil and mechanicExistsInGen(gen, "ability") then
        table.insert(
            rows,
            secondaryInfoRow("Ability:", "None")
        )
    end
    if info["tera"] then
        table.insert(
            rows,
            secondaryInfoRow(
                "Tera Type:",
                mw.ustring.format("[[File:%sIC Tera.png|x24px|link=%s (type)|alt=%s]]", info["tera"], info["tera"], info["tera"])
            )
        )
    end
    if not isEmpty(rows) then
        return mw.ustring.format('<div class="secondary">\n{| class="data"\n%s|}</div>', table.concat(rows, "|-\n"))
    end
    return "" -- skip this div were the table empty
end

function p.moves(f)
    local moveTable = {
        { f.args.move1, f.args.move1type, f.args.move1cat },
        { f.args.move2, f.args.move2type, f.args.move2cat },
        { f.args.move3, f.args.move3type, f.args.move3cat },
        { f.args.move4, f.args.move4type, f.args.move4cat },
    }
    local moveTags = {}
    for k, mv in ipairs(moveTable) do
        moveTags[k] = moveEntry(f, mv[1], mv[2], mv[3])
    end
    return '<div class="moves"><span class="field-name">Moves:</span>\n' .. table.concat(moveTags, "\n") .. '</div>'
end

return p