Mudanças entre as edições de "Módulo:Info"

De Wiki Gla
Ir para navegação Ir para pesquisar
m
Etiqueta: Reversão manual
m (test subskill)
Linha 15: Linha 15:
         return nil
         return nil
     end
     end
     local ok, data
     local ok, data = pcall(function()
 
    ok, data = pcall(function()
         return require("Módulo:" .. charName)
         return require("Módulo:" .. charName)
     end)
     end)
Linha 23: Linha 21:
         return data
         return data
     end
     end
 
    -- fallback: nomes com espaço vs underscore
    charName = charName:gsub(" ", "_")
     ok, data = pcall(function()
     ok, data = pcall(function()
         return require("Modulo:" .. charName)
         return require("Módulo:" .. charName)
     end)
     end)
     if ok and type(data) == "table" then
     if ok and type(data) == "table" then
         return data
         return data
     end
     end
    return nil
end


    ok, data = pcall(function()
-- Coleta objetos JSON em sequência a partir de um texto concatenado (ex.: |subs= com vários {{Subskill}})
        return require("Module:" .. charName)
local function collectJsonObjects(s)
     end)
    s = tostring(s or "")
     if ok and type(data) == "table" then
     local out = {}
         return data
     for chunk in s:gmatch("%b{}") do
         table.insert(out, chunk)
     end
     end
 
     return out
     return nil
end
end


-- resolve personagem a partir de frame/parent (usado por expand/tier/tags/skill)
local function resolveCharFromFrames(frame, a)
local function resolveCharFromFrames(frame, a)
     if a.char and trim(a.char) ~= "" then
     a = a or {}
        return trim(a.char)
    -- tenta parent args.nome
    if frame and frame.getParent then
        local parent = frame:getParent()
        if parent and parent.args then
            if parent.args.nome and trim(parent.args.nome) ~= "" then
                return trim(parent.args.nome)
            end
            -- alguns usos guardam em classX (tierX/tagsX)
            for k, v in pairs(parent.args) do
                if type(k) == "string" and k:match("^class") and trim(v) ~= "" then
                    return trim(v)
                end
            end
        end
     end
     end
    -- tenta o próprio frame (às vezes vem direto)
     if a.nome and trim(a.nome) ~= "" then
     if a.nome and trim(a.nome) ~= "" then
         return trim(a.nome)
         return trim(a.nome)
     end
     end
 
     if a.char and trim(a.char) ~= "" then
    local p1 = frame:getParent()
         return trim(a.char)
     if p1 then
        local ap1 = safeArgs(p1)
        if ap1.char and trim(ap1.char) ~= "" then
            return trim(ap1.char)
        end
         if ap1.nome and trim(ap1.nome) ~= "" then
            return trim(ap1.nome)
        end
 
        local p2 = p1.getParent and p1:getParent()
        if p2 then
            local ap2 = safeArgs(p2)
            if ap2.char and trim(ap2.char) ~= "" then
                return trim(ap2.char)
            end
            if ap2.nome and trim(ap2.nome) ~= "" then
                return trim(ap2.nome)
            end
        end
     end
     end
 
    -- título da página
     local title = mw.title.getCurrentTitle()
     local title = mw.title.getCurrentTitle()
     return title and trim(title.text) or ""
     return title and trim(title.text) or ""
Linha 78: Linha 75:
local COLOR = {
local COLOR = {
     debuff = "#ff5252",
     debuff = "#ff5252",
     atk = "#ffcc00",
     ms    = "#5bc0de",
     def = "#64b5f6",
     hp    = "#70d86b",
     ms = "#43a047",
     atk    = "#ffd166",
     hp = "#42a5f5",
     def    = "#c0a9ff",
     sec = "#ffcc00"
     sec   = "#ffa07a",
}
}


local function colorize(txt)
-- substitui marcadores {{atk:}}, {{def:}}, {{ms:}}, {{hp:}}, {{sec:}}, {{debuff:}} no texto
     if not txt or txt == "" then
local function colorize(text)
         return ""
    text = tostring(text or "")
    local patterns = {
        { "atk",  COLOR.atk  },
        { "def",  COLOR.def  },
        { "ms",    COLOR.ms    },
        { "hp",    COLOR.hp    },
        { "sec",  COLOR.sec  },
        { "debuff",COLOR.debuff},
    }
    for _, pair in ipairs(patterns) do
        local key, color = pair[1], pair[2]
        -- {{key:VALOR}} → <span class="tag key">VALOR</span>
        text = text:gsub("{{%s*" .. key .. "%s*:%s*([^}]+)%s*}}", function(val)
            return string.format('<span class="tag %s" style="--tag:%s">%s</span>', key, color, trim(val))
        end)
    end
    return text
end
 
-- ===== Tier/Tags helpers =====
local function expandTierFrom(data, lang)
    lang = (lang or "pt"):lower()
    -- tenta i18n
     if data.tier_i18n and data.tier_i18n[lang] then
        return tostring(data.tier_i18n[lang])
    end
    return tostring(data.tier or "")
end
 
local function expandTagsFrom(data, lang)
    lang = (lang or "pt"):lower()
    if data.tags_i18n and data.tags_i18n[lang] and type(data.tags_i18n[lang]) == "table" then
         return table.concat(data.tags_i18n[lang], " / ")
    end
    if type(data.tags) == "table" then
        return table.concat(data.tags, " / ")
     end
     end
     return (txt:gsub("{{(%a+):([^}]+)}}", function(tag, inner)
     return tostring(data.tags or "")
        local hex = COLOR[tag]
        if not hex then
            return "{{" .. tag .. ":" .. inner .. "}}"
        end
        return string.format('<span style="color:%s;">%s</span>', hex, inner)
    end))
end
end


-- ===== API =====
-- ===== API =====


function p.getTier(frame)
-- Tier: {{#invoke:Info|tier|pt|NomeDoPersonagem}}  ou {{#invoke:Info|tier|tierNome}}
     local a = safeArgs(frame)
function p.tier(frame)
     local char = trim(frame.args[1] or a.nome)
     local token = trim(frame.args[1] or "")
     local nome  = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
    local lang  = trim(frame.args.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower()
    if token == "" then
        return ""
    end
    local char = ""
    if token:lower():sub(1,4) == "tier" then
        char = token:match("^tier(.+)$") or ""
    else
        char = nome
    end
     if char == "" then
     if char == "" then
         char = resolveCharFromFrames(frame, a)
         char = resolveCharFromFrames(frame, frame.args)
     end
     end
     local data = requireCharacterModule(char) or {}
     local data = requireCharacterModule(char) or {}
     return trim(data.tier or "")
     return expandTierFrom(data, lang)
end
end


function p.getTags(frame)
-- Tags: {{#invoke:Info|tags|pt|NomeDoPersonagem}}  ou {{#invoke:Info|tags|tagsNome}}
     local a = safeArgs(frame)
function p.tags(frame)
     local char = trim(frame.args[1] or a.nome)
     local token = trim(frame.args[1] or "")
     local nome  = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
    local lang  = trim(frame.args.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower()
    if token == "" then
        return ""
    end
    local char = ""
    if token:lower():sub(1,4) == "tags" then
        char = token:match("^tags(.+)$") or ""
    else
        char = nome
    end
     if char == "" then
     if char == "" then
         char = resolveCharFromFrames(frame, a)
         char = resolveCharFromFrames(frame, frame.args)
     end
     end
     local data = requireCharacterModule(char) or {}
     local data = requireCharacterModule(char) or {}
     local tags = data.tags or {}
     return expandTagsFrom(data, lang)
    if type(tags) ~= "table" then
        return ""
    end
    return trim(table.concat(tags, " / "))
end
end


-- Skill (normal: com M)
-- Skill principal → serializa objeto consumido pelo Widget/Character
-- Uso: {{#invoke:Info|skill|M=1|icon=...|level=...|energy=...|powerpve=...|powerpvp=...|cooldown=...|video=...|subs=...}}
function p.skill(frame)
function p.skill(frame)
     local a = safeArgs(frame)
     local a = safeArgs(frame)
Linha 171: Linha 216:
         powerpvp = nz(a.powerpvp),
         powerpvp = nz(a.powerpvp),
         cooldown = nz(a.cooldown),
         cooldown = nz(a.cooldown),
         video = a.video or ""
         video = a.video or "",
        name = name, -- pode estar vazio caso use outside (não quebra)
     }
     }
    if name and name ~= "" then
 
        obj.name = name
     -- inclui a descrição escolhida (se preferir, o Widget usa desc_i18n)
     end
     if chosen ~= "" then
     if chosen ~= "" then
         obj.desc = chosen
         obj.desc = chosen
Linha 187: Linha 232:
         obj.descEs = desc_i18n.es
         obj.descEs = desc_i18n.es
         obj.descPl = desc_i18n.pl
         obj.descPl = desc_i18n.pl
    end
    -- Subskills: empacota os JSONs vindos via |subs= (cada {{Subskill}} vira um objeto)
    do
        local subsPacked = a.subs or ""
        local subsArr = {}
        for _, chunk in ipairs(collectJsonObjects(subsPacked)) do
            local ok, sub = pcall(mw.text.jsonDecode, chunk)
            if ok and type(sub) == "table" then
                table.insert(subsArr, sub)
            end
        end
        if #subsArr > 0 then
            obj.subs = subsArr
        end
        -- Se o módulo do personagem definiu suborder para essa skill (pelo índice M), inclui para a UI respeitar a ordem
        if a.M and tonumber(a.M) then
            local m = tonumber(a.M)
            local order = (data and data.order) or {}
            local key = order[m] or ""
            local sk = (data and data.skills and data.skills[key]) or {}
            if type(sk) == "table" and type(sk.suborder) == "table" then
                obj.suborder = sk.suborder
            end
        end
     end
     end


     return mw.text.jsonEncode(obj)
     return mw.text.jsonEncode(obj)
end
end
-- Skill (emote: sem M)
 
-- Emote (sem M)
function p.emote(frame)
function p.emote(frame)
     local a = safeArgs(frame)
     local a = safeArgs(frame)
     local obj = {
     local obj = {
         name = "Emote",
         name = "Emote",
        desc = "",
         icon = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
         icon = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
         level = 25,
         level = (trim(a.level or "") ~= "" and a.level or "NIVEL"),
         video = a.video or "",
         video = a.video or "",
        desc_i18n = {
            pt = "",
            en = "",
            es = "",
            pl = ""
        },
        descPt = "",
        descEn = "",
        descEs = "",
        descPl = ""
     }
     }
     return mw.text.jsonEncode(obj)
     return mw.text.jsonEncode(obj)
end
end


-- expandTier/expandTags mantidos
-- Subskill → serializa dados da subskill (nome exato conforme suborder do módulo do personagem)
-- Uso: dentro de |subs= da Skill:
--  {{Subskill|n=Trap Door|icon=...|level=...|video=...|energy=...|powerpve=...|powerpvp=...|cooldown=...}}
function p.subskill(frame)
    local a = safeArgs(frame)
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    local n = trim(a.n or "")
    if n == "" then
        return mw.text.jsonEncode({})
    end
 
    -- Resolve personagem pelo contexto
    local char = trim(a.char or (frame:getParent() and frame:getParent().args.nome) or "")
    if char == "" then
        char = resolveCharFromFrames(frame, a)
    end
 
    local data = requireCharacterModule(char) or {}
 
    -- Busca a subskill "n" dentro das skills
    local desc_i18n = {}
    for _, key in ipairs(data.order or {}) do
        local sk = (data.skills or {})[key]
        if type(sk) == "table" and type(sk.subskills) == "table" then
            local sub = sk.subskills[n]
            if sub then
                local langs = {"pt","en","es","pl"}
                for _, code in ipairs(langs) do
                    local d = sub.desc and sub.desc[code]
                    if d and trim(d) ~= "" then
                        desc_i18n[code] = colorize(d)
                    end
                end
                break
            end
        end
    end
 
    local function nz(v)
        v = v or ""
        return (trim(v) ~= "" and v or nil)
    end
 
    local obj = {
        name  = n,
        icon  = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
        level = (trim(a.level or "") ~= "" and a.level or "NIVEL"),
        video = a.video or "",
        energy  = nz(a.energy),
        powerpve = nz(a.powerpve),
        powerpvp = nz(a.powerpvp),
        cooldown = nz(a.cooldown),
    }
 
    if next(desc_i18n) ~= nil then
        obj.desc_i18n = desc_i18n
        obj.descPt = desc_i18n.pt
        obj.descEn = desc_i18n.en
        obj.descEs = desc_i18n.es
        obj.descPl = desc_i18n.pl
    end
 
    return mw.text.jsonEncode(obj)
end
 
-- ===== atalhos “gambiarra” para wikitext simples =====
-- permitem usar {{#invoke:Info|expandTier|tierKalifa}} e {{#invoke:Info|expandTags|tagsKalifa}}
function p.expandTier(frame)
function p.expandTier(frame)
     local token = trim(frame.args[1] or "")
     local token = trim(frame.args[1] or "")
Linha 230: Linha 357:
     end
     end
     local data = requireCharacterModule(char) or {}
     local data = requireCharacterModule(char) or {}
     local i18n = (data.tier_i18n and data.tier_i18n.pt) or data.tier
     return expandTierFrom(data, (frame.args.lang or "pt"))
    return trim(i18n or "")
end
end


Linha 240: Linha 366:
         return ""
         return ""
     end
     end
     if token:lower():sub(1, 5) ~= "class" then
     if token:lower():sub(1, 4) ~= "tags" then
         return token
         return token
     end
     end
     local char = nome ~= "" and nome or token:match("^class(.+)$") or ""
     local char = nome ~= "" and nome or token:match("^tags(.+)$") or ""
     if char == "" then
     if char == "" then
         local title = mw.title.getCurrentTitle()
         local title = mw.title.getCurrentTitle()

Edição das 03h15min de 14 de setembro de 2025

A documentação para este módulo pode ser criada em Módulo:Info/doc

-- Módulo:Info — resolve metadados e skills (robusto/corrigido + gambiarra expandTier/expandTags)
local p = {}

-- ===== util =====
local function trim(s)
    return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
end
local function safeArgs(node)
    return (node and node.args) or {}
end

local function requireCharacterModule(charName)
    charName = trim(charName)
    if charName == "" then
        return nil
    end
    local ok, data = pcall(function()
        return require("Módulo:" .. charName)
    end)
    if ok and type(data) == "table" then
        return data
    end
    -- fallback: nomes com espaço vs underscore
    charName = charName:gsub(" ", "_")
    ok, data = pcall(function()
        return require("Módulo:" .. charName)
    end)
    if ok and type(data) == "table" then
        return data
    end
    return nil
end

-- Coleta objetos JSON em sequência a partir de um texto concatenado (ex.: |subs= com vários {{Subskill}})
local function collectJsonObjects(s)
    s = tostring(s or "")
    local out = {}
    for chunk in s:gmatch("%b{}") do
        table.insert(out, chunk)
    end
    return out
end

-- resolve personagem a partir de frame/parent (usado por expand/tier/tags/skill)
local function resolveCharFromFrames(frame, a)
    a = a or {}
    -- tenta parent args.nome
    if frame and frame.getParent then
        local parent = frame:getParent()
        if parent and parent.args then
            if parent.args.nome and trim(parent.args.nome) ~= "" then
                return trim(parent.args.nome)
            end
            -- alguns usos guardam em classX (tierX/tagsX)
            for k, v in pairs(parent.args) do
                if type(k) == "string" and k:match("^class") and trim(v) ~= "" then
                    return trim(v)
                end
            end
        end
    end
    -- tenta o próprio frame (às vezes vem direto)
    if a.nome and trim(a.nome) ~= "" then
        return trim(a.nome)
    end
    if a.char and trim(a.char) ~= "" then
        return trim(a.char)
    end
    -- título da página
    local title = mw.title.getCurrentTitle()
    return title and trim(title.text) or ""
end

-- paleta para marcadores
local COLOR = {
    debuff = "#ff5252",
    ms     = "#5bc0de",
    hp     = "#70d86b",
    atk    = "#ffd166",
    def    = "#c0a9ff",
    sec    = "#ffa07a",
}

-- substitui marcadores {{atk:}}, {{def:}}, {{ms:}}, {{hp:}}, {{sec:}}, {{debuff:}} no texto
local function colorize(text)
    text = tostring(text or "")
    local patterns = {
        { "atk",   COLOR.atk   },
        { "def",   COLOR.def   },
        { "ms",    COLOR.ms    },
        { "hp",    COLOR.hp    },
        { "sec",   COLOR.sec   },
        { "debuff",COLOR.debuff},
    }
    for _, pair in ipairs(patterns) do
        local key, color = pair[1], pair[2]
        -- {{key:VALOR}} → <span class="tag key">VALOR</span>
        text = text:gsub("{{%s*" .. key .. "%s*:%s*([^}]+)%s*}}", function(val)
            return string.format('<span class="tag %s" style="--tag:%s">%s</span>', key, color, trim(val))
        end)
    end
    return text
end

-- ===== Tier/Tags helpers =====
local function expandTierFrom(data, lang)
    lang = (lang or "pt"):lower()
    -- tenta i18n
    if data.tier_i18n and data.tier_i18n[lang] then
        return tostring(data.tier_i18n[lang])
    end
    return tostring(data.tier or "")
end

local function expandTagsFrom(data, lang)
    lang = (lang or "pt"):lower()
    if data.tags_i18n and data.tags_i18n[lang] and type(data.tags_i18n[lang]) == "table" then
        return table.concat(data.tags_i18n[lang], " / ")
    end
    if type(data.tags) == "table" then
        return table.concat(data.tags, " / ")
    end
    return tostring(data.tags or "")
end

-- ===== API =====

-- Tier: {{#invoke:Info|tier|pt|NomeDoPersonagem}}  ou {{#invoke:Info|tier|tierNome}}
function p.tier(frame)
    local token = trim(frame.args[1] or "")
    local nome  = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
    local lang  = trim(frame.args.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower()
    if token == "" then
        return ""
    end
    local char = ""
    if token:lower():sub(1,4) == "tier" then
        char = token:match("^tier(.+)$") or ""
    else
        char = nome
    end
    if char == "" then
        char = resolveCharFromFrames(frame, frame.args)
    end
    local data = requireCharacterModule(char) or {}
    return expandTierFrom(data, lang)
end

-- Tags: {{#invoke:Info|tags|pt|NomeDoPersonagem}}  ou {{#invoke:Info|tags|tagsNome}}
function p.tags(frame)
    local token = trim(frame.args[1] or "")
    local nome  = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
    local lang  = trim(frame.args.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower()
    if token == "" then
        return ""
    end
    local char = ""
    if token:lower():sub(1,4) == "tags" then
        char = token:match("^tags(.+)$") or ""
    else
        char = nome
    end
    if char == "" then
        char = resolveCharFromFrames(frame, frame.args)
    end
    local data = requireCharacterModule(char) or {}
    return expandTagsFrom(data, lang)
end

-- Skill principal → serializa objeto consumido pelo Widget/Character
-- Uso: {{#invoke:Info|skill|M=1|icon=...|level=...|energy=...|powerpve=...|powerpvp=...|cooldown=...|video=...|subs=...}}
function p.skill(frame)
    local a = safeArgs(frame)
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    local char = trim(a.char or (frame:getParent() and frame:getParent().args.nome) or "")
    if char == "" then
        char = resolveCharFromFrames(frame, a)
    end
    local data = requireCharacterModule(char) or {}

    local name, desc_i18n = nil, {}

    if a.M and trim(a.M) ~= "" then
        local m = tonumber(a.M) or 0
        local order = data.order or {}
        local key = order[m] or ""
        local sk = (data.skills or {})[key] or {}
        name = trim(sk.name or key or "")

        -- monta tabela de descrições por idioma (já colorizadas)
        if type(sk.desc) == "table" then
            local langs = {"pt", "en", "es", "pl"}
            for _, code in ipairs(langs) do
                local d = sk.desc[code]
                if d and trim(d) ~= "" then
                    desc_i18n[code] = colorize(d)
                end
            end
        elseif sk.desc and trim(sk.desc) ~= "" then
            desc_i18n["pt"] = colorize(sk.desc) -- fallback: único texto vira pt
        end
    end

    local function nz(v)
        v = v or ""
        return (trim(v) ~= "" and v or nil)
    end

    local chosen = desc_i18n[lang] or desc_i18n["pt"] or ""

    local obj = {
        icon = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
        level = (trim(a.level or "") ~= "" and a.level or "NIVEL"),
        energy = nz(a.energy),
        powerpve = nz(a.powerpve),
        powerpvp = nz(a.powerpvp),
        cooldown = nz(a.cooldown),
        video = a.video or "",
        name = name, -- pode estar vazio caso use outside (não quebra)
    }

    -- inclui a descrição escolhida (se preferir, o Widget usa desc_i18n)
    if chosen ~= "" then
        obj.desc = chosen
    end

    -- inclui as variantes para o Character expor como data-desc-*
    if next(desc_i18n) ~= nil then
        obj.desc_i18n = desc_i18n
        obj.descPt = desc_i18n.pt
        obj.descEn = desc_i18n.en
        obj.descEs = desc_i18n.es
        obj.descPl = desc_i18n.pl
    end

    -- Subskills: empacota os JSONs vindos via |subs= (cada {{Subskill}} vira um objeto)
    do
        local subsPacked = a.subs or ""
        local subsArr = {}
        for _, chunk in ipairs(collectJsonObjects(subsPacked)) do
            local ok, sub = pcall(mw.text.jsonDecode, chunk)
            if ok and type(sub) == "table" then
                table.insert(subsArr, sub)
            end
        end
        if #subsArr > 0 then
            obj.subs = subsArr
        end

        -- Se o módulo do personagem definiu suborder para essa skill (pelo índice M), inclui para a UI respeitar a ordem
        if a.M and tonumber(a.M) then
            local m = tonumber(a.M)
            local order = (data and data.order) or {}
            local key = order[m] or ""
            local sk = (data and data.skills and data.skills[key]) or {}
            if type(sk) == "table" and type(sk.suborder) == "table" then
                obj.suborder = sk.suborder
            end
        end
    end

    return mw.text.jsonEncode(obj)
end

-- Emote (sem M)
function p.emote(frame)
    local a = safeArgs(frame)
    local obj = {
        name = "Emote",
        icon = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
        level = (trim(a.level or "") ~= "" and a.level or "NIVEL"),
        video = a.video or "",
    }
    return mw.text.jsonEncode(obj)
end

-- Subskill → serializa dados da subskill (nome exato conforme suborder do módulo do personagem)
-- Uso: dentro de |subs= da Skill:
--   {{Subskill|n=Trap Door|icon=...|level=...|video=...|energy=...|powerpve=...|powerpvp=...|cooldown=...}}
function p.subskill(frame)
    local a = safeArgs(frame)
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    local n = trim(a.n or "")
    if n == "" then
        return mw.text.jsonEncode({})
    end

    -- Resolve personagem pelo contexto
    local char = trim(a.char or (frame:getParent() and frame:getParent().args.nome) or "")
    if char == "" then
        char = resolveCharFromFrames(frame, a)
    end

    local data = requireCharacterModule(char) or {}

    -- Busca a subskill "n" dentro das skills
    local desc_i18n = {}
    for _, key in ipairs(data.order or {}) do
        local sk = (data.skills or {})[key]
        if type(sk) == "table" and type(sk.subskills) == "table" then
            local sub = sk.subskills[n]
            if sub then
                local langs = {"pt","en","es","pl"}
                for _, code in ipairs(langs) do
                    local d = sub.desc and sub.desc[code]
                    if d and trim(d) ~= "" then
                        desc_i18n[code] = colorize(d)
                    end
                end
                break
            end
        end
    end

    local function nz(v)
        v = v or ""
        return (trim(v) ~= "" and v or nil)
    end

    local obj = {
        name  = n,
        icon  = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
        level = (trim(a.level or "") ~= "" and a.level or "NIVEL"),
        video = a.video or "",
        energy   = nz(a.energy),
        powerpve = nz(a.powerpve),
        powerpvp = nz(a.powerpvp),
        cooldown = nz(a.cooldown),
    }

    if next(desc_i18n) ~= nil then
        obj.desc_i18n = desc_i18n
        obj.descPt = desc_i18n.pt
        obj.descEn = desc_i18n.en
        obj.descEs = desc_i18n.es
        obj.descPl = desc_i18n.pl
    end

    return mw.text.jsonEncode(obj)
end

-- ===== atalhos “gambiarra” para wikitext simples =====
-- permitem usar {{#invoke:Info|expandTier|tierKalifa}} e {{#invoke:Info|expandTags|tagsKalifa}}
function p.expandTier(frame)
    local token = trim(frame.args[1] or "")
    local nome = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
    if token == "" then
        return ""
    end
    if token:lower():sub(1, 4) ~= "tier" then
        return token
    end
    local char = nome ~= "" and nome or token:match("^tier(.+)$") or ""
    if char == "" then
        local title = mw.title.getCurrentTitle()
        char = title and trim(title.text) or ""
    end
    local data = requireCharacterModule(char) or {}
    return expandTierFrom(data, (frame.args.lang or "pt"))
end

function p.expandTags(frame)
    local token = trim(frame.args[1] or "")
    local nome = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
    if token == "" then
        return ""
    end
    if token:lower():sub(1, 4) ~= "tags" then
        return token
    end
    local char = nome ~= "" and nome or token:match("^tags(.+)$") or ""
    if char == "" then
        local title = mw.title.getCurrentTitle()
        char = title and trim(title.text) or ""
    end
    local data = requireCharacterModule(char) or {}
    local arr = (data.tags_i18n and data.tags_i18n.pt) or data.tags
    if type(arr) == "table" then
        return table.concat(arr, " / ")
    end
    return trim(arr or "")
end

return p