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

De Wiki Gla
Ir para navegação Ir para pesquisar
m (test subskill)
m (fix colorize (subskills))
Linha 1: Linha 1:
-- Módulo:Info — resolve metadados e skills (robusto/corrigido + gambiarra expandTier/expandTags)
-- Módulo:Info — resolve metadados e skills (com Subskill e cores inline)
local p = {}
local p = {}


Linha 21: Linha 21:
         return data
         return data
     end
     end
    -- fallback: nomes com espaço vs underscore
     charName = charName:gsub(" ", "_")
     charName = charName:gsub(" ", "_")
     ok, data = pcall(function()
     ok, data = pcall(function()
Linha 32: Linha 31:
end
end


-- Coleta objetos JSON em sequência a partir de um texto concatenado (ex.: |subs= com vários {{Subskill}})
-- Separa uma sequência de {} (JSONs colados) em uma tabela de chunks
local function collectJsonObjects(s)
local function collectJsonObjects(s)
     s = tostring(s or "")
     s = tostring(s or "")
Linha 42: Linha 41:
end
end


-- resolve personagem a partir de frame/parent (usado por expand/tier/tags/skill)
--------------------------------------------------------------------------------
-- I18N: rótulos de atributos + "Nível"
--------------------------------------------------------------------------------
local I18N = {
    pt = { level = "Nível", power = "Poder", power_pvp = "Poder PvP", energy = "Energia", energy_cost = "Energia", energy_gain = "Energia", cooldown = "Recarga" },
    en = { level = "Level", power = "Power", power_pvp = "Power PvP", energy = "Energy", energy_cost = "Energy", energy_gain = "Energy", cooldown = "Cooldown" },
    es = { level = "Nivel", power = "Poder", power_pvp = "Poder PvP", energy = "Energía", energy_cost = "Energía", energy_gain = "Energía", cooldown = "Enfriamiento" },
    pl = { level = "Poziom", power = "Moc", power_pvp = "Moc PvP", energy = "Energia", energy_cost = "Energia", energy_gain = "Energia", cooldown = "Odnawianie" },
}
 
local function resolveCharFromFrames(frame, a)
local function resolveCharFromFrames(frame, a)
     a = a or {}
     a = a or {}
    -- tenta parent args.nome
     if frame and frame.getParent then
     if frame and frame.getParent then
         local parent = frame:getParent()
         local parent = frame:getParent()
Linha 52: Linha 59:
                 return trim(parent.args.nome)
                 return trim(parent.args.nome)
             end
             end
            -- alguns usos guardam em classX (tierX/tagsX)
             for k, v in pairs(parent.args) do
             for k, v in pairs(parent.args) do
                 if type(k) == "string" and k:match("^class") and trim(v) ~= "" then
                 if type(k) == "string" and k:match("^class") and trim(v) ~= "" then
Linha 60: Linha 66:
         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)
Linha 67: Linha 72:
         return trim(a.char)
         return trim(a.char)
     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 ""
end
end


-- paleta para marcadores
-- mapa de cores (inline!)
local COLOR = {
local COLOR = {
     debuff = "#ff5252",
     debuff = "#ff5252",
Linha 79: Linha 83:
     atk    = "#ffd166",
     atk    = "#ffd166",
     def    = "#c0a9ff",
     def    = "#c0a9ff",
     sec    = "#ffa07a",
     sec    = "#ffcc00",
}
}


-- substitui marcadores {{atk:}}, {{def:}}, {{ms:}}, {{hp:}}, {{sec:}}, {{debuff:}} no texto
-- substitui {{atk:}}, {{def:}}, {{ms:}}, {{hp:}}, {{sec:}}, {{debuff:}} por <span style="color:#hex"></span>
local function colorize(text)
local function colorize(txt)
    text = tostring(text or "")
    if not txt or txt == "" then
    local patterns = {
         return ""
        { "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
     end
     return text
     return (tostring(txt):gsub("{{%s*(%a+)%s*:%s*([^}]+)%s*}}", function(tag, inner)
end
        tag = tag:lower()
 
        local hex = COLOR[tag]
-- ===== Tier/Tags helpers =====
        if not hex then
local function expandTierFrom(data, lang)
            return "{{" .. tag .. ":" .. inner .. "}}"
    lang = (lang or "pt"):lower()
        end
    -- tenta i18n
         return string.format('<span style="color:%s;">%s</span>', hex, trim(inner))
    if data.tier_i18n and data.tier_i18n[lang] then
     end))
        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
end


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


-- Tier: {{#invoke:Info|tier|pt|NomeDoPersonagem}}  ou {{#invoke:Info|tier|tierNome}}
-- Tier textual (i18n)
function p.tier(frame)
function p.getTier(frame)
     local token = trim(frame.args[1] or "")
     local a = safeArgs(frame)
     local nome  = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
     local char = trim(frame.args[1] or a.nome)
    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, frame.args)
         char = resolveCharFromFrames(frame, a)
     end
     end
     local data = requireCharacterModule(char) or {}
     local data = requireCharacterModule(char) or {}
     return expandTierFrom(data, lang)
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    if data.tier_i18n and data.tier_i18n[lang] then
        return data.tier_i18n[lang]
    end
     return trim(data.tier or "")
end
end


-- Tags: {{#invoke:Info|tags|pt|NomeDoPersonagem}}  ou {{#invoke:Info|tags|tagsNome}}
-- Tags textuais (i18n)
function p.tags(frame)
function p.getTags(frame)
     local token = trim(frame.args[1] or "")
     local a = safeArgs(frame)
     local nome  = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
     local char = trim(frame.args[1] or a.nome)
    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, frame.args)
         char = resolveCharFromFrames(frame, a)
     end
     end
     local data = requireCharacterModule(char) or {}
     local data = requireCharacterModule(char) or {}
     return expandTagsFrom(data, lang)
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    local tags = (data.tags_i18n and data.tags_i18n[lang]) or data.tags or {}
    if type(tags) ~= "table" then
        return trim(tags or "")
    end
     return trim(table.concat(tags, " / "))
end
end


-- Skill principal → serializa objeto consumido pelo Widget/Character
-- Skill (com M)
-- 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 180: Linha 145:


     local name, desc_i18n = nil, {}
     local name, desc_i18n = nil, {}
     if a.M and trim(a.M) ~= "" then
     if a.M and trim(a.M) ~= "" then
         local m = tonumber(a.M) or 0
         local m = tonumber(a.M) or 0
Linha 187: Linha 151:
         local sk = (data.skills or {})[key] or {}
         local sk = (data.skills or {})[key] or {}
         name = trim(sk.name 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
         if type(sk.desc) == "table" then
             local langs = {"pt", "en", "es", "pl"}
             local langs = {"pt","en","es","pl"}
             for _, code in ipairs(langs) do
             for _, code in ipairs(langs) do
                 local d = sk.desc[code]
                 local d = sk.desc[code]
Linha 198: Linha 160:
             end
             end
         elseif sk.desc and trim(sk.desc) ~= "" then
         elseif sk.desc and trim(sk.desc) ~= "" then
             desc_i18n["pt"] = colorize(sk.desc) -- fallback: único texto vira pt
             desc_i18n["pt"] = colorize(sk.desc)
         end
         end
     end
     end
Linha 217: Linha 179:
         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)
         name = name,
     }
     }


    -- inclui a descrição escolhida (se preferir, o Widget usa desc_i18n)
     if chosen ~= "" then obj.desc = chosen end
     if chosen ~= "" then
        obj.desc = chosen
    end
 
    -- inclui as variantes para o Character expor como data-desc-*
     if next(desc_i18n) ~= nil then
     if next(desc_i18n) ~= nil then
         obj.desc_i18n = desc_i18n
         obj.desc_i18n = desc_i18n
Linha 234: Linha 191:
     end
     end


     -- Subskills: empacota os JSONs vindos via |subs= (cada {{Subskill}} vira um objeto)
     -- empacota subskills vindas via |subs= (cada {{Subskill}} vira um objeto JSON)
     do
     do
         local subsPacked = a.subs or ""
         local subsPacked = a.subs or ""
Linha 248: Linha 205:
         end
         end


         -- Se o módulo do personagem definiu suborder para essa skill (pelo índice M), inclui para a UI respeitar a ordem
         -- suborder do módulo (se existir pra esta skill M)
         if a.M and tonumber(a.M) then
         if a.M and tonumber(a.M) then
             local m = tonumber(a.M)
             local m = tonumber(a.M)
Linha 268: Linha 225:
     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 = (trim(a.level or "") ~= "" and a.level or "NIVEL"),
         level = (trim(a.level or "") ~= "" and a.level or "NIVEL"),
Linha 275: Linha 233:
end
end


-- Subskill → serializa dados da subskill (nome exato conforme suborder do módulo do personagem)
-- Subskill (nome n EXATO do suborder; agrega desc_i18n do módulo + atributos locais)
-- Uso: dentro de |subs= da Skill:
--  {{Subskill|n=Trap Door|icon=...|level=...|video=...|energy=...|powerpve=...|powerpvp=...|cooldown=...}}
function p.subskill(frame)
function p.subskill(frame)
     local a = safeArgs(frame)
     local a = safeArgs(frame)
Linha 286: Linha 242:
     end
     end


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


    -- Busca a subskill "n" dentro das skills
     local desc_i18n = {}
     local desc_i18n = {}
     for _, key in ipairs(data.order or {}) do
     for _, key in ipairs(data.order or {}) do
Linha 313: Linha 266:
     end
     end


     local function nz(v)
     local function nz(v) v = v or ""; return (trim(v) ~= "" and v or nil) end
        v = v or ""
        return (trim(v) ~= "" and v or nil)
    end
 
     local obj = {
     local obj = {
         name  = n,
         name  = n,
Linha 334: Linha 283:
         obj.descEn = desc_i18n.en
         obj.descEn = desc_i18n.en
         obj.descEs = desc_i18n.es
         obj.descEs = desc_i18n.es
         obj.descPl = desc_i18n.pl
         obj.descPl = desc_ini18n and desc_i18n.pl or nil
     end
     end


Linha 340: Linha 289:
end
end


-- ===== atalhos “gambiarra” para wikitext simples =====
-- Gambis: expandir tier/tags via token "tierNome", "tagsNome"
-- 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 "")
     local nome = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
     local nome = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
     if token == "" then
     if token == "" then return "" end
        return ""
     if token:lower():sub(1,4) ~= "tier" then return token end
    end
     if token:lower():sub(1, 4) ~= "tier" then
        return token
    end
     local char = nome ~= "" and nome or token:match("^tier(.+)$") or ""
     local char = nome ~= "" and nome or token:match("^tier(.+)$") or ""
     if char == "" then
     if char == "" then
Linha 357: Linha 301:
     end
     end
     local data = requireCharacterModule(char) or {}
     local data = requireCharacterModule(char) or {}
     return expandTierFrom(data, (frame.args.lang or "pt"))
     local lang = trim((frame.args.lang or "pt"):lower())
    if data.tier_i18n and data.tier_i18n[lang] then return data.tier_i18n[lang] end
    return trim(data.tier or "")
end
end


Linha 363: Linha 309:
     local token = trim(frame.args[1] or "")
     local token = trim(frame.args[1] or "")
     local nome = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
     local nome = trim(frame.args[2] or (frame:getParent() and frame:getParent().args.nome) or "")
     if token == "" then
     if token == "" then return "" end
        return ""
     if token:lower():sub(1,4) ~= "tags" then return token end
    end
     if token:lower():sub(1, 4) ~= "tags" then
        return token
    end
     local char = nome ~= "" and nome or token:match("^tags(.+)$") or ""
     local char = nome ~= "" and nome or token:match("^tags(.+)$") or ""
     if char == "" then
     if char == "" then

Edição das 03h20min 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 (com Subskill e cores inline)
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
    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

-- Separa uma sequência de {} (JSONs colados) em uma tabela de chunks
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

--------------------------------------------------------------------------------
-- I18N: rótulos de atributos + "Nível"
--------------------------------------------------------------------------------
local I18N = {
    pt = { level = "Nível", power = "Poder", power_pvp = "Poder PvP", energy = "Energia", energy_cost = "Energia", energy_gain = "Energia", cooldown = "Recarga" },
    en = { level = "Level", power = "Power", power_pvp = "Power PvP", energy = "Energy", energy_cost = "Energy", energy_gain = "Energy", cooldown = "Cooldown" },
    es = { level = "Nivel", power = "Poder", power_pvp = "Poder PvP", energy = "Energía", energy_cost = "Energía", energy_gain = "Energía", cooldown = "Enfriamiento" },
    pl = { level = "Poziom", power = "Moc", power_pvp = "Moc PvP", energy = "Energia", energy_cost = "Energia", energy_gain = "Energia", cooldown = "Odnawianie" },
}

local function resolveCharFromFrames(frame, a)
    a = a or {}
    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
            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
    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
    local title = mw.title.getCurrentTitle()
    return title and trim(title.text) or ""
end

-- mapa de cores (inline!)
local COLOR = {
    debuff = "#ff5252",
    ms     = "#5bc0de",
    hp     = "#70d86b",
    atk    = "#ffd166",
    def    = "#c0a9ff",
    sec    = "#ffcc00",
}

-- substitui {{atk:}}, {{def:}}, {{ms:}}, {{hp:}}, {{sec:}}, {{debuff:}} por <span style="color:#hex">…</span>
local function colorize(txt)
    if not txt or txt == "" then
        return ""
    end
    return (tostring(txt):gsub("{{%s*(%a+)%s*:%s*([^}]+)%s*}}", function(tag, inner)
        tag = tag:lower()
        local hex = COLOR[tag]
        if not hex then
            return "{{" .. tag .. ":" .. inner .. "}}"
        end
        return string.format('<span style="color:%s;">%s</span>', hex, trim(inner))
    end))
end

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

-- Tier textual (i18n)
function p.getTier(frame)
    local a = safeArgs(frame)
    local char = trim(frame.args[1] or a.nome)
    if char == "" then
        char = resolveCharFromFrames(frame, a)
    end
    local data = requireCharacterModule(char) or {}
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    if data.tier_i18n and data.tier_i18n[lang] then
        return data.tier_i18n[lang]
    end
    return trim(data.tier or "")
end

-- Tags textuais (i18n)
function p.getTags(frame)
    local a = safeArgs(frame)
    local char = trim(frame.args[1] or a.nome)
    if char == "" then
        char = resolveCharFromFrames(frame, a)
    end
    local data = requireCharacterModule(char) or {}
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    local tags = (data.tags_i18n and data.tags_i18n[lang]) or data.tags or {}
    if type(tags) ~= "table" then
        return trim(tags or "")
    end
    return trim(table.concat(tags, " / "))
end

-- Skill (com M)
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 "")
        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)
        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,
    }

    if chosen ~= "" then obj.desc = chosen end
    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

    -- empacota subskills vindas via |subs= (cada {{Subskill}} vira um objeto JSON)
    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

        -- suborder do módulo (se existir pra esta skill M)
        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",
        desc = "",
        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 (nome n EXATO do suborder; agrega desc_i18n do módulo + atributos locais)
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

    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 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_ini18n and desc_i18n.pl or nil
    end

    return mw.text.jsonEncode(obj)
end

-- Gambis: expandir tier/tags via token "tierNome", "tagsNome"
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 {}
    local lang = trim((frame.args.lang or "pt"):lower())
    if data.tier_i18n and data.tier_i18n[lang] then return data.tier_i18n[lang] end
    return trim(data.tier or "")
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