Mudanças entre as edições de "Módulo:I.Skills"

De Wiki Gla
Ir para navegação Ir para pesquisar
m
Etiqueta: Revertido
m
Etiqueta: Revertido
Linha 223: Linha 223:
                 subObj.desc_i18n = nil
                 subObj.desc_i18n = nil
             end
             end


             -- RECURSÃO NATIVA: processa sub-subskills (e sub-sub-subskills, etc.)
             -- RECURSÃO NATIVA: processa sub-subskills (e sub-sub-subskills, etc.)
Linha 424: Linha 423:
             if type(sk.subskills) == "table" and type(sk.suborder) == "table" then
             if type(sk.subskills) == "table" and type(sk.suborder) == "table" then
                 skillObj.subs = processSubskills(sk.subskills, sk.suborder, mainSkillsById, mainSkillsByName, skillObj
                 skillObj.subs = processSubskills(sk.subskills, sk.suborder, mainSkillsById, mainSkillsByName, skillObj
                    .id, moduleName)
                .id, moduleName)
                 skillObj.suborder = sk.suborder
                 skillObj.suborder = sk.suborder
             end
             end

Edição das 04h39min de 4 de janeiro de 2026

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

-- Módulo:I.Skills — skill(), skin() com suporte nativo a subskills recursivas
local p = {}

local utils = require("Módulo:I.Utils")
local trim = utils.trim
local colorize = utils.colorize

-- Fallback se colorize não existir
if not colorize or type(colorize) ~= "function" then
    colorize = function(txt)
        if not txt or txt == "" then
            return ""
        end
        -- Versão simplificada sem colorização
        return tostring(txt)
    end
end

local cUtils = require("Módulo:C.Utils")
local requireCharModule = cUtils.requireCharModule

-- FASE 2: Normaliza descrição para formato i18n
-- Aceita desc (table com pt/en/es/pl) ou campos legados (descPt, descEn, etc)
-- Sempre retorna desc_i18n no formato { pt: "...", en: "...", ... }
local function normalizeDesc(desc, descPt, descEn, descEs, descPl)
    local desc_i18n = {}

    -- Se desc é uma table, processa diretamente
    if desc and type(desc) == "table" then
        for code, text in pairs(desc) do
            if type(text) == "string" and text ~= "" then
                desc_i18n[code] = colorize(text)
            end
        end
    elseif type(desc) == "string" and desc ~= "" then
        -- Se desc é string, assume português
        desc_i18n.pt = colorize(desc)
    end

    -- Processa campos legados (descPt, descEn, etc) se existirem
    if descPt and type(descPt) == "string" and descPt ~= "" then
        desc_i18n.pt = colorize(descPt)
    end
    if descEn and type(descEn) == "string" and descEn ~= "" then
        desc_i18n.en = colorize(descEn)
    end
    if descEs and type(descEs) == "string" and descEs ~= "" then
        desc_i18n.es = colorize(descEs)
    end
    if descPl and type(descPl) == "string" and descPl ~= "" then
        desc_i18n.pl = colorize(descPl)
    end

    return next(desc_i18n) and desc_i18n or nil
end

-- Processa descrição (desc table → desc_i18n)
-- Mantido para compatibilidade, agora usa normalizeDesc internamente
local function processDesc(desc)
    if not desc then return nil end
    return normalizeDesc(desc)
end

-- Processa weapon (aplica colorize nas descrições)
local function processWeapon(weapon)
    if not weapon or type(weapon) ~= "table" then
        return weapon
    end

    local processed = {}
    -- Copia todos os campos do weapon
    for k, v in pairs(weapon) do
        processed[k] = v
    end

    -- FASE 2: Processa descrição do weapon usando normalizeDesc (normaliza campos legados)
    if weapon.desc or weapon.descPt or weapon.descEn or weapon.descEs or weapon.descPl then
        processed.desc_i18n = normalizeDesc(weapon.desc, weapon.descPt, weapon.descEn, weapon.descEs, weapon.descPl)
        -- Remove campos legados se desc_i18n foi criado (para evitar duplicação)
        if processed.desc_i18n then
            processed.desc = nil
            processed.descPt = nil
            processed.descEn = nil
            processed.descEs = nil
            processed.descPl = nil
        end
    end

    -- Se o weapon processado estiver vazio (sem campos), retorna nil
    -- Isso evita que weapons vazios {} sejam considerados válidos
    if next(processed) == nil then
        return nil
    end

    return processed
end

-- Processa subskills recursivamente (suporte ilimitado a níveis)
-- parentSkillsById: mapa de skills principais por ID (moduleName.skillName)
-- parentSkillsByName: mapa de skills principais por nome (compatibilidade)
-- parentPath: caminho hierárquico para gerar IDs únicos (ex: "skill1.subskill2")
-- moduleName: nome do módulo para gerar IDs únicos
local function processSubskills(subskills, suborder, parentSkillsById, parentSkillsByName, parentPath, moduleName)
    if not subskills or not suborder or type(subskills) ~= "table" or type(suborder) ~= "table" then
        return nil
    end

    local arr = {}
    for idx, subName in ipairs(suborder) do
        local sub = subskills[subName]
        if type(sub) == "table" then
            -- Gera ID único baseado no caminho hierárquico
            -- Se parentPath existe, usa "parentPath.subName", senão usa apenas "subName"
            -- Adiciona índice para garantir unicidade mesmo com nomes duplicados
            local uniqueId = parentPath and (parentPath .. "." .. subName .. "." .. tostring(idx)) or
                (subName .. "." .. tostring(idx))
            -- FASE 1: Herança explícita com suporte a ID
            -- inherit_from_id: ID da skill para herdar (prioridade)
            -- inherit_from: Nome da skill para herdar (fallback legado)
            -- inherit_fields: O QUE herdar (array de campos, obrigatório para herdar)
            -- Se não especificar ambos, não herda nada (mais seguro)

            local inheritFromId = sub.inherit_from_id      -- ID da skill (prioridade)
            local inheritFrom = sub.inherit_from           -- Nome da skill (fallback legado)
            local inheritFields = sub.inherit_fields or {} -- O QUE herdar (array)

            -- Converte inheritFields para set para busca rápida
            local inheritFieldsSet = {}
            if type(inheritFields) == "table" then
                for _, field in ipairs(inheritFields) do
                    if type(field) == "string" then
                        inheritFieldsSet[field] = true
                    end
                end
            end

            -- FASE 1: Busca skill principal por ID primeiro, depois por nome (fallback)
            local parentSkill = nil
            if inheritFromId and parentSkillsById and type(parentSkillsById) == "table" then
                parentSkill = parentSkillsById[inheritFromId]
            end
            -- Fallback legado: busca por nome
            if not parentSkill and inheritFrom and parentSkillsByName and type(parentSkillsByName) == "table" then
                parentSkill = parentSkillsByName[inheritFrom]
            end

            -- Função auxiliar: verifica se um campo DEVE ser herdado
            -- Só herda se inherit_from E inherit_fields estiverem definidos E o campo estiver na lista
            local function shouldInheritField(fieldName)
                if not inheritFrom or not parentSkill then
                    return false                           -- Sem inherit_from ou parentSkill, não herda
                end
                return inheritFieldsSet[fieldName] == true -- Só herda se estiver em inherit_fields
            end

            -- Valida flags: deve ser uma tabela com pelo menos um elemento, senão nil
            -- Flags herdam apenas se "flags" estiver em inherit_fields
            local subFlags = (sub.flags ~= nil) and sub.flags or
                (shouldInheritField("flags") and parentSkill and parentSkill.flags)
            local validSubFlags = (type(subFlags) == "table" and #subFlags > 0) and subFlags or nil

            -- Cria o objeto da subskill, herdando apenas campos explicitamente listados em inherit_fields
            local subObj = {
                id = uniqueId,          -- ID único para evitar colisão de nomes
                display_name = subName, -- Nome para exibição (sem função lógica)
                name = subName,         -- Mantido para compatibilidade
                n = subName,            -- Mantido para compatibilidade
                -- Herda icon apenas se "icon" estiver em inherit_fields
                icon = sub.icon or (shouldInheritField("icon") and parentSkill and parentSkill.icon) or "",
                -- Herda level apenas se "level" estiver em inherit_fields
                level = sub.level or (shouldInheritField("level") and parentSkill and parentSkill.level) or "",
                -- Herda energy apenas se "energy" estiver em inherit_fields
                energy = (sub.energy ~= nil) and sub.energy or
                    (shouldInheritField("energy") and parentSkill and parentSkill.energy),
                -- Herda powerpve apenas se "powerpve" estiver em inherit_fields
                powerpve = (sub.powerpve ~= nil) and sub.powerpve or
                    (shouldInheritField("powerpve") and parentSkill and parentSkill.powerpve),
                -- Herda powerpvp apenas se "powerpvp" estiver em inherit_fields
                powerpvp = (sub.powerpvp ~= nil) and sub.powerpvp or
                    (shouldInheritField("powerpvp") and parentSkill and parentSkill.powerpvp),
                -- Herda cooldown apenas se "cooldown" estiver em inherit_fields
                cooldown = (sub.cooldown ~= nil) and sub.cooldown or
                    (shouldInheritField("cooldown") and parentSkill and parentSkill.cooldown),
                -- Video sempre vem da subskill (nunca herda)
                video = sub.video or "",
                -- Descrição: será processada depois, herda apenas se "desc" estiver em inherit_fields
                desc_i18n = nil, -- Será definido depois
                -- Flags: herda apenas se "flags" estiver em inherit_fields
                flags = validSubFlags,
                -- Herda weapon apenas se "weapon" estiver em inherit_fields
                weapon = (sub.weapon and processWeapon(sub.weapon)) or
                    (shouldInheritField("weapon") and parentSkill and processWeapon(parentSkill.weapon)),
                -- Herda back apenas se "back" estiver em inherit_fields
                back = (sub.back ~= nil) and sub.back or
                    (shouldInheritField("back") and parentSkill and parentSkill.back),
                -- Inclui campos de herança para o frontend
                inherit_from_id = inheritFromId or inheritFrom, -- ID ou nome (compatibilidade)
                inherit_from = inheritFrom,                     -- Mantido para compatibilidade
                inherit_fields = inheritFields
            }

            -- FASE 2: Processa descrição usando normalizeDesc (normaliza campos legados)
            -- Herda apenas se "desc" estiver em inherit_fields
            -- PROTEÇÃO TOTAL: Se "desc" NÃO está em inherit_fields, desc_i18n NUNCA herda
            -- PRIORIDADE: Sempre processa descrição da subskill se existir (mesmo que "desc" esteja em inherit_fields)
            if sub.desc ~= nil or sub.descPt ~= nil or sub.descEn ~= nil or sub.descEs ~= nil or sub.descPl ~= nil then
                -- Sempre processa descrição da subskill se existir (prioridade máxima)
                -- Normaliza campos legados (descPt, descEn, etc) para desc_i18n
                subObj.desc_i18n = normalizeDesc(sub.desc, sub.descPt, sub.descEn, sub.descEs, sub.descPl)
            elseif shouldInheritField("desc") and parentSkill then
                -- Herda descrição apenas se "desc" estiver em inherit_fields
                -- Normaliza campos legados do parentSkill também
                -- Se parentSkill já tem desc_i18n processado, usa diretamente; senão normaliza
                if parentSkill.desc_i18n then
                    subObj.desc_i18n = parentSkill.desc_i18n
                else
                    subObj.desc_i18n = normalizeDesc(parentSkill.desc, parentSkill.descPt, parentSkill.descEn,
                        parentSkill.descEs, parentSkill.descPl)
                end
            else
                -- PROTEÇÃO: Se não tem desc na subskill e "desc" NÃO está em inherit_fields, desc_i18n é nil
                -- GARANTE que não há herança quando "desc" não está em inherit_fields
                subObj.desc_i18n = nil
            end

            -- RECURSÃO NATIVA: processa sub-subskills (e sub-sub-subskills, etc.)
            -- Passa parentSkillsById e parentSkillsByName para recursão (herança sempre vem das skills principais)
            -- Passa uniqueId como parentPath para manter unicidade na hierarquia
            if type(sub.subskills) == "table" and type(sub.suborder) == "table" then
                subObj.subs = processSubskills(sub.subskills, sub.suborder, parentSkillsById, parentSkillsByName,
                    uniqueId, moduleName)
                subObj.suborder = sub.suborder
            end

            table.insert(arr, subObj)
        end
    end

    return #arr > 0 and arr or nil
end

-- Gera JSON de skills a partir do módulo
function p.skill(frame)
    local parent = frame:getParent() or frame
    local args = parent.args or {}

    local moduleName = trim(args.module or "")
    if moduleName == "" then
        return "[]"
    end

    local data = requireCharModule(moduleName) or {}
    if not data.order or not data.skills then
        return "[]"
    end

    local skillsArr = {}

    -- Se houver forms, obtém a primeira forma disponível (genérico)
    local firstForm = nil
    local firstFormName = nil
    if type(data.forms) == "table" then
        -- Pega a primeira forma disponível (ordem não garantida em pairs, mas pega a primeira válida)
        for formName, formData in pairs(data.forms) do
            if type(formData) == "table" and type(formData.skills) == "table" and type(formData.order) == "table" then
                firstForm = formData
                firstFormName = formName
                break
            end
        end
    end

    -- Encontra a skill com form_switch = true (genérico)
    local formSwitchSkillName = nil
    local nextFixedSkillAfterFormSwitch = nil
    if type(data.skills) == "table" then
        for skillName, sk in pairs(data.skills) do
            if type(sk) == "table" and sk.form_switch == true then
                formSwitchSkillName = skillName
                break
            end
        end

        -- Encontra a próxima skill fixa depois do form_switch no order
        if formSwitchSkillName and type(data.order) == "table" then
            local foundFormSwitch = false
            for _, skillName in ipairs(data.order) do
                if foundFormSwitch then
                    -- Esta é a próxima skill fixa depois do form_switch
                    nextFixedSkillAfterFormSwitch = skillName
                    break
                end
                if skillName == formSwitchSkillName then
                    foundFormSwitch = true
                end
            end
        end
    end

    -- Se houver forms, insere a primeira skill da forma ANTES da skill com form_switch
    if firstForm and type(firstForm.skills) == "table" and type(firstForm.order) == "table" and #firstForm.order > 0 then
        local firstSkillName = firstForm.order[1]
        local formSk = firstForm.skills[firstSkillName]
        if type(formSk) == "table" then
            -- Valida flags: deve ser uma tabela com pelo menos um elemento, senão nil
            local formFlags = nil
            if type(formSk.flags) == "table" and #formSk.flags > 0 then
                formFlags = formSk.flags
            end

            local formSkillObj = {
                id = "form." .. firstSkillName, -- ID único para skills de forma
                display_name = firstSkillName,  -- Nome para exibição
                name = firstSkillName,          -- Mantido para compatibilidade
                n = firstSkillName,             -- Mantido para compatibilidade
                icon = (formSk.icon and formSk.icon ~= "") and formSk.icon or "Nada.png",
                level = formSk.level or "NIVEL",
                energy = formSk.energy,
                powerpve = formSk.powerpve,
                powerpvp = formSk.powerpvp,
                cooldown = formSk.cooldown,
                video = formSk.video or "",
                desc_i18n = normalizeDesc(formSk.desc, formSk.descPt, formSk.descEn, formSk.descEs, formSk.descPl),
                flags = formFlags,
                weapon = processWeapon(formSk.weapon)
            }
            if type(formSk.subskills) == "table" and type(formSk.suborder) == "table" then
                -- Para skills de forma, usa data.skills como fallback (ainda não processadas)
                -- Isso será corrigido quando as skills principais forem processadas
                local formSkillsById = {}
                local formSkillsByName = {}
                for sn, s in pairs(data.skills) do
                    if type(s) == "table" then
                        formSkillsById["form." .. sn] = s
                        formSkillsByName[sn] = s
                    end
                end
                formSkillObj.subs = processSubskills(formSk.subskills, formSk.suborder, formSkillsById, formSkillsByName,
                    formSkillObj.id, "form")
                formSkillObj.suborder = formSk.suborder
            end
            table.insert(skillsArr, formSkillObj)
        end
    end

    -- FASE 1: Cria mapa de IDs das skills principais para lookup por ID
    -- Este mapa será populado DEPOIS que as skills forem processadas
    local mainSkillsById = {}
    local mainSkillsByName = {}

    -- Itera sobre order para manter ordem correta (skills fixas)
    local nextFixedSkillIndex = -1
    for idx, skillName in ipairs(data.order) do
        local sk = data.skills[skillName]
        if type(sk) == "table" then
            -- Valida flags: deve ser uma tabela com pelo menos um elemento, senão nil
            -- Proteção extra: verifica se flags é uma tabela válida e serializável
            -- Valida flags: deve ser uma tabela com pelo menos um elemento, senão nil
            -- Simplificado: se é uma tabela válida com strings, inclui diretamente
            local validFlags = nil
            if type(sk.flags) == "table" and #sk.flags > 0 then
                -- Verifica se todos os elementos são strings válidas
                local allValid = true
                for i, flag in ipairs(sk.flags) do
                    if type(flag) ~= "string" or flag == "" then
                        allValid = false
                        break
                    end
                end
                if allValid then
                    validFlags = sk.flags
                else
                    -- Se houver elementos inválidos, cria uma cópia limpa
                    local cleanFlags = {}
                    for i, flag in ipairs(sk.flags) do
                        if type(flag) == "string" and flag ~= "" then
                            table.insert(cleanFlags, flag)
                        end
                    end
                    if #cleanFlags > 0 then
                        validFlags = cleanFlags
                    end
                end
            end

            -- FASE 1: ID único obrigatório para skills principais
            -- Usa moduleName como prefixo para garantir unicidade global
            local skillId = moduleName .. "." .. skillName

            local skillObj = {
                id = skillId,             -- ID único global (moduleName.skillName)
                display_name = skillName, -- Nome para exibição (sem função lógica)
                name = skillName,         -- Mantido para compatibilidade
                n = skillName,            -- Mantido para compatibilidade
                icon = (sk.icon and sk.icon ~= "") and sk.icon or "Nada.png",
                level = sk.level or "NIVEL",
                energy = sk.energy,
                powerpve = sk.powerpve,
                powerpvp = sk.powerpvp,
                cooldown = sk.cooldown,
                video = sk.video or "",
                desc_i18n = normalizeDesc(sk.desc, sk.descPt, sk.descEn, sk.descEs, sk.descPl),
                flags = validFlags,
                weapon = processWeapon(sk.weapon),
                weaponicon = data.weaponicon,
                form_switch = sk.form_switch,
                form_videos = sk.form_videos -- Vídeos específicos para transições de forma
            }

            -- Valida flags antes de inserir no array (proteção extra)
            if skillObj.flags and (type(skillObj.flags) ~= "table" or #skillObj.flags == 0) then
                skillObj.flags = nil
            end

            -- FASE 1: Popula mapa de IDs com skill processada ANTES de processar subskills
            -- Isso garante que as subskills possam herdar dos objetos processados (com desc_i18n)
            mainSkillsById[skillId] = skillObj
            mainSkillsByName[skillName] = skillObj

            -- Processa subskills recursivamente (suporte nativo ilimitado)
            -- Passa mainSkillsById e mainSkillsByName para lookup por ID ou nome
            -- Passa skillObj.id como parentPath para gerar IDs únicos para subskills
            if type(sk.subskills) == "table" and type(sk.suborder) == "table" then
                skillObj.subs = processSubskills(sk.subskills, sk.suborder, mainSkillsById, mainSkillsByName, skillObj
                .id, moduleName)
                skillObj.suborder = sk.suborder
            end

            table.insert(skillsArr, skillObj)

            -- Se for a skill com form_switch e houver forms, adiciona a terceira skill da forma logo após
            if skillName == formSwitchSkillName and firstForm and type(firstForm.skills) == "table" and type(firstForm.order) == "table" and #firstForm.order >= 2 then
                local thirdSkillName = firstForm.order[2] -- Segunda na ordem = terceira skill (1, 2, 3)
                local formSk = firstForm.skills[thirdSkillName]
                if type(formSk) == "table" then
                    -- Valida flags: deve ser uma tabela com pelo menos um elemento, senão nil
                    local formFlags3 = nil
                    if type(formSk.flags) == "table" and #formSk.flags > 0 then
                        formFlags3 = formSk.flags
                    end

                    local formSkillObj = {
                        id = "form." .. thirdSkillName, -- ID único para skills de forma
                        display_name = thirdSkillName,  -- Nome para exibição
                        name = thirdSkillName,          -- Mantido para compatibilidade
                        n = thirdSkillName,             -- Mantido para compatibilidade
                        icon = (formSk.icon and formSk.icon ~= "") and formSk.icon or "Nada.png",
                        level = formSk.level or "NIVEL",
                        energy = formSk.energy,
                        powerpve = formSk.powerpve,
                        powerpvp = formSk.powerpvp,
                        cooldown = formSk.cooldown,
                        video = formSk.video or "",
                        desc_i18n = normalizeDesc(formSk.desc, formSk.descPt, formSk.descEn, formSk.descEs, formSk
                            .descPl),
                        flags = formFlags3,
                        weapon = processWeapon(formSk.weapon)
                    }
                    if type(formSk.subskills) == "table" and type(formSk.suborder) == "table" then
                        formSkillObj.subs = processSubskills(formSk.subskills, formSk.suborder, mainSkillsById,
                            mainSkillsByName, formSkillObj.id, "form")
                        formSkillObj.suborder = formSk.suborder
                    end
                    table.insert(skillsArr, formSkillObj)
                end
            end

            -- Guarda índice da próxima skill fixa depois do form_switch para inserir a quinta skill depois
            if skillName == nextFixedSkillAfterFormSwitch then
                nextFixedSkillIndex = #skillsArr
            end

            -- Se for a próxima skill fixa depois do form_switch e houver forms, adiciona a quinta skill da forma logo após
            if skillName == nextFixedSkillAfterFormSwitch and firstForm and type(firstForm.skills) == "table" and type(firstForm.order) == "table" and #firstForm.order >= 3 then
                local fifthSkillName = firstForm.order[3] -- Terceira na ordem = quinta skill (1, 2, 3)
                local formSk = firstForm.skills[fifthSkillName]
                if type(formSk) == "table" then
                    -- Valida flags: deve ser uma tabela com pelo menos um elemento, senão nil
                    local formFlags5 = nil
                    if type(formSk.flags) == "table" and #formSk.flags > 0 then
                        formFlags5 = formSk.flags
                    end

                    local formSkillObj = {
                        id = "form." .. fifthSkillName, -- ID único para skills de forma
                        display_name = fifthSkillName,  -- Nome para exibição
                        name = fifthSkillName,          -- Mantido para compatibilidade
                        n = fifthSkillName,             -- Mantido para compatibilidade
                        icon = (formSk.icon and formSk.icon ~= "") and formSk.icon or "Nada.png",
                        level = formSk.level or "NIVEL",
                        energy = formSk.energy,
                        powerpve = formSk.powerpve,
                        powerpvp = formSk.powerpvp,
                        cooldown = formSk.cooldown,
                        video = formSk.video or "",
                        desc_i18n = normalizeDesc(formSk.desc, formSk.descPt, formSk.descEn, formSk.descEs, formSk
                            .descPl),
                        flags = formFlags5,
                        weapon = processWeapon(formSk.weapon)
                    }
                    if type(formSk.subskills) == "table" and type(formSk.suborder) == "table" then
                        formSkillObj.subs = processSubskills(formSk.subskills, formSk.suborder, mainSkillsById,
                            mainSkillsByName, formSkillObj.id, "form")
                        formSkillObj.suborder = formSk.suborder
                    end
                    table.insert(skillsArr, formSkillObj)
                end
            end
        end
    end

    -- Proteção contra erros no jsonEncode (especialmente para flags)
    -- Limpa flags problemáticas ANTES de tentar serializar
    -- NOTA: Não remove flags válidas, apenas valida e limpa flags inválidas
    for _, skill in ipairs(skillsArr) do
        if skill.flags then
            if type(skill.flags) ~= "table" or #skill.flags == 0 then
                skill.flags = nil
            else
                -- Limpa flags inválidas (não strings ou vazias)
                local cleanFlags = {}
                for i, flag in ipairs(skill.flags) do
                    if type(flag) == "string" and flag ~= "" then
                        table.insert(cleanFlags, flag)
                    end
                end
                if #cleanFlags > 0 then
                    skill.flags = cleanFlags
                else
                    skill.flags = nil
                end
            end
        end
        -- Limpa flags em subskills também
        if skill.subs and type(skill.subs) == "table" then
            for _, sub in ipairs(skill.subs) do
                if sub.flags then
                    if type(sub.flags) ~= "table" or #sub.flags == 0 then
                        sub.flags = nil
                    else
                        local cleanSubFlags = {}
                        for i, flag in ipairs(sub.flags) do
                            if type(flag) == "string" and flag ~= "" then
                                table.insert(cleanSubFlags, flag)
                            end
                        end
                        if #cleanSubFlags > 0 then
                            sub.flags = cleanSubFlags
                        else
                            sub.flags = nil
                        end
                    end
                end
            end
        end
    end

    local ok, jsonResult = pcall(function()
        return mw.text.jsonEncode(skillsArr)
    end)
    if ok and jsonResult then
        return jsonResult
    else
        -- Última tentativa: remove TODAS as flags
        for _, skill in ipairs(skillsArr) do
            skill.flags = nil
            if skill.subs and type(skill.subs) == "table" then
                for _, sub in ipairs(skill.subs) do
                    sub.flags = nil
                end
            end
        end
        return mw.text.jsonEncode(skillsArr)
    end
end

-- Gera JSON de forms a partir do módulo
function p.forms(frame)
    local parent = frame:getParent() or frame
    local args = parent.args or {}

    local moduleName = trim(args.module or "")
    if moduleName == "" then
        return "{}"
    end

    local data = requireCharModule(moduleName) or {}
    if not data.forms or type(data.forms) ~= "table" then
        return "{}"
    end

    local formsData = {}
    for formName, formData in pairs(data.forms) do
        if type(formData) == "table" and type(formData.skills) == "table" and type(formData.order) == "table" then
            local formSkills = {}
            for _, skillName in ipairs(formData.order) do
                local sk = formData.skills[skillName]
                if type(sk) == "table" then
                    -- Valida flags: deve ser uma tabela com pelo menos um elemento, senão nil
                    local formSkillFlags = nil
                    if type(sk.flags) == "table" and #sk.flags > 0 then
                        formSkillFlags = sk.flags
                    end

                    local skillObj = {
                        name = skillName,
                        n = skillName,
                        icon = (sk.icon and sk.icon ~= "") and sk.icon or "Nada.png",
                        level = sk.level or "NIVEL",
                        energy = sk.energy,
                        powerpve = sk.powerpve,
                        powerpvp = sk.powerpvp,
                        cooldown = sk.cooldown,
                        video = sk.video or "",
                        desc_i18n = normalizeDesc(sk.desc, sk.descPt, sk.descEn, sk.descEs, sk.descPl),
                        flags = formSkillFlags,
                        weapon = processWeapon(sk.weapon)
                    }
                    -- Processa subskills se houver
                    -- Passa data.skills para herança automática quando subskill tem mesmo nome que skill principal
                    if type(sk.subskills) == "table" and type(sk.suborder) == "table" then
                        skillObj.subs = processSubskills(sk.subskills, sk.suborder, data.skills)
                        skillObj.suborder = sk.suborder
                    end
                    table.insert(formSkills, skillObj)
                end
            end
            formsData[formName] = {
                order = formData.order,
                skills = formSkills
            }
        end
    end

    return mw.text.jsonEncode(formsData)
end

-- Gera JSON de skins a partir do módulo
function p.skin(frame)
    local parent = frame:getParent() or frame
    local args = parent.args or {}

    local moduleName = trim(args.module or "")
    if moduleName == "" then
        return "[]"
    end

    local data = requireCharModule(moduleName) or {}
    if not data.skins or type(data.skins) ~= "table" then
        return "[]"
    end

    -- Processa skins do módulo
    local skinsArr = {}
    for _, skin in ipairs(data.skins) do
        if type(skin) == "table" then
            local skinObj = {
                name = skin.name or "",
                sprite = skin.sprite or "",
                tooltip = skin.tooltip or "",
                offset_x = skin.offset_x,
                tile = skin.tile or "",
                tile_x = skin.tile_x,
                tile_y = skin.tile_y,
                youtube = skin.youtube
            }

            -- Se tooltip é table, converte para JSON string
            if type(skin.tooltip) == "table" then
                skinObj.tooltip = mw.text.jsonEncode(skin.tooltip)
            end

            table.insert(skinsArr, skinObj)
        end
    end

    return mw.text.jsonEncode(skinsArr)
end

return p