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

De Wiki Gla
Ir para navegação Ir para pesquisar
m
m
 
(48 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 20: Linha 20:
local requireCharModule = cUtils.requireCharModule
local requireCharModule = cUtils.requireCharModule


-- Processa descrição (desc table desc_i18n)
-- FASE 2: Normaliza descrição para formato i18n
local function processDesc(desc)
-- Aceita desc (table com pt/en/es/pl) ou campos legados (descPt, descEn, etc)
     if not desc then return nil end
-- Sempre retorna desc_i18n no formato { pt: "...", en: "...", ... }
    if type(desc) == "string" then
local function normalizeDesc(desc, descPt, descEn, descEs, descPl)
        return { pt = colorize(desc) }
     local desc_i18n = {}
     end
 
     if type(desc) == "table" then
     -- Se desc é uma table, processa diretamente
        local desc_i18n = {}
     if desc and type(desc) == "table" then
         for code, text in pairs(desc) do
         for code, text in pairs(desc) do
             if type(text) == "string" and text ~= "" then
             if type(text) == "string" and text ~= "" then
Linha 33: Linha 33:
             end
             end
         end
         end
        return next(desc_i18n) and desc_i18n or nil
    elseif type(desc) == "string" and desc ~= "" then
        -- Se desc é string, assume português
        desc_i18n.pt = colorize(desc)
     end
     end
     return nil
 
    -- 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
end


Linha 50: Linha 74:
     end
     end


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


Linha 63: Linha 97:


-- Processa subskills recursivamente (suporte ilimitado a níveis)
-- Processa subskills recursivamente (suporte ilimitado a níveis)
local function processSubskills(subskills, suborder)
-- 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
     if not subskills or not suborder or type(subskills) ~= "table" or type(suborder) ~= "table" then
         return nil
         return nil
Linha 69: Linha 107:


     local arr = {}
     local arr = {}
     for _, subName in ipairs(suborder) do
     for idx, subName in ipairs(suborder) do
         local sub = subskills[subName]
         local sub = subskills[subName]
         if type(sub) == "table" then
         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
            -- Fallback: se sub.flags é string "aggro,bridge" (formato corrompido), converte para table
            local subFlagsRaw = (sub.flags ~= nil) and sub.flags or
                (shouldInheritField("flags") and parentSkill and parentSkill.flags)
            local subFlags = subFlagsRaw
            if type(subFlagsRaw) == "string" and subFlagsRaw:match("%S") then
                local parts = {}
                for part in (subFlagsRaw .. ","):gmatch("([^,]*)") do
                    part = trim(part)
                    if part ~= "" then table.insert(parts, part) end
                end
                if #parts > 0 then subFlags = parts end
            end
            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 = {
             local subObj = {
                 name = subName,
                id = uniqueId,          -- ID único para evitar colisão de nomes
                 n = subName,
                display_name = subName, -- Nome para exibição (sem função lógica)
                 icon = sub.icon or "",
                 name = subName,         -- Mantido para compatibilidade
                 level = sub.level or "",
                 n = subName,           -- Mantido para compatibilidade
                 energy = sub.energy,
                -- Herda icon apenas se "icon" estiver em inherit_fields
                 powerpve = sub.powerpve,
                 icon = sub.icon or (shouldInheritField("icon") and parentSkill and parentSkill.icon) or "",
                 powerpvp = sub.powerpvp,
                -- Herda level apenas se "level" estiver em inherit_fields
                 cooldown = sub.cooldown,
                 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 "",
                 video = sub.video or "",
                 desc_i18n = processDesc(sub.desc),
                -- Descrição: sempre vem da subskill (nunca herda)
                 flags = sub.flags,
                 desc_i18n = nil, -- Será definido depois
                 weapon = processWeapon(sub.weapon),
                -- Flags: herda apenas se "flags" estiver em inherit_fields
                 back = sub.back
                 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
             }
             }
            -- Descrição: sempre vem da subskill, nunca herda
            -- PROTEÇÃO TOTAL: NUNCA copia descrição do parentSkill, mesmo que subskill não tenha
            if sub.desc ~= nil or sub.descPt ~= nil or sub.descEn ~= nil or sub.descEs ~= nil or sub.descPl ~= nil then
                subObj.desc_i18n = normalizeDesc(sub.desc, sub.descPt, sub.descEn, sub.descEs, sub.descPl)
            else
                subObj.desc_i18n = nil
            end
            -- GARANTIA: Remove qualquer campo legado de descrição que possa ter sido copiado acidentalmente
            subObj.desc = nil
            subObj.descPt = nil
            subObj.descEn = nil
            subObj.descEs = nil
            subObj.descPl = nil


             -- RECURSÃO NATIVA: processa sub-subskills (e sub-sub-subskills, etc.)
             -- 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
             if type(sub.subskills) == "table" and type(sub.suborder) == "table" then
                 subObj.subs = processSubskills(sub.subskills, sub.suborder)
                 subObj.subs = processSubskills(sub.subskills, sub.suborder, parentSkillsById, parentSkillsByName,
                    uniqueId, moduleName)
                 subObj.suborder = sub.suborder
                 subObj.suborder = sub.suborder
             end
             end
Linha 118: Linha 257:
     local skillsArr = {}
     local skillsArr = {}


     -- Itera sobre order para manter ordem correta
    -- Se houver forms, obtém a primeira forma (usa forms_order se existir, ex: Chopper TS inicia em Brain Point)
    local firstForm = nil
    local firstFormName = nil
    if type(data.forms) == "table" then
        if type(data.forms_order) == "table" and #data.forms_order > 0 then
            for _, formName in ipairs(data.forms_order) do
                local formData = data.forms[formName]
                if type(formData) == "table" and type(formData.skills) == "table" and type(formData.order) == "table" then
                    firstForm = formData
                    firstFormName = formName
                    break
                end
            end
        end
        if not firstForm then
            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
    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
 
    -- FASE 1: Cria mapa de IDs das skills principais para lookup por ID
    -- Este mapa será usado por processSubskills para buscar skills por ID
    -- IMPORTANTE: Deve ser criado ANTES de processar forms para que possa ser usado em subskills de forms
    local mainSkillsById = {}
    local mainSkillsByName = {}
 
    -- Pre-popula o mapa com todas as skills principais (necessário para forms também)
    for skillName, sk in pairs(data.skills) do
        if type(sk) == "table" then
            local skillId = moduleName .. "." .. skillName
            mainSkillsById[skillId] = sk
            mainSkillsByName[skillName] = sk
        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 "",
                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),
                effect = formSk.effect
            }
            if type(formSk.subskills) == "table" and type(formSk.suborder) == "table" then
                -- Para skills de forma, usa mainSkillsById e mainSkillsByName
                formSkillObj.subs = processSubskills(formSk.subskills, formSk.suborder, mainSkillsById, mainSkillsByName,
                    formSkillObj.id, "form")
                formSkillObj.suborder = formSk.suborder
            end
            table.insert(skillsArr, formSkillObj)
        end
    end
 
     -- Itera sobre order para manter ordem correta (skills fixas)
    local nextFixedSkillIndex = -1
     for idx, skillName in ipairs(data.order) do
     for idx, skillName in ipairs(data.order) do
         local sk = data.skills[skillName]
         local sk = data.skills[skillName]
         if type(sk) == "table" then
         if type(sk) == "table" then
            -- Valida flags: deve ser uma tabela com pelo menos um elemento, senão nil
            -- Fallback: se flags veio como string "aggro,bridge" (formato corrompido pelo editor antigo), converte para table
            local flagsToValidate = sk.flags
            if type(sk.flags) == "string" and sk.flags:match("%S") then
                local parts = {}
                for part in (sk.flags .. ","):gmatch("([^,]*)") do
                    part = trim(part)
                    if part ~= "" then table.insert(parts, part) end
                end
                if #parts > 0 then flagsToValidate = parts end
            end
            local validFlags = nil
            if type(flagsToValidate) == "table" and #flagsToValidate > 0 then
                -- Verifica se todos os elementos são strings válidas
                local allValid = true
                for i, flag in ipairs(flagsToValidate) do
                    if type(flag) ~= "string" or flag == "" then
                        allValid = false
                        break
                    end
                end
                if allValid then
                    validFlags = flagsToValidate
                else
                    -- Se houver elementos inválidos, cria uma cópia limpa
                    local cleanFlags = {}
                    for i, flag in ipairs(flagsToValidate) 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 = {
             local skillObj = {
                 name = skillName,
                id = skillId,            -- ID único global (moduleName.skillName)
                 n = skillName,
                display_name = skillName, -- Nome para exibição (sem função lógica)
                 icon = sk.icon or "Nada.png",
                 name = skillName,         -- Mantido para compatibilidade
                 n = skillName,           -- Mantido para compatibilidade
                 icon = (sk.icon and sk.icon ~= "") and sk.icon or "",
                 level = sk.level or "NIVEL",
                 level = sk.level or "NIVEL",
                 energy = sk.energy,
                 energy = sk.energy,
Linha 132: Linha 420:
                 cooldown = sk.cooldown,
                 cooldown = sk.cooldown,
                 video = sk.video or "",
                 video = sk.video or "",
                 desc_i18n = processDesc(sk.desc),
                 desc_i18n = normalizeDesc(sk.desc, sk.descPt, sk.descEn, sk.descEs, sk.descPl),
                 flags = sk.flags,
                 flags = validFlags,
                 weapon = processWeapon(sk.weapon),
                 weapon = processWeapon(sk.weapon),
                 weaponicon = data.weaponicon
                effect = sk.effect,
                 weaponicon = data.weaponicon,
                form_switch = sk.form_switch,
                form_videos = sk.form_videos,
                -- Sistema genérico de swap de personagens (ex: Buchi & Sham, futuros personagens)
                swap = sk.swap,                          -- true = skill de troca entre personagens
                only_character = sk.only_character or "", -- String: nome do personagem que pode usar (ex: "Buchi", "Sham")
                character_videos = sk.character_videos or
                    {}                                    -- Tabela: { "CharacterName": "video.mp4" } - vídeos por personagem
             }
             }


             -- Processa subskills recursivamente (suporte nativo ilimitado)
             -- 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
             if type(sk.subskills) == "table" and type(sk.suborder) == "table" then
                 skillObj.subs = processSubskills(sk.subskills, sk.suborder)
                 skillObj.subs = processSubskills(sk.subskills, sk.suborder, mainSkillsById, mainSkillsByName, skillObj
                    .id, moduleName)
                 skillObj.suborder = sk.suborder
                 skillObj.suborder = sk.suborder
            end
            -- 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
             end


             table.insert(skillsArr, skillObj)
             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 "",
                        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),
                        effect = formSk.effect
                    }
                    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 "",
                        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),
                        effect = formSk.effect
                    }
                    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
     end
     end


     return mw.text.jsonEncode(skillsArr)
     -- Adiciona emote no final (se existir no módulo)
    if type(data.emote) == "table" and data.emote.video and data.emote.video ~= "" then
        local emoteObj = {
            id = moduleName .. ".Emote",
            display_name = "Emote",
            name = "Emote",
            n = "Emote",
            icon = "Emote.png",
            level = "25",
            video = data.emote.video or "",
            desc = "",
            desc_i18n = nil
        }
        table.insert(skillsArr, emoteObj)
    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 = {}
    local function addForm(formName, formData)
        if type(formData) ~= "table" or type(formData.skills) ~= "table" or type(formData.order) ~= "table" then
            return
        end
        if formsData[formName] then return end -- já adicionado
        local formSkills = {}
        for _, skillName in ipairs(formData.order) do
            local sk = formData.skills[skillName]
            if type(sk) == "table" then
                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 "",
                    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)
                }
                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
 
    -- Usa forms_order se existir (ex: Chopper TS = Brain Point primeiro) para ordem consistente no JSON
    if type(data.forms_order) == "table" and #data.forms_order > 0 then
        for _, formName in ipairs(data.forms_order) do
            local formData = data.forms[formName]
            if formData then addForm(formName, formData) end
        end
    end
    for formName, formData in pairs(data.forms) do
        addForm(formName, formData)
    end
 
    return mw.text.jsonEncode(formsData)
end
end



Edição atual tal como às 23h09min de 21 de fevereiro 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
            -- Fallback: se sub.flags é string "aggro,bridge" (formato corrompido), converte para table
            local subFlagsRaw = (sub.flags ~= nil) and sub.flags or
                (shouldInheritField("flags") and parentSkill and parentSkill.flags)
            local subFlags = subFlagsRaw
            if type(subFlagsRaw) == "string" and subFlagsRaw:match("%S") then
                local parts = {}
                for part in (subFlagsRaw .. ","):gmatch("([^,]*)") do
                    part = trim(part)
                    if part ~= "" then table.insert(parts, part) end
                end
                if #parts > 0 then subFlags = parts end
            end
            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: sempre vem da subskill (nunca herda)
                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
            }

            -- Descrição: sempre vem da subskill, nunca herda
            -- PROTEÇÃO TOTAL: NUNCA copia descrição do parentSkill, mesmo que subskill não tenha
            if sub.desc ~= nil or sub.descPt ~= nil or sub.descEn ~= nil or sub.descEs ~= nil or sub.descPl ~= nil then
                subObj.desc_i18n = normalizeDesc(sub.desc, sub.descPt, sub.descEn, sub.descEs, sub.descPl)
            else
                subObj.desc_i18n = nil
            end
            -- GARANTIA: Remove qualquer campo legado de descrição que possa ter sido copiado acidentalmente
            subObj.desc = nil
            subObj.descPt = nil
            subObj.descEn = nil
            subObj.descEs = nil
            subObj.descPl = nil

            -- 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 (usa forms_order se existir, ex: Chopper TS inicia em Brain Point)
    local firstForm = nil
    local firstFormName = nil
    if type(data.forms) == "table" then
        if type(data.forms_order) == "table" and #data.forms_order > 0 then
            for _, formName in ipairs(data.forms_order) do
                local formData = data.forms[formName]
                if type(formData) == "table" and type(formData.skills) == "table" and type(formData.order) == "table" then
                    firstForm = formData
                    firstFormName = formName
                    break
                end
            end
        end
        if not firstForm then
            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
    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

    -- FASE 1: Cria mapa de IDs das skills principais para lookup por ID
    -- Este mapa será usado por processSubskills para buscar skills por ID
    -- IMPORTANTE: Deve ser criado ANTES de processar forms para que possa ser usado em subskills de forms
    local mainSkillsById = {}
    local mainSkillsByName = {}

    -- Pre-popula o mapa com todas as skills principais (necessário para forms também)
    for skillName, sk in pairs(data.skills) do
        if type(sk) == "table" then
            local skillId = moduleName .. "." .. skillName
            mainSkillsById[skillId] = sk
            mainSkillsByName[skillName] = sk
        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 "",
                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),
                effect = formSk.effect
            }
            if type(formSk.subskills) == "table" and type(formSk.suborder) == "table" then
                -- Para skills de forma, usa mainSkillsById e mainSkillsByName
                formSkillObj.subs = processSubskills(formSk.subskills, formSk.suborder, mainSkillsById, mainSkillsByName,
                    formSkillObj.id, "form")
                formSkillObj.suborder = formSk.suborder
            end
            table.insert(skillsArr, formSkillObj)
        end
    end

    -- 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
            -- Fallback: se flags veio como string "aggro,bridge" (formato corrompido pelo editor antigo), converte para table
            local flagsToValidate = sk.flags
            if type(sk.flags) == "string" and sk.flags:match("%S") then
                local parts = {}
                for part in (sk.flags .. ","):gmatch("([^,]*)") do
                    part = trim(part)
                    if part ~= "" then table.insert(parts, part) end
                end
                if #parts > 0 then flagsToValidate = parts end
            end
            local validFlags = nil
            if type(flagsToValidate) == "table" and #flagsToValidate > 0 then
                -- Verifica se todos os elementos são strings válidas
                local allValid = true
                for i, flag in ipairs(flagsToValidate) do
                    if type(flag) ~= "string" or flag == "" then
                        allValid = false
                        break
                    end
                end
                if allValid then
                    validFlags = flagsToValidate
                else
                    -- Se houver elementos inválidos, cria uma cópia limpa
                    local cleanFlags = {}
                    for i, flag in ipairs(flagsToValidate) 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 "",
                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),
                effect = sk.effect,
                weaponicon = data.weaponicon,
                form_switch = sk.form_switch,
                form_videos = sk.form_videos,
                -- Sistema genérico de swap de personagens (ex: Buchi & Sham, futuros personagens)
                swap = sk.swap,                           -- true = skill de troca entre personagens
                only_character = sk.only_character or "", -- String: nome do personagem que pode usar (ex: "Buchi", "Sham")
                character_videos = sk.character_videos or
                    {}                                    -- Tabela: { "CharacterName": "video.mp4" } - vídeos por personagem
            }

            -- 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

            -- 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

            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 "",
                        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),
                        effect = formSk.effect
                    }
                    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 "",
                        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),
                        effect = formSk.effect
                    }
                    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

    -- Adiciona emote no final (se existir no módulo)
    if type(data.emote) == "table" and data.emote.video and data.emote.video ~= "" then
        local emoteObj = {
            id = moduleName .. ".Emote",
            display_name = "Emote",
            name = "Emote",
            n = "Emote",
            icon = "Emote.png",
            level = "25",
            video = data.emote.video or "",
            desc = "",
            desc_i18n = nil
        }
        table.insert(skillsArr, emoteObj)
    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 = {}
    local function addForm(formName, formData)
        if type(formData) ~= "table" or type(formData.skills) ~= "table" or type(formData.order) ~= "table" then
            return
        end
        if formsData[formName] then return end -- já adicionado
        local formSkills = {}
        for _, skillName in ipairs(formData.order) do
            local sk = formData.skills[skillName]
            if type(sk) == "table" then
                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 "",
                    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)
                }
                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

    -- Usa forms_order se existir (ex: Chopper TS = Brain Point primeiro) para ordem consistente no JSON
    if type(data.forms_order) == "table" and #data.forms_order > 0 then
        for _, formName in ipairs(data.forms_order) do
            local formData = data.forms[formName]
            if formData then addForm(formName, formData) end
        end
    end
    for formName, formData in pairs(data.forms) do
        addForm(formName, formData)
    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